[
  {
    "path": ".github/actions/install/action.yml",
    "content": "name: \"Browsercore install\"\ndescription: \"Install deps for the project browsercore\"\n\ninputs:\n  arch:\n    description: 'CPU arch used to select the v8 lib'\n    required: false\n    default: 'x86_64'\n  os:\n    description: 'OS used to select the v8 lib'\n    required: false\n    default: 'linux'\n  zig-v8:\n    description: 'zig v8 version to install'\n    required: false\n    default: 'v0.3.4'\n  v8:\n    description: 'v8 version to install'\n    required: false\n    default: '14.0.365.4'\n  cache-dir:\n    description: 'cache dir to use'\n    required: false\n    default: '~/.cache'\n  debug:\n    description: 'enable v8 pre-built debug version, only available for linux x86_64'\n    required: false\n    default: 'false'\n\nruns:\n  using: \"composite\"\n\n  steps:\n    - name: Install apt deps\n      if: ${{ inputs.os == 'linux' }}\n      shell: bash\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y wget xz-utils ca-certificates clang make git\n\n    # Zig version used from the `minimum_zig_version` field in build.zig.zon\n    - uses: mlugg/setup-zig@v2\n\n    # Rust Toolchain for html5ever\n    - uses: dtolnay/rust-toolchain@stable\n\n    - name: Cache v8\n      id: cache-v8\n      uses: actions/cache@v5\n      env:\n        cache-name: cache-v8\n      with:\n        path: ${{ inputs.cache-dir }}/v8\n        key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}${{inputs.debug == 'true' && '_debug' || '' }}.a\n\n    - if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}\n      shell: bash\n      run: |\n        mkdir -p ${{ inputs.cache-dir }}/v8\n\n        wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}${{inputs.debug == 'true' && '_debug' || '' }}.a\n\n    - name: install v8\n      shell: bash\n      run: |\n        mkdir -p v8\n        ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8${{inputs.debug == 'true' && '_debug' || '' }}.a\n"
  },
  {
    "path": ".github/workflows/cla.yml",
    "content": "name: \"CLA Assistant\"\non:\n  issue_comment:\n    types: [created]\n  pull_request_target:\n    types: [opened,closed,synchronize]\n\npermissions:\n  actions: write\n  contents: read\n  pull-requests: write\n  statuses: write\n\njobs:\n  CLAAssistant:\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n      - name: \"CLA Assistant\"\n        if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'\n        uses: contributor-assistant/github-action@v2.6.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_GH_PAT }}\n        with:\n          path-to-signatures: 'signatures/browser/version1/cla.json'\n          path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'\n          # branch should not be protected\n          branch: 'main'\n          allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex\n\n          remote-organization-name: lightpanda-io\n          remote-repository-name: cla\n"
  },
  {
    "path": ".github/workflows/e2e-integration-test.yml",
    "content": "name: e2e-integration-test\n\nenv:\n  LIGHTPANDA_DISABLE_TELEMETRY: true\n\non:\n  schedule:\n    - cron: \"4 4 * * *\"\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  zig-build-release:\n    name: zig build release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    # Don't run the CI with draft PR.\n    if: github.event.pull_request.draft == false\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n\n      - name: zig build release\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: lightpanda-build-release\n          path: |\n            zig-out/bin/lightpanda\n          retention-days: 1\n\n  demo-scripts:\n    name: demo-integration-scripts\n    needs: zig-build-release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          repository: 'lightpanda-io/demo'\n          fetch-depth: 0\n\n      - run: npm install\n\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      - name: run end to end integration tests\n        run: |\n          ./lightpanda serve --log_level error & echo $! > LPD.pid\n          go run integration/main.go\n          kill `cat LPD.pid`\n"
  },
  {
    "path": ".github/workflows/e2e-test.yml",
    "content": "name: e2e-test\n\nenv:\n  AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}\n  AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}\n  AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}\n  LIGHTPANDA_DISABLE_TELEMETRY: true\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \".github/**\"\n      - \"src/**\"\n      - \"build.zig\"\n      - \"build.zig.zon\"\n\n  pull_request:\n\n    # By default GH trigger on types opened, synchronize and reopened.\n    # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request\n    # Since we skip the job when the PR is in draft state, we want to force CI\n    # running when the PR is marked ready_for_review w/o other change.\n    # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917\n    types: [opened, synchronize, reopened, ready_for_review]\n\n    paths:\n      - \".github/**\"\n      - \"src/**\"\n      - \"build.zig\"\n      - \"build.zig.zon\"\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  zig-build-release:\n    name: zig build release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    # Don't run the CI with draft PR.\n    if: github.event.pull_request.draft == false\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n\n      - name: zig build release\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: lightpanda-build-release\n          path: |\n            zig-out/bin/lightpanda\n          retention-days: 1\n\n  demo-scripts:\n    name: demo-scripts\n    needs: zig-build-release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          repository: 'lightpanda-io/demo'\n          fetch-depth: 0\n\n      - run: npm install\n\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      - name: run end to end tests\n        run: |\n          ./lightpanda serve & echo $! > LPD.pid\n          go run runner/main.go\n          kill `cat LPD.pid`\n\n      - name: build proxy\n        run: |\n          cd proxy\n          go build\n\n      - name: run end to end tests through proxy\n        run: |\n          ./proxy/proxy & echo $! > PROXY.id\n          ./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid\n          go run runner/main.go\n          kill `cat LPD.pid` `cat PROXY.id`\n\n      - name: run request interception through proxy\n        run: |\n          export PROXY_USERNAME=username PROXY_PASSWORD=password\n          ./proxy/proxy & echo $! > PROXY.id\n          ./lightpanda serve & echo $! > LPD.pid\n          URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js\n          BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js\n          kill `cat LPD.pid` `cat PROXY.id`\n\n  # e2e tests w/ web-bot-auth configuration on.\n  wba-demo-scripts:\n    name: wba-demo-scripts\n    needs: zig-build-release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          repository: 'lightpanda-io/demo'\n          fetch-depth: 0\n\n      - run: npm install\n\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      - run: echo \"${{ secrets.WBA_PRIVATE_KEY_PEM }}\" > private_key.pem\n\n      - name: run end to end tests\n        run: |\n          ./lightpanda serve \\\n            --web_bot_auth_key_file private_key.pem \\\n            --web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \\\n            --web_bot_auth_domain ${{ vars.WBA_DOMAIN }} \\\n            & echo $! > LPD.pid\n          go run runner/main.go\n          kill `cat LPD.pid`\n\n      - name: build proxy\n        run: |\n          cd proxy\n          go build\n\n      - name: run end to end tests through proxy\n        run: |\n          ./proxy/proxy & echo $! > PROXY.id\n          ./lightpanda serve \\\n            --web_bot_auth_key_file private_key.pem \\\n            --web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \\\n            --web_bot_auth_domain ${{ vars.WBA_DOMAIN }} \\\n            --http_proxy 'http://127.0.0.1:3000' \\\n            & echo $! > LPD.pid\n          go run runner/main.go\n          kill `cat LPD.pid` `cat PROXY.id`\n\n      - name: run request interception through proxy\n        run: |\n          export PROXY_USERNAME=username PROXY_PASSWORD=password\n          ./proxy/proxy & echo $! > PROXY.id\n          ./lightpanda serve & echo $! > LPD.pid\n          URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js\n          BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js\n          kill `cat LPD.pid` `cat PROXY.id`\n\n  wba-test:\n    name: wba-test\n    needs: zig-build-release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          repository: 'lightpanda-io/demo'\n          fetch-depth: 0\n\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      # force a wakup of the auth server before requesting it w/ the test itself\n      - run: curl https://${{ vars.WBA_DOMAIN }}\n\n      - name: run wba test\n        shell: bash\n        run: |\n          node webbotauth/validator.js &\n          VALIDATOR_PID=$!\n          sleep 5\n\n          exec 3<<< \"${{ secrets.WBA_PRIVATE_KEY_PEM }}\"\n\n          ./lightpanda fetch --dump http://127.0.0.1:8989/ \\\n            --web_bot_auth_key_file /proc/self/fd/3 \\\n            --web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \\\n            --web_bot_auth_domain ${{ vars.WBA_DOMAIN }}\n\n          wait $VALIDATOR_PID\n          exec 3>&-\n\n  cdp-and-hyperfine-bench:\n    name: cdp-and-hyperfine-bench\n    needs: zig-build-release\n\n    env:\n      MAX_VmHWM: 28000 # 28MB (KB)\n      MAX_CG_PEAK: 8000 # 8MB (KB)\n      MAX_AVG_DURATION: 17\n\n      # How to give cgroups access to the user actions-runner on the host:\n      # $ sudo apt install cgroup-tools\n      # $ sudo chmod o+w /sys/fs/cgroup/cgroup.procs\n      # $ sudo mkdir -p /sys/fs/cgroup/actions-runner\n      # $ sudo chown -R actions-runner:actions-runner /sys/fs/cgroup/actions-runner\n      CG_ROOT: /sys/fs/cgroup\n      CG: actions-runner/lpd_${{ github.run_id }}_${{ github.run_attempt }}\n\n    # use a self host runner.\n    runs-on: lpd-bench-hetzner\n    timeout-minutes: 15\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          repository: 'lightpanda-io/demo'\n          fetch-depth: 0\n\n      - run: npm install\n\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      - name: start http\n        run: |\n          go run ws/main.go & echo $! > WS.pid\n          sleep 2\n\n      - name: run lightpanda in cgroup\n        run: |\n          if [ ! -f /sys/fs/cgroup/cgroup.controllers ]; then\n            echo \"cgroup v2 not available: /sys/fs/cgroup/cgroup.controllers missing\"\n            exit 1\n          fi\n\n          mkdir -p $CG_ROOT/$CG\n          cgexec -g memory:$CG ./lightpanda serve & echo $! > LPD.pid\n\n          sleep 2\n\n      - name: run puppeteer\n        run: |\n          RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1\n          cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\\d+' > LPD.VmHWM\n          kill `cat LPD.pid`\n\n          PID=$(cat LPD.pid)\n          while kill -0 $PID 2>/dev/null; do\n            sleep 1\n          done\n          if [ ! -f $CG_ROOT/$CG/memory.peak ]; then\n            echo \"memory.peak not available in $CG\"\n            exit 1\n          fi\n          cat $CG_ROOT/$CG/memory.peak > LPD.cg_mem_peak\n\n      - name: puppeteer result\n        run: cat puppeteer.out\n\n      - name: cgroup memory regression\n        run: |\n          PEAK_BYTES=$(cat LPD.cg_mem_peak)\n          PEAK_KB=$((PEAK_BYTES / 1024))\n          echo \"memory.peak_bytes=$PEAK_BYTES\"\n          echo \"memory.peak_kb=$PEAK_KB\"\n          test \"$PEAK_KB\" -le \"$MAX_CG_PEAK\"\n\n      - name: virtual memory regression\n        run: |\n          export LPD_VmHWM=`cat LPD.VmHWM`\n          echo \"Peak resident set size: $LPD_VmHWM\"\n          test \"$LPD_VmHWM\" -le \"$MAX_VmHWM\"\n\n      - name: cleanup cgroup\n        run: rmdir $CG_ROOT/$CG\n\n      - name: duration regression\n        run: |\n          export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`\n          echo \"puppeteer avg duration: $PUPPETEER_AVG_DURATION\"\n          test \"$PUPPETEER_AVG_DURATION\" -le \"$MAX_AVG_DURATION\"\n\n      - name: json output\n        run: |\n          export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`\n          export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`\n          export LPD_VmHWM=`cat LPD.VmHWM`\n          export LPD_CG_PEAK_KB=$(( $(cat LPD.cg_mem_peak) / 1024 ))\n          echo \"{\\\"duration_total\\\":${TOTAL_DURATION},\\\"duration_avg\\\":${AVG_DURATION},\\\"mem_peak\\\":${LPD_VmHWM},\\\"cg_mem_peak\\\":${LPD_CG_PEAK_KB}}\" > bench.json\n          cat bench.json\n\n      - name: run hyperfine\n        run: |\n          hyperfine --export-json=hyperfine.json --warmup 3 --runs 20 --shell=none \"./lightpanda --dump http://127.0.0.1:1234/campfire-commerce/\"\n\n      - name: stop http\n        run: kill `cat WS.pid`\n\n      - name: write commit\n        run: |\n          echo \"${{github.sha}}\" > commit.txt\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: bench-results\n          path: |\n            bench.json\n            hyperfine.json\n            commit.txt\n          retention-days: 10\n\n\n  perf-fmt:\n    name: perf-fmt\n    needs: cdp-and-hyperfine-bench\n\n    # Don't execute on PR\n    if: github.event_name != 'pull_request'\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    container:\n      image: ghcr.io/lightpanda-io/perf-fmt:latest\n      credentials:\n        username: ${{ github.actor }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    steps:\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: bench-results\n\n      - name: format and send json result\n        run: /perf-fmt cdp ${{ github.sha }} bench.json\n\n      - name: format and send json result\n        run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json\n\n  browser-fetch:\n    name: browser fetch\n    needs: zig-build-release\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      - run: ./lightpanda fetch https://demo-browser.lightpanda.io/campfire-commerce/\n"
  },
  {
    "path": ".github/workflows/nightly.yml",
    "content": "name: nightly build\n\nenv:\n  AWS_ACCESS_KEY_ID: ${{ vars.NIGHTLY_BUILD_AWS_ACCESS_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}\n  AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}\n  AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}\n\n  RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}\n  GIT_VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dgit_version={0}', github.ref_name) || '' }}\n\non:\n  push:\n    tags:\n      - '*'\n  schedule:\n    - cron: \"2 2 * * *\"\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  build-linux-x86_64:\n    env:\n      ARCH: x86_64\n      OS: linux\n\n    runs-on: ubuntu-22.04\n    timeout-minutes: 20\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n        with:\n          os: ${{env.OS}}\n          arch: ${{env.ARCH}}\n\n      - name: v8 snapshot\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin\n\n      - name: zig build\n        run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}\n\n      - name: Rename binary\n        run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: upload on s3\n        run: |\n          export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`\n          aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: Upload the build\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}\n          tag: ${{ env.RELEASE }}\n          makeLatest: true\n\n  build-linux-aarch64:\n    env:\n      ARCH: aarch64\n      OS: linux\n\n    runs-on: ubuntu-22.04-arm\n    timeout-minutes: 20\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n        with:\n          os: ${{env.OS}}\n          arch: ${{env.ARCH}}\n\n      - name: v8 snapshot\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin\n\n      - name: zig build\n        run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}\n\n      - name: Rename binary\n        run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: upload on s3\n        run: |\n          export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`\n          aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: Upload the build\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}\n          tag: ${{ env.RELEASE }}\n          makeLatest: true\n\n  build-macos-aarch64:\n    env:\n      ARCH: aarch64\n      OS: macos\n\n    # macos-14 runs on arm CPU. see\n    # https://github.com/actions/runner-images?tab=readme-ov-file\n    runs-on: macos-14\n    timeout-minutes: 20\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n        with:\n          os: ${{env.OS}}\n          arch: ${{env.ARCH}}\n\n      - name: v8 snapshot\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin\n\n      - name: zig build\n        run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}\n\n      - name: Rename binary\n        run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: upload on s3\n        run: |\n          export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`\n          aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: Upload the build\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}\n          tag: ${{ env.RELEASE }}\n          makeLatest: true\n\n  build-macos-x86_64:\n    env:\n      ARCH: x86_64\n      OS: macos\n\n    runs-on: macos-14-large\n    timeout-minutes: 20\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n        with:\n          os: ${{env.OS}}\n          arch: ${{env.ARCH}}\n\n      - name: v8 snapshot\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin\n\n      - name: zig build\n        run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}\n\n      - name: Rename binary\n        run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: upload on s3\n        run: |\n          export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`\n          aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}\n\n      - name: Upload the build\n        uses: ncipollo/release-action@v1\n        with:\n          allowUpdates: true\n          artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}\n          tag: ${{ env.RELEASE }}\n          makeLatest: true\n"
  },
  {
    "path": ".github/workflows/wpt.yml",
    "content": "name: wpt\n\nenv:\n  AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}\n  AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}\n  AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}\n  AWS_CF_DISTRIBUTION: ${{ vars.AWS_CF_DISTRIBUTION }}\n  LIGHTPANDA_DISABLE_TELEMETRY: true\n\non:\n  schedule:\n    - cron: \"21 2 * * *\"\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  wpt-build-release:\n    name: zig build release\n\n    env:\n      ARCH: aarch64\n      OS: linux\n\n    runs-on: ubuntu-24.04-arm\n    timeout-minutes: 20\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n        with:\n          os: ${{env.OS}}\n          arch: ${{env.ARCH}}\n\n      - name: v8 snapshot\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin\n\n      - name: zig build release\n        run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: lightpanda-build-release\n          path: |\n            zig-out/bin/lightpanda\n          retention-days: 1\n\n  wpt-build-runner:\n    name: build wpt runner\n\n    runs-on: ubuntu-24.04-arm\n    timeout-minutes: 15\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          repository: 'lightpanda-io/demo'\n          fetch-depth: 0\n\n      - run: |\n          cd ./wptrunner\n          CGO_ENABLED=0 go build\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: wptrunner\n          path: |\n            wptrunner/wptrunner\n          retention-days: 1\n\n  run-wpt:\n    name: web platform tests json output\n    needs:\n      - wpt-build-release\n      - wpt-build-runner\n\n    # use a self host runner.\n    runs-on: lpd-wpt-aws\n    timeout-minutes: 600\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          ref: fork\n          repository: 'lightpanda-io/wpt'\n          fetch-depth: 0\n\n      # The hosts are configured manually on the self host runner.\n      # - name: create custom hosts\n      #   run: ./wpt make-hosts-file | sudo tee -a /etc/hosts\n\n      - name: generate manifest\n        run: ./wpt manifest\n\n      - name: download lightpanda release\n        uses: actions/download-artifact@v8\n        with:\n          name: lightpanda-build-release\n\n      - run: chmod a+x ./lightpanda\n\n      - name: download wptrunner\n        uses: actions/download-artifact@v8\n        with:\n          name: wptrunner\n\n      - run: chmod a+x ./wptrunner\n\n      - name: run test with json output\n        run: |\n          ./wpt serve 2> /dev/null & echo $! > WPT.pid\n          sleep 20s\n          ./wptrunner -lpd-path ./lightpanda -json -concurrency 5 -pool 5 --mem-limit 400 > wpt.json\n          kill `cat WPT.pid`\n\n      - name: write commit\n        run: |\n          echo \"${{github.sha}}\" > commit.txt\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: wpt-results\n          path: |\n            wpt.json\n            commit.txt\n          retention-days: 10\n\n  perf-fmt:\n    name: perf-fmt\n    needs: run-wpt\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    container:\n      image: ghcr.io/lightpanda-io/perf-fmt:latest\n      credentials:\n       username: ${{ github.actor }}\n       password: ${{ secrets.GITHUB_TOKEN }}\n\n    steps:\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: wpt-results\n\n      - name: format and send json result\n        run: /perf-fmt wpt ${{ github.sha }} wpt.json\n"
  },
  {
    "path": ".github/workflows/zig-test.yml",
    "content": "name: zig-test\n\nenv:\n  AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}\n  AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}\n  AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}\n  LIGHTPANDA_DISABLE_TELEMETRY: true\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \".github/**\"\n      - \"src/**\"\n      - \"build.zig\"\n      - \"build.zig.zon\"\n\n  pull_request:\n    # By default GH trigger on types opened, synchronize and reopened.\n    # see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request\n    # Since we skip the job when the PR is in draft state, we want to force CI\n    # running when the PR is marked ready_for_review w/o other change.\n    # see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917\n    types: [opened, synchronize, reopened, ready_for_review]\n\n    paths:\n      - \".github/**\"\n      - \"src/**\"\n      - \"build.zig\"\n      - \"build.zig.zon\"\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  zig-fmt:\n    name: zig fmt\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    if: github.event.pull_request.draft == false\n\n    steps:\n      - uses: actions/checkout@v6\n\n      # Zig version used from the `minimum_zig_version` field in build.zig.zon\n      - uses: mlugg/setup-zig@v2\n\n      - name: Run zig fmt\n        id: fmt\n        run: |\n          zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo \"Failed\"\n          delimiter=\"$(openssl rand -hex 8)\"\n          echo \"zig_fmt_errs<<${delimiter}\" >> \"${GITHUB_OUTPUT}\"\n\n          if [ -s zig-fmt.err ]; then\n            echo \"// The following errors occurred:\" >> \"${GITHUB_OUTPUT}\"\n            cat zig-fmt.err >> \"${GITHUB_OUTPUT}\"\n          fi\n\n          if [ -s zig-fmt.err2 ]; then\n            echo \"// The following files were not formatted:\" >> \"${GITHUB_OUTPUT}\"\n            cat zig-fmt.err2 >> \"${GITHUB_OUTPUT}\"\n          fi\n\n          echo \"${delimiter}\" >> \"${GITHUB_OUTPUT}\"\n\n      - name: Fail the job\n        if: steps.fmt.outputs.zig_fmt_errs != ''\n        run: exit 1\n\n  zig-test-debug:\n    name: zig test using v8 in debug mode\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    if: github.event.pull_request.draft == false\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n        with:\n          debug: true\n\n      - name: zig build test\n        run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test\n\n  zig-test-release:\n    name: zig test\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    if: github.event.pull_request.draft == false\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/install\n\n      - name: zig build test\n        run: METRICS=true zig build -Dprebuilt_v8_path=v8/libc_v8.a test > bench.json\n\n      - name: write commit\n        run: |\n          echo \"${{github.sha}}\" > commit.txt\n\n      - name: upload artifact\n        uses: actions/upload-artifact@v7\n        with:\n          name: bench-results\n          path: |\n            bench.json\n            commit.txt\n          retention-days: 10\n\n  bench-fmt:\n    name: perf-fmt\n    needs: zig-test-release\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    if: github.event_name != 'pull_request'\n\n    container:\n      image: ghcr.io/lightpanda-io/perf-fmt:latest\n      credentials:\n        username: ${{ github.actor }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    steps:\n      - name: download artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: bench-results\n\n      - name: format and send json result\n        run: /perf-fmt bench-browser ${{ github.sha }} bench.json\n"
  },
  {
    "path": ".gitignore",
    "content": "/.zig-cache/\n/.lp-cache/\nzig-out\nlightpanda.id\n/src/html5ever/target/\nsrc/snapshot.bin\n"
  },
  {
    "path": "CLA.md",
    "content": "# Lightpanda (Selecy SAS) Grant and Contributor License Agreement (“Agreement”)\n\nThis agreement is based on the Apache Software Foundation Contributor License\nAgreement. (v r190612)\n\nThank you for your interest in software projects stewarded by Lightpanda\n(Selecy SAS) (“Lightpanda”). In order to clarify the intellectual property\nlicense granted with Contributions from any person or entity, Lightpanda must\nhave a Contributor License Agreement (CLA) on file that has been agreed to by\neach Contributor, indicating agreement to the license terms below. This license\nis for your protection as a Contributor as well as the protection of Lightpanda\nand its users; it does not change your rights to use your own Contributions for\nany other purpose. This Agreement allows an individual to contribute to\nLightpanda on that individual’s own behalf, or an entity (the “Corporation”) to\nsubmit Contributions to Lightpanda, to authorize Contributions submitted by its\ndesignated employees to Lightpanda, and to grant copyright and patent licenses\nthereto.\n\nYou accept and agree to the following terms and conditions for Your present and\nfuture Contributions submitted to Lightpanda. Except for the license granted\nherein to Lightpanda and recipients of software distributed by Lightpanda, You\nreserve all right, title, and interest in and to Your Contributions.\n\n1. Definitions. “You” (or “Your”) shall mean the copyright owner or legal\n   entity authorized by the copyright owner that is making this Agreement with\n   Lightpanda. For legal entities, the entity making a Contribution and all\n   other entities that control, are controlled by, or are under common control\n   with that entity are considered to be a single Contributor. For the purposes\n   of this definition, “control” means (i) the power, direct or indirect, to\n   cause the 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   “Contribution” shall mean any work, as well as any modifications or\n   additions to an existing work, that is intentionally submitted by You to\n   Lightpanda for inclusion in, or documentation of, any of the products owned\n   or managed by Lightpanda (the “Work”). For the purposes of this definition,\n   “submitted” means any form of electronic, verbal, or written communication\n   sent to Lightpanda or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems (such\n   as GitHub), and issue tracking systems that are managed by, or on behalf of,\n   Lightpanda for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise designated\n   in writing by You as “Not a Contribution.”\n\n2. Grant of Copyright License. Subject to the terms and conditions of this\n   Agreement, You hereby grant to Lightpanda and to recipients of software\n   distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,\n   royalty-free, irrevocable copyright license to reproduce, prepare derivative\n   works of, publicly display, publicly perform, sublicense, and distribute\n   Your Contributions and such derivative works.\n\n3. Grant of Patent License. Subject to the terms and conditions of this\n   Agreement, You hereby grant to Lightpanda and to recipients of software\n   distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,\n   royalty-free, irrevocable (except as stated in this section) patent license\n   to make, have made, use, offer to sell, sell, import, and otherwise transfer\n   the Work, where such license applies only to those patent claims licensable\n   by You that are necessarily infringed by Your Contribution(s) alone or by\n   combination of Your Contribution(s) with the Work to which such\n   Contribution(s) were submitted. If any entity institutes patent litigation\n   against You or any other entity (including a cross-claim or counterclaim in\n   a lawsuit) alleging that your Contribution, or the Work to which you have\n   contributed, constitutes direct or contributory patent infringement, then\n   any patent licenses granted to that entity under this Agreement for that\n   Contribution or Work shall terminate as of the date such litigation is\n   filed.\n\n4. You represent that You are legally entitled to grant the above license. If\n   You are an individual, and if Your employer(s) has rights to intellectual\n   property that you create that includes Your Contributions, you represent\n   that You have received permission to make Contributions on behalf of that\n   employer, or that Your employer has waived such rights for your\n   Contributions to Lightpanda. If You are a Corporation, any individual who\n   makes a contribution from an account associated with You will be considered\n   authorized to Contribute on Your behalf.\n\n5. You represent that each of Your Contributions is Your original creation (see\n   section 7 for submissions on behalf of others).\n\n6. You are not expected to provide support for Your Contributions,except to the\n   extent You desire to provide support. You may provide support for free, for\n   a fee, or not at all. Unless required by applicable law or agreed to in\n   writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT\n   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,\n   without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,\n   MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.\n\n7. Should You wish to submit work that is not Your original creation, You may\n   submit it to Lightpanda separately from any Contribution, identifying the\n   complete details of its source and of any license or other restriction\n   (including, but not limited to, related patents, trademarks, and license\n   agreements) of which you are personally aware, and conspicuously marking the\n   work as “Submitted on behalf of a third-party: [named here]”.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nLightpanda accepts pull requests through GitHub.\n\nYou have to sign our [CLA](CLA.md) during your first pull request process\notherwise we're not able to accept your contributions.\n\nThe process signature uses the [CLA assistant\nlite](https://github.com/marketplace/actions/cla-assistant-lite). You can see\nan example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM debian:stable-slim\n\nARG MINISIG=0.12\nARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U\nARG V8=14.0.365.4\nARG ZIG_V8=v0.3.4\nARG TARGETPLATFORM\n\nRUN apt-get update -yq && \\\n    apt-get install -yq xz-utils ca-certificates \\\n        pkg-config libglib2.0-dev \\\n        clang make curl git\n\n# Get Rust\nRUN curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal -y\nENV PATH=\"/root/.cargo/bin:${PATH}\"\n\n# install minisig\nRUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \\\n    tar xvzf minisign-${MINISIG}-linux.tar.gz -C /\n\n# clone lightpanda\nRUN git clone https://github.com/lightpanda-io/browser.git\nWORKDIR /browser\n\n# install zig\nRUN ZIG=$(grep '\\.minimum_zig_version = \"' \"build.zig.zon\" | cut -d'\"' -f2) && \\\n    case $TARGETPLATFORM in \\\n      \"linux/arm64\") ARCH=\"aarch64\" ;; \\\n      *) ARCH=\"x86_64\" ;; \\\n    esac && \\\n    curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \\\n    curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \\\n    /minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \\\n    tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \\\n    mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \\\n    ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig\n\n# download and install v8\nRUN case $TARGETPLATFORM in \\\n    \"linux/arm64\") ARCH=\"aarch64\" ;; \\\n    *) ARCH=\"x86_64\" ;; \\\n    esac && \\\n    curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \\\n    mkdir -p v8/ && \\\n    mv libc_v8.a v8/libc_v8.a\n\n# build v8 snapshot\nRUN zig build -Doptimize=ReleaseFast \\\n    -Dprebuilt_v8_path=v8/libc_v8.a \\\n    snapshot_creator -- src/snapshot.bin\n\n# build release\nRUN zig build -Doptimize=ReleaseFast \\\n    -Dsnapshot_path=../../snapshot.bin \\\n    -Dprebuilt_v8_path=v8/libc_v8.a \\\n    -Dgit_commit=$(git rev-parse --short HEAD)\n\nFROM debian:stable-slim\n\nRUN apt-get update -yq && \\\n    apt-get install -yq tini\n\nFROM debian:stable-slim\n\n# copy ca certificates\nCOPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\n\nCOPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda\nCOPY --from=1 /usr/bin/tini /usr/bin/tini\n\nEXPOSE 9222/tcp\n\n# Lightpanda install only some signal handlers, and PID 1 doesn't have a default SIGTERM signal handler.\n# Using \"tini\" as PID1 ensures that signals work as expected, so e.g. \"docker stop\" will not hang.\n# (See https://github.com/krallin/tini#why-tini).\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nCMD [\"/bin/lightpanda\", \"serve\", \"--host\", \"0.0.0.0\", \"--port\", \"9222\", \"--log_level\", \"info\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "LICENSING.md",
    "content": "# Licensing\n\nLicense names used in this document are as per [SPDX License\nList](https://spdx.org/licenses/).\n\nThe default license for this project is [AGPL-3.0-only](LICENSE).\n\nThe following directories and their subdirectories are licensed under their\noriginal upstream licenses:\n\n```\nvendor/\ntests/wpt/\n```\n"
  },
  {
    "path": "Makefile",
    "content": "# Variables\n# ---------\n\nZIG := zig\nBC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n# option test filter make test F=\"server\"\nF=\n\n# OS and ARCH\nkernel = $(shell uname -ms)\nifeq ($(kernel), Darwin arm64)\n\tOS := macos\n\tARCH := aarch64\nelse ifeq ($(kernel), Darwin x86_64)\n\tOS := macos\n\tARCH := x86_64\nelse ifeq ($(kernel), Linux aarch64)\n\tOS := linux\n\tARCH := aarch64\nelse ifeq ($(kernel), Linux arm64)\n\tOS := linux\n\tARCH := aarch64\nelse ifeq ($(kernel), Linux x86_64)\n\tOS := linux\n\tARCH := x86_64\nelse\n\t$(error \"Unhandled kernel: $(kernel)\")\nendif\n\n\n# Infos\n# -----\n.PHONY: help\n\n## Display this help screen\nhelp:\n\t@printf \"\\033[36m%-35s %s\\033[0m\\n\" \"Command\" \"Usage\"\n\t@sed -n -e '/^## /{'\\\n\t\t-e 's/## //g;'\\\n\t\t-e 'h;'\\\n\t\t-e 'n;'\\\n\t\t-e 's/:.*//g;'\\\n\t\t-e 'G;'\\\n\t\t-e 's/\\n/ /g;'\\\n\t\t-e 'p;}' Makefile | awk '{printf \"\\033[33m%-35s\\033[0m%s\\n\", $$1, substr($$0,length($$1)+1)}'\n\n\n# $(ZIG) commands\n# ------------\n.PHONY: build build-v8-snapshot build-dev run run-release test bench data end2end\n\n## Build v8 snapshot\nbuild-v8-snapshot:\n\t@printf \"\\033[36mBuilding v8 snapshot (release safe)...\\033[0m\\n\"\n\t@$(ZIG) build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin || (printf \"\\033[33mBuild ERROR\\033[0m\\n\"; exit 1;)\n\t@printf \"\\033[33mBuild OK\\033[0m\\n\"\n\n## Build in release-fast mode\nbuild: build-v8-snapshot\n\t@printf \"\\033[36mBuilding (release fast)...\\033[0m\\n\"\n\t@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf \"\\033[33mBuild ERROR\\033[0m\\n\"; exit 1;)\n\t@printf \"\\033[33mBuild OK\\033[0m\\n\"\n\n## Build in debug mode\nbuild-dev:\n\t@printf \"\\033[36mBuilding (debug)...\\033[0m\\n\"\n\t@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf \"\\033[33mBuild ERROR\\033[0m\\n\"; exit 1;)\n\t@printf \"\\033[33mBuild OK\\033[0m\\n\"\n\n## Run the server in release mode\nrun: build\n\t@printf \"\\033[36mRunning...\\033[0m\\n\"\n\t@./zig-out/bin/lightpanda || (printf \"\\033[33mRun ERROR\\033[0m\\n\"; exit 1;)\n\n## Run the server in debug mode\nrun-debug: build-dev\n\t@printf \"\\033[36mRunning...\\033[0m\\n\"\n\t@./zig-out/bin/lightpanda || (printf \"\\033[33mRun ERROR\\033[0m\\n\"; exit 1;)\n\n## Test - `grep` is used to filter out the huge compile command on build\nifeq ($(OS), macos)\ntest:\n\t@script -q /dev/null sh -c 'TEST_FILTER=\"${F}\" $(ZIG) build test -freference-trace' 2>&1 \\\n\t\t| grep --line-buffered -v \"^/.*zig test -freference-trace\"\nelse\ntest:\n\t@script -qec 'TEST_FILTER=\"${F}\" $(ZIG) build test -freference-trace' /dev/null 2>&1 \\\n\t\t| grep --line-buffered -v \"^/.*zig test -freference-trace\"\nendif\n\n## Run demo/runner end to end tests\nend2end:\n\t@test -d ../demo\n\tcd ../demo && go run runner/main.go\n\n# Install and build required dependencies commands\n# ------------\n.PHONY: install\n\ninstall: build\n\ndata:\n\tcd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://lightpanda.io\"><img src=\"https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png\" alt=\"Logo\" height=170></a>\n</p>\n<h1 align=\"center\">Lightpanda Browser</h1>\n<p align=\"center\">\n<strong>The headless browser built from scratch for AI agents and automation.</strong><br>\nNot a Chromium fork. Not a WebKit patch. A new browser, written in Zig.\n</p>\n\n</div>\n<div align=\"center\">\n\n[![License](https://img.shields.io/github/license/lightpanda-io/browser)](https://github.com/lightpanda-io/browser/blob/main/LICENSE)\n[![Twitter Follow](https://img.shields.io/twitter/follow/lightpanda_io)](https://twitter.com/lightpanda_io)\n[![GitHub stars](https://img.shields.io/github/stars/lightpanda-io/browser)](https://github.com/lightpanda-io/browser)\n[![Discord](https://img.shields.io/discord/1391984864894521354?style=flat-square&label=discord)](https://discord.gg/K63XeymfB5)\n\n</div>\n<div align=\"center\">\n\n[<img width=\"350px\" src=\"https://cdn.lightpanda.io/assets/images/github/execution-time.svg\">\n](https://github.com/lightpanda-io/demo)\n&emsp;\n[<img width=\"350px\" src=\"https://cdn.lightpanda.io/assets/images/github/memory-frame.svg\">\n](https://github.com/lightpanda-io/demo)\n</div>\n\n_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.\nSee [benchmark details](https://github.com/lightpanda-io/demo)._\n\nLightpanda is the open-source browser made for headless usage:\n\n- Javascript execution\n- Support of Web APIs (partial, WIP)\n- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/)\n\nFast web automation for AI agents, LLM training, scraping and testing:\n\n- Ultra-low memory footprint (9x less than Chrome)\n- Exceptionally fast execution (11x faster than Chrome)\n- Instant startup\n\n[^1]: **Playwright support disclaimer:**\nDue to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.\n\n## Quick start\n\n### Install\n**Install from the nightly builds**\n\nYou can download the last binary from the [nightly\nbuilds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for\nLinux x86_64 and MacOS aarch64.\n\n*For Linux*\n```console\ncurl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \\\nchmod a+x ./lightpanda\n```\n\n*For MacOS*\n```console\ncurl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \\\nchmod a+x ./lightpanda\n```\n\n*For Windows + WSL2*\n\nThe Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.\nIt is recommended to install clients like Puppeteer on the Windows host.\n\n**Install from Docker**\n\nLightpanda provides [official Docker\nimages](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and\narm64 architectures.\nThe following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.\n```console\ndocker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly\n```\n\n### Dump a URL\n\n```console\n./lightpanda fetch --obey_robots --log_format pretty  --log_level info https://demo-browser.lightpanda.io/campfire-commerce/\n```\n```console\nINFO  telemetry : telemetry status . . . . . . . . . . . . .  [+0ms]\n      disabled = false\n\nINFO  page : navigate . . . . . . . . . . . . . . . . . . . . [+6ms]\n      url = https://demo-browser.lightpanda.io/campfire-commerce/\n      method = GET\n      reason = address_bar\n      body = false\n      req_id = 1\n\nINFO  browser : executing script . . . . . . . . . . . . . .  [+118ms]\n      src = https://demo-browser.lightpanda.io/campfire-commerce/script.js\n      kind = javascript\n      cacheable = true\n\nINFO  http : request complete . . . . . . . . . . . . . . . . [+140ms]\n      source = xhr\n      url = https://demo-browser.lightpanda.io/campfire-commerce/json/product.json\n      status = 200\n      len = 4770\n\nINFO  http : request complete . . . . . . . . . . . . . . . . [+141ms]\n      source = fetch\n      url = https://demo-browser.lightpanda.io/campfire-commerce/json/reviews.json\n      status = 200\n      len = 1615\n<!DOCTYPE html>\n```\n\n### Start a CDP server\n\n```console\n./lightpanda serve --obey_robots --log_format pretty  --log_level info --host 127.0.0.1 --port 9222\n```\n```console\nINFO  telemetry : telemetry status . . . . . . . . . . . . .  [+0ms]\n      disabled = false\n\nINFO  app : server running . . . . . . . . . . . . . . . . .  [+0ms]\n      address = 127.0.0.1:9222\n```\n\nOnce the CDP server started, you can run a Puppeteer script by configuring the\n`browserWSEndpoint`.\n\n```js\n'use strict'\n\nimport puppeteer from 'puppeteer-core';\n\n// use browserWSEndpoint to pass the Lightpanda's CDP server address.\nconst browser = await puppeteer.connect({\n  browserWSEndpoint: \"ws://127.0.0.1:9222\",\n});\n\n// The rest of your script remains the same.\nconst context = await browser.createBrowserContext();\nconst page = await context.newPage();\n\n// Dump all the links from the page.\nawait page.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: \"networkidle0\"});\n\nconst links = await page.evaluate(() => {\n  return Array.from(document.querySelectorAll('a')).map(row => {\n    return row.getAttribute('href');\n  });\n});\n\nconsole.log(links);\n\nawait page.close();\nawait context.close();\nawait browser.disconnect();\n```\n\n### Telemetry\nBy default, Lightpanda collects and sends usage telemetry. This can be disabled by setting an environment variable `LIGHTPANDA_DISABLE_TELEMETRY=true`. You can read Lightpanda's privacy policy at: [https://lightpanda.io/privacy-policy](https://lightpanda.io/privacy-policy).\n\n## Status\n\nLightpanda is in Beta and currently a work in progress. Stability and coverage are improving and many websites now work.\nYou may still encounter errors or crashes. Please open an issue with specifics if so.\n\nHere are the key features we have implemented:\n\n- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))\n- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))\n- [x] DOM tree\n- [x] Javascript support ([v8](https://v8.dev/))\n- [x] DOM APIs\n- [x] Ajax\n  - [x] XHR API\n  - [x] Fetch API\n- [x] DOM dump\n- [x] CDP/websockets server\n- [x] Click\n- [x] Input form\n- [x] Cookies\n- [x] Custom HTTP headers\n- [x] Proxy support\n- [x] Network interception\n- [x] Respect `robots.txt` with option `--obey_robots`\n\nNOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.\n\n## Build from sources\n\n### Prerequisites\n\nLightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to\ninstall it with the right version in order to build the project.\n\nLightpanda also depends on\n[v8](https://chromium.googlesource.com/v8/v8.git),\n[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).\n\nTo be able to build the v8 engine, you have to install some libs:\n\nFor **Debian/Ubuntu based Linux**:\n\n```\nsudo apt install xz-utils ca-certificates \\\n    pkg-config libglib2.0-dev \\\n    clang make curl git\n```\nYou also need to [install Rust](https://rust-lang.org/tools/install/).\n\nFor systems with [**Nix**](https://nixos.org/download/), you can use the devShell:\n```\nnix develop\n```\n\nFor **MacOS**, you need cmake and [Rust](https://rust-lang.org/tools/install/).\n```\nbrew install cmake\n```\n\n### Build and run\n\nYou an build the entire browser with `make build` or `make build-dev` for debug\nenv.\n\nBut you can directly use the zig command: `zig build run`.\n\n#### Embed v8 snapshot\n\nLighpanda uses v8 snapshot. By default, it is created on startup but you can\nembed it by using the following commands:\n\nGenerate the snapshot.\n```\nzig build snapshot_creator -- src/snapshot.bin\n```\n\nBuild using the snapshot binary.\n```\nzig build -Dsnapshot_path=../../snapshot.bin\n```\n\nSee [#1279](https://github.com/lightpanda-io/browser/pull/1279) for more details.\n\n## Test\n\n### Unit Tests\n\nYou can test Lightpanda by running `make test`.\n\n### End to end tests\n\nTo run end to end tests, you need to clone the [demo\nrepository](https://github.com/lightpanda-io/demo) into `../demo` dir.\n\nYou have to install the [demo's node\nrequirements](https://github.com/lightpanda-io/demo?tab=readme-ov-file#dependencies-1)\n\nYou also need to install [Go](https://go.dev) > v1.24.\n\n```\nmake end2end\n```\n\n### Web Platform Tests\n\nLightpanda is tested against the standardized [Web Platform\nTests](https://web-platform-tests.org/).\n\nWe use [a fork](https://github.com/lightpanda-io/wpt/tree/fork) including a custom\n[`testharnessreport.js`](https://github.com/lightpanda-io/wpt/commit/01a3115c076a3ad0c84849dbbf77a6e3d199c56f).\n\nFor reference, you can easily execute a WPT test case with your browser via\n[wpt.live](https://wpt.live).\n\n#### Configure WPT HTTP server\n\nTo run the test, you must clone the repository, configure the custom hosts and generate the\n`MANIFEST.json` file.\n\nClone the repository with the `fork` branch.\n```\ngit clone -b fork --depth=1 git@github.com:lightpanda-io/wpt.git\n```\n\nEnter into the `wpt/` dir.\n\nInstall custom domains in your `/etc/hosts`\n```\n./wpt make-hosts-file | sudo tee -a /etc/hosts\n```\n\nGenerate `MANIFEST.json`\n```\n./wpt manifest\n```\nUse the [WPT's setup\nguide](https://web-platform-tests.org/running-tests/from-local-system.html) for\ndetails.\n\n#### Run WPT test suite\n\nAn external [Go](https://go.dev) runner is provided by\n[github.com/lightpanda-io/demo/](https://github.com/lightpanda-io/demo/)\nrepository, located into `wptrunner/` dir.\nYou need to clone the project first.\n\nFirst start the WPT's HTTP server from your `wpt/` clone dir.\n```\n./wpt serve\n```\n\nRun a Lightpanda browser\n\n```\nzig build run -- --insecure_disable_tls_host_verification\n```\n\nThen you can start the wptrunner from the Demo's clone dir:\n```\ncd wptrunner && go run .\n```\n\nOr one specific test:\n\n```\ncd wptrunner && go run . Node-childNodes.html\n```\n\n`wptrunner` command accepts `--summary` and `--json` options modifying output.\nAlso `--concurrency` define the concurrency limit.\n\n:warning: Running the whole test suite will take a long time. In this case,\nit's useful to build in `releaseFast` mode to make tests faster.\n\n```\nzig build -Doptimize=ReleaseFast run\n```\n\n## Contributing\n\nLightpanda accepts pull requests through GitHub.\n\nYou have to sign our [CLA](CLA.md) during the pull request process otherwise\nwe're not able to accept your contributions.\n\n## Why?\n\n### Javascript execution is mandatory for the modern web\n\nIn the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:\n\n- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.\n- JS web frameworks: React, Vue, Angular & others\n\n### Chrome is not the right tool\n\nIf we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?\n\n- Heavy on RAM and CPU, expensive to run\n- Hard to package, deploy and maintain at scale\n- Bloated, lots of features are not useful in headless usage\n\n### Lightpanda is built for performance\n\nIf we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But that’s what we did:\n\n- Not based on Chromium, Blink or WebKit\n- Low-level system programming language (Zig) with optimisations in mind\n- Opinionated: without graphical rendering\n"
  },
  {
    "path": "build.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Build = std.Build;\n\npub fn build(b: *Build) !void {\n    const target = b.standardTargetOptions(.{});\n    const optimize = b.standardOptimizeOption(.{});\n\n    const manifest = Manifest.init(b);\n\n    const git_commit = b.option([]const u8, \"git_commit\", \"Current git commit\");\n    const git_version = b.option([]const u8, \"git_version\", \"Current git version (from tag)\");\n    const prebuilt_v8_path = b.option([]const u8, \"prebuilt_v8_path\", \"Path to prebuilt libc_v8.a\");\n    const snapshot_path = b.option([]const u8, \"snapshot_path\", \"Path to v8 snapshot\");\n\n    var opts = b.addOptions();\n    opts.addOption([]const u8, \"version\", manifest.version);\n    opts.addOption([]const u8, \"git_commit\", git_commit orelse \"dev\");\n    opts.addOption(?[]const u8, \"git_version\", git_version orelse null);\n    opts.addOption(?[]const u8, \"snapshot_path\", snapshot_path);\n\n    const enable_tsan = b.option(bool, \"tsan\", \"Enable Thread Sanitizer\") orelse false;\n    const enable_asan = b.option(bool, \"asan\", \"Enable Address Sanitizer\") orelse false;\n    const enable_csan = b.option(std.zig.SanitizeC, \"csan\", \"Enable C Sanitizers\");\n\n    const lightpanda_module = blk: {\n        const mod = b.addModule(\"lightpanda\", .{\n            .root_source_file = b.path(\"src/lightpanda.zig\"),\n            .target = target,\n            .optimize = optimize,\n            .link_libc = true,\n            .link_libcpp = true,\n            .sanitize_c = enable_csan,\n            .sanitize_thread = enable_tsan,\n        });\n        mod.addImport(\"lightpanda\", mod); // allow circular \"lightpanda\" import\n        mod.addImport(\"build_config\", opts.createModule());\n\n        // Format check\n        const fmt_step = b.step(\"fmt\", \"Check code formatting\");\n        const fmt = b.addFmt(.{\n            .paths = &.{ \"src\", \"build.zig\", \"build.zig.zon\" },\n            .check = true,\n        });\n        fmt_step.dependOn(&fmt.step);\n\n        // Set default behavior\n        b.default_step.dependOn(fmt_step);\n\n        try linkV8(b, mod, enable_asan, enable_tsan, prebuilt_v8_path);\n        try linkCurl(b, mod, enable_tsan);\n        try linkHtml5Ever(b, mod);\n\n        break :blk mod;\n    };\n\n    {\n        // browser\n        const exe = b.addExecutable(.{\n            .name = \"lightpanda\",\n            .use_llvm = true,\n            .root_module = b.createModule(.{\n                .root_source_file = b.path(\"src/main.zig\"),\n                .target = target,\n                .optimize = optimize,\n                .sanitize_c = enable_csan,\n                .sanitize_thread = enable_tsan,\n                .imports = &.{\n                    .{ .name = \"lightpanda\", .module = lightpanda_module },\n                },\n            }),\n        });\n        b.installArtifact(exe);\n\n        const run_cmd = b.addRunArtifact(exe);\n        if (b.args) |args| {\n            run_cmd.addArgs(args);\n        }\n        const run_step = b.step(\"run\", \"Run the app\");\n        run_step.dependOn(&run_cmd.step);\n    }\n\n    {\n        // snapshot creator\n        const exe = b.addExecutable(.{\n            .name = \"lightpanda-snapshot-creator\",\n            .use_llvm = true,\n            .root_module = b.createModule(.{\n                .root_source_file = b.path(\"src/main_snapshot_creator.zig\"),\n                .target = target,\n                .optimize = optimize,\n                .imports = &.{\n                    .{ .name = \"lightpanda\", .module = lightpanda_module },\n                },\n            }),\n        });\n        b.installArtifact(exe);\n\n        const run_cmd = b.addRunArtifact(exe);\n        if (b.args) |args| {\n            run_cmd.addArgs(args);\n        }\n        const run_step = b.step(\"snapshot_creator\", \"Generate a v8 snapshot\");\n        run_step.dependOn(&run_cmd.step);\n    }\n\n    {\n        // test\n        const tests = b.addTest(.{\n            .root_module = lightpanda_module,\n            .use_llvm = true,\n            .test_runner = .{ .path = b.path(\"src/test_runner.zig\"), .mode = .simple },\n        });\n        const run_tests = b.addRunArtifact(tests);\n        const test_step = b.step(\"test\", \"Run unit tests\");\n        test_step.dependOn(&run_tests.step);\n    }\n\n    {\n        // browser\n        const exe = b.addExecutable(.{\n            .name = \"legacy_test\",\n            .use_llvm = true,\n            .root_module = b.createModule(.{\n                .root_source_file = b.path(\"src/main_legacy_test.zig\"),\n                .target = target,\n                .optimize = optimize,\n                .sanitize_c = enable_csan,\n                .sanitize_thread = enable_tsan,\n                .imports = &.{\n                    .{ .name = \"lightpanda\", .module = lightpanda_module },\n                },\n            }),\n        });\n        b.installArtifact(exe);\n\n        const run_cmd = b.addRunArtifact(exe);\n        if (b.args) |args| {\n            run_cmd.addArgs(args);\n        }\n        const run_step = b.step(\"legacy_test\", \"Run the app\");\n        run_step.dependOn(&run_cmd.step);\n    }\n}\n\nfn linkV8(\n    b: *Build,\n    mod: *Build.Module,\n    is_asan: bool,\n    is_tsan: bool,\n    prebuilt_v8_path: ?[]const u8,\n) !void {\n    const target = mod.resolved_target.?;\n\n    const dep = b.dependency(\"v8\", .{\n        .target = target,\n        .optimize = mod.optimize.?,\n        .is_asan = is_asan,\n        .is_tsan = is_tsan,\n        .inspector_subtype = false,\n        .v8_enable_sandbox = is_tsan,\n        .cache_root = b.pathFromRoot(\".lp-cache\"),\n        .prebuilt_v8_path = prebuilt_v8_path,\n    });\n    mod.addImport(\"v8\", dep.module(\"v8\"));\n}\n\nfn linkHtml5Ever(b: *Build, mod: *Build.Module) !void {\n    const is_debug = if (mod.optimize.? == .Debug) true else false;\n\n    const exec_cargo = b.addSystemCommand(&.{\n        \"cargo\",           \"build\",\n        \"--profile\",       if (is_debug) \"dev\" else \"release\",\n        \"--manifest-path\", \"src/html5ever/Cargo.toml\",\n    });\n\n    // TODO: We can prefer `--artifact-dir` once it become stable.\n    const out_dir = exec_cargo.addPrefixedOutputDirectoryArg(\"--target-dir=\", \"html5ever\");\n\n    const html5ever_step = b.step(\"html5ever\", \"Install html5ever dependency (requires cargo)\");\n    html5ever_step.dependOn(&exec_cargo.step);\n\n    const obj = out_dir.path(b, if (is_debug) \"debug\" else \"release\").path(b, \"liblitefetch_html5ever.a\");\n    mod.addObjectFile(obj);\n}\n\nfn linkCurl(b: *Build, mod: *Build.Module, is_tsan: bool) !void {\n    const target = mod.resolved_target.?;\n\n    const curl = buildCurl(b, target, mod.optimize.?, is_tsan);\n    mod.linkLibrary(curl);\n\n    const zlib = buildZlib(b, target, mod.optimize.?, is_tsan);\n    curl.root_module.linkLibrary(zlib);\n\n    const brotli = buildBrotli(b, target, mod.optimize.?, is_tsan);\n    for (brotli) |lib| curl.root_module.linkLibrary(lib);\n\n    const nghttp2 = buildNghttp2(b, target, mod.optimize.?, is_tsan);\n    curl.root_module.linkLibrary(nghttp2);\n\n    const boringssl = buildBoringSsl(b, target, mod.optimize.?);\n    for (boringssl) |lib| curl.root_module.linkLibrary(lib);\n\n    switch (target.result.os.tag) {\n        .macos => {\n            // needed for proxying on mac\n            mod.addSystemFrameworkPath(.{ .cwd_relative = \"/System/Library/Frameworks\" });\n            mod.linkFramework(\"CoreFoundation\", .{});\n            mod.linkFramework(\"SystemConfiguration\", .{});\n        },\n        else => {},\n    }\n}\n\nfn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) *Build.Step.Compile {\n    const dep = b.dependency(\"zlib\", .{});\n\n    const mod = b.createModule(.{\n        .target = target,\n        .optimize = optimize,\n        .link_libc = true,\n        .sanitize_thread = is_tsan,\n    });\n\n    const lib = b.addLibrary(.{ .name = \"z\", .root_module = mod });\n    lib.installHeadersDirectory(dep.path(\"\"), \"\", .{});\n    lib.addCSourceFiles(.{\n        .root = dep.path(\"\"),\n        .flags = &.{\n            \"-DHAVE_SYS_TYPES_H\",\n            \"-DHAVE_STDINT_H\",\n            \"-DHAVE_STDDEF_H\",\n            \"-DHAVE_UNISTD_H\",\n        },\n        .files = &.{\n            \"adler32.c\", \"compress.c\", \"crc32.c\",\n            \"deflate.c\", \"gzclose.c\",  \"gzlib.c\",\n            \"gzread.c\",  \"gzwrite.c\",  \"infback.c\",\n            \"inffast.c\", \"inflate.c\",  \"inftrees.c\",\n            \"trees.c\",   \"uncompr.c\",  \"zutil.c\",\n        },\n    });\n\n    return lib;\n}\n\nfn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) [3]*Build.Step.Compile {\n    const dep = b.dependency(\"brotli\", .{});\n\n    const mod = b.createModule(.{\n        .target = target,\n        .optimize = optimize,\n        .link_libc = true,\n        .sanitize_thread = is_tsan,\n    });\n    mod.addIncludePath(dep.path(\"c/include\"));\n\n    const brotlicmn = b.addLibrary(.{ .name = \"brotlicommon\", .root_module = mod });\n    const brotlidec = b.addLibrary(.{ .name = \"brotlidec\", .root_module = mod });\n    const brotlienc = b.addLibrary(.{ .name = \"brotlienc\", .root_module = mod });\n\n    brotlicmn.installHeadersDirectory(dep.path(\"c/include/brotli\"), \"brotli\", .{});\n    brotlicmn.addCSourceFiles(.{\n        .root = dep.path(\"c/common\"),\n        .files = &.{\n            \"transform.c\",  \"shared_dictionary.c\", \"platform.c\",\n            \"dictionary.c\", \"context.c\",           \"constants.c\",\n        },\n    });\n    brotlidec.addCSourceFiles(.{\n        .root = dep.path(\"c/dec\"),\n        .files = &.{\n            \"bit_reader.c\", \"decode.c\", \"huffman.c\",\n            \"prefix.c\",     \"state.c\",  \"static_init.c\",\n        },\n    });\n    brotlienc.addCSourceFiles(.{\n        .root = dep.path(\"c/enc\"),\n        .files = &.{\n            \"backward_references.c\",        \"backward_references_hq.c\", \"bit_cost.c\",\n            \"block_splitter.c\",             \"brotli_bit_stream.c\",      \"cluster.c\",\n            \"command.c\",                    \"compound_dictionary.c\",    \"compress_fragment.c\",\n            \"compress_fragment_two_pass.c\", \"dictionary_hash.c\",        \"encode.c\",\n            \"encoder_dict.c\",               \"entropy_encode.c\",         \"fast_log.c\",\n            \"histogram.c\",                  \"literal_cost.c\",           \"memory.c\",\n            \"metablock.c\",                  \"static_dict.c\",            \"static_dict_lut.c\",\n            \"static_init.c\",                \"utf8_util.c\",\n        },\n    });\n\n    return .{ brotlicmn, brotlidec, brotlienc };\n}\n\nfn buildBoringSsl(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) [2]*Build.Step.Compile {\n    const dep = b.dependency(\"boringssl-zig\", .{\n        .target = target,\n        .optimize = optimize,\n        .force_pic = true,\n    });\n\n    const ssl = dep.artifact(\"ssl\");\n    ssl.bundle_ubsan_rt = false;\n\n    const crypto = dep.artifact(\"crypto\");\n    crypto.bundle_ubsan_rt = false;\n\n    return .{ ssl, crypto };\n}\n\nfn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) *Build.Step.Compile {\n    const dep = b.dependency(\"nghttp2\", .{});\n\n    const mod = b.createModule(.{\n        .target = target,\n        .optimize = optimize,\n        .link_libc = true,\n        .sanitize_thread = is_tsan,\n    });\n    mod.addIncludePath(dep.path(\"lib/includes\"));\n\n    const config = b.addConfigHeader(.{\n        .include_path = \"nghttp2ver.h\",\n        .style = .{ .cmake = dep.path(\"lib/includes/nghttp2/nghttp2ver.h.in\") },\n    }, .{\n        .PACKAGE_VERSION = \"1.68.90\",\n        .PACKAGE_VERSION_NUM = 0x016890,\n    });\n    mod.addConfigHeader(config);\n\n    const lib = b.addLibrary(.{ .name = \"nghttp2\", .root_module = mod });\n\n    lib.installConfigHeader(config);\n    lib.installHeadersDirectory(dep.path(\"lib/includes/nghttp2\"), \"nghttp2\", .{});\n    lib.addCSourceFiles(.{\n        .root = dep.path(\"lib\"),\n        .flags = &.{\n            \"-DNGHTTP2_STATICLIB\",\n            \"-DHAVE_TIME_H\",\n            \"-DHAVE_ARPA_INET_H\",\n            \"-DHAVE_NETINET_IN_H\",\n        },\n        .files = &.{\n            \"sfparse.c\",                 \"nghttp2_alpn.c\",   \"nghttp2_buf.c\",\n            \"nghttp2_callbacks.c\",       \"nghttp2_debug.c\",  \"nghttp2_extpri.c\",\n            \"nghttp2_frame.c\",           \"nghttp2_hd.c\",     \"nghttp2_hd_huffman.c\",\n            \"nghttp2_hd_huffman_data.c\", \"nghttp2_helper.c\", \"nghttp2_http.c\",\n            \"nghttp2_map.c\",             \"nghttp2_mem.c\",    \"nghttp2_option.c\",\n            \"nghttp2_outbound_item.c\",   \"nghttp2_pq.c\",     \"nghttp2_priority_spec.c\",\n            \"nghttp2_queue.c\",           \"nghttp2_rcbuf.c\",  \"nghttp2_session.c\",\n            \"nghttp2_stream.c\",          \"nghttp2_submit.c\", \"nghttp2_version.c\",\n            \"nghttp2_ratelim.c\",         \"nghttp2_time.c\",\n        },\n    });\n\n    return lib;\n}\n\nfn buildCurl(\n    b: *Build,\n    target: Build.ResolvedTarget,\n    optimize: std.builtin.OptimizeMode,\n    is_tsan: bool,\n) *Build.Step.Compile {\n    const dep = b.dependency(\"curl\", .{});\n\n    const mod = b.createModule(.{\n        .target = target,\n        .optimize = optimize,\n        .link_libc = true,\n        .sanitize_thread = is_tsan,\n    });\n    mod.addIncludePath(dep.path(\"lib\"));\n    mod.addIncludePath(dep.path(\"include\"));\n\n    const os = target.result.os.tag;\n    const abi = target.result.abi;\n\n    const is_gnu = abi.isGnu();\n    const is_ios = os == .ios;\n    const is_android = abi.isAndroid();\n    const is_linux = os == .linux;\n    const is_darwin = os.isDarwin();\n    const is_windows = os == .windows;\n    const is_netbsd = os == .netbsd;\n    const is_openbsd = os == .openbsd;\n    const is_freebsd = os == .freebsd;\n\n    const byte_size = struct {\n        fn it(b2: *std.Build, target2: Build.ResolvedTarget, name: []const u8, comptime ctype: std.Target.CType) []const u8 {\n            const size = target2.result.cTypeByteSize(ctype);\n            return std.fmt.allocPrint(b2.allocator, \"#define SIZEOF_{s} {d}\", .{ name, size }) catch @panic(\"OOM\");\n        }\n    }.it;\n\n    const config = .{\n        .HAVE_LIBZ = true,\n        .HAVE_BROTLI = true,\n        .USE_NGHTTP2 = true,\n\n        .USE_OPENSSL = true,\n        .OPENSSL_IS_BORINGSSL = true,\n        .CURL_CA_PATH = null,\n        .CURL_CA_BUNDLE = null,\n        .CURL_CA_FALLBACK = false,\n        .CURL_CA_SEARCH_SAFE = false,\n        .CURL_DEFAULT_SSL_BACKEND = \"openssl\",\n\n        .CURL_DISABLE_AWS = true,\n        .CURL_DISABLE_DICT = true,\n        .CURL_DISABLE_DOH = true,\n        .CURL_DISABLE_FILE = true,\n        .CURL_DISABLE_FTP = true,\n        .CURL_DISABLE_GOPHER = true,\n        .CURL_DISABLE_KERBEROS_AUTH = true,\n        .CURL_DISABLE_IMAP = true,\n        .CURL_DISABLE_IPFS = true,\n        .CURL_DISABLE_LDAP = true,\n        .CURL_DISABLE_LDAPS = true,\n        .CURL_DISABLE_MQTT = true,\n        .CURL_DISABLE_NTLM = true,\n        .CURL_DISABLE_PROGRESS_METER = true,\n        .CURL_DISABLE_POP3 = true,\n        .CURL_DISABLE_RTSP = true,\n        .CURL_DISABLE_SMB = true,\n        .CURL_DISABLE_SMTP = true,\n        .CURL_DISABLE_TELNET = true,\n        .CURL_DISABLE_TFTP = true,\n\n        .ssize_t = null,\n        ._FILE_OFFSET_BITS = 64,\n\n        .USE_IPV6 = true,\n        .CURL_OS = switch (os) {\n            .linux => if (is_android) \"\\\"android\\\"\" else \"\\\"linux\\\"\",\n            else => std.fmt.allocPrint(b.allocator, \"\\\"{s}\\\"\", .{@tagName(os)}) catch @panic(\"OOM\"),\n        },\n\n        // Adjusts the sizes of variables\n        .SIZEOF_INT_CODE = byte_size(b, target, \"INT\", .int),\n        .SIZEOF_LONG_CODE = byte_size(b, target, \"LONG\", .long),\n        .SIZEOF_LONG_LONG_CODE = byte_size(b, target, \"LONG_LONG\", .longlong),\n\n        .SIZEOF_OFF_T_CODE = byte_size(b, target, \"OFF_T\", .longlong),\n        .SIZEOF_CURL_OFF_T_CODE = byte_size(b, target, \"CURL_OFF_T\", .longlong),\n        .SIZEOF_CURL_SOCKET_T_CODE = byte_size(b, target, \"CURL_SOCKET_T\", .int),\n\n        .SIZEOF_SIZE_T_CODE = byte_size(b, target, \"SIZE_T\", .longlong),\n        .SIZEOF_TIME_T_CODE = byte_size(b, target, \"TIME_T\", .longlong),\n\n        // headers availability\n        .HAVE_ARPA_INET_H = !is_windows,\n        .HAVE_DIRENT_H = true,\n        .HAVE_FCNTL_H = true,\n        .HAVE_IFADDRS_H = !is_windows,\n        .HAVE_IO_H = is_windows,\n        .HAVE_LIBGEN_H = true,\n        .HAVE_LINUX_TCP_H = is_linux and is_gnu,\n        .HAVE_LOCALE_H = true,\n        .HAVE_NETDB_H = !is_windows,\n        .HAVE_NETINET_IN6_H = is_android,\n        .HAVE_NETINET_IN_H = !is_windows,\n        .HAVE_NETINET_TCP_H = !is_windows,\n        .HAVE_NETINET_UDP_H = !is_windows,\n        .HAVE_NET_IF_H = !is_windows,\n        .HAVE_POLL_H = !is_windows,\n        .HAVE_PWD_H = !is_windows,\n        .HAVE_STDATOMIC_H = true,\n        .HAVE_STDBOOL_H = true,\n        .HAVE_STDDEF_H = true,\n        .HAVE_STDINT_H = true,\n        .HAVE_STRINGS_H = true,\n        .HAVE_STROPTS_H = false,\n        .HAVE_SYS_EVENTFD_H = is_linux or is_freebsd or is_netbsd,\n        .HAVE_SYS_FILIO_H = !is_linux and !is_windows,\n        .HAVE_SYS_IOCTL_H = !is_windows,\n        .HAVE_SYS_PARAM_H = true,\n        .HAVE_SYS_POLL_H = !is_windows,\n        .HAVE_SYS_RESOURCE_H = !is_windows,\n        .HAVE_SYS_SELECT_H = !is_windows,\n        .HAVE_SYS_SOCKIO_H = !is_linux and !is_windows,\n        .HAVE_SYS_TYPES_H = true,\n        .HAVE_SYS_UN_H = !is_windows,\n        .HAVE_SYS_UTIME_H = is_windows,\n        .HAVE_TERMIOS_H = !is_windows,\n        .HAVE_TERMIO_H = is_linux,\n        .HAVE_UNISTD_H = true,\n        .HAVE_UTIME_H = true,\n        .STDC_HEADERS = true,\n\n        // general environment\n        .CURL_KRB5_VERSION = null,\n        .HAVE_ALARM = !is_windows,\n        .HAVE_ARC4RANDOM = is_android,\n        .HAVE_ATOMIC = true,\n        .HAVE_BOOL_T = true,\n        .HAVE_BUILTIN_AVAILABLE = true,\n        .HAVE_CLOCK_GETTIME_MONOTONIC = !is_darwin and !is_windows,\n        .HAVE_CLOCK_GETTIME_MONOTONIC_RAW = is_linux,\n        .HAVE_FILE_OFFSET_BITS = true,\n        .HAVE_GETEUID = !is_windows,\n        .HAVE_GETPPID = !is_windows,\n        .HAVE_GETTIMEOFDAY = true,\n        .HAVE_GLIBC_STRERROR_R = is_gnu,\n        .HAVE_GMTIME_R = !is_windows,\n        .HAVE_LOCALTIME_R = !is_windows,\n        .HAVE_LONGLONG = !is_windows,\n        .HAVE_MACH_ABSOLUTE_TIME = is_darwin,\n        .HAVE_MEMRCHR = !is_darwin and !is_windows,\n        .HAVE_POSIX_STRERROR_R = !is_gnu and !is_windows,\n        .HAVE_PTHREAD_H = !is_windows,\n        .HAVE_SETLOCALE = true,\n        .HAVE_SETRLIMIT = !is_windows,\n        .HAVE_SIGACTION = !is_windows,\n        .HAVE_SIGINTERRUPT = !is_windows,\n        .HAVE_SIGNAL = true,\n        .HAVE_SIGSETJMP = !is_windows,\n        .HAVE_SIZEOF_SA_FAMILY_T = false,\n        .HAVE_SIZEOF_SUSECONDS_T = false,\n        .HAVE_SNPRINTF = true,\n        .HAVE_STRCASECMP = !is_windows,\n        .HAVE_STRCMPI = false,\n        .HAVE_STRDUP = true,\n        .HAVE_STRERROR_R = !is_windows,\n        .HAVE_STRICMP = false,\n        .HAVE_STRUCT_TIMEVAL = true,\n        .HAVE_TIME_T_UNSIGNED = false,\n        .HAVE_UTIME = true,\n        .HAVE_UTIMES = !is_windows,\n        .HAVE_WRITABLE_ARGV = !is_windows,\n        .HAVE__SETMODE = is_windows,\n        .USE_THREADS_POSIX = !is_windows,\n\n        // filesystem, network\n        .HAVE_ACCEPT4 = is_linux or is_freebsd or is_netbsd or is_openbsd,\n        .HAVE_BASENAME = true,\n        .HAVE_CLOSESOCKET = is_windows,\n        .HAVE_DECL_FSEEKO = !is_windows,\n        .HAVE_EVENTFD = is_linux or is_freebsd or is_netbsd,\n        .HAVE_FCNTL = !is_windows,\n        .HAVE_FCNTL_O_NONBLOCK = !is_windows,\n        .HAVE_FNMATCH = !is_windows,\n        .HAVE_FREEADDRINFO = true,\n        .HAVE_FSEEKO = !is_windows,\n        .HAVE_FSETXATTR = is_darwin or is_linux or is_netbsd,\n        .HAVE_FSETXATTR_5 = is_linux or is_netbsd,\n        .HAVE_FSETXATTR_6 = is_darwin,\n        .HAVE_FTRUNCATE = true,\n        .HAVE_GETADDRINFO = true,\n        .HAVE_GETADDRINFO_THREADSAFE = is_linux or is_freebsd or is_netbsd,\n        .HAVE_GETHOSTBYNAME_R = is_linux or is_freebsd,\n        .HAVE_GETHOSTBYNAME_R_3 = false,\n        .HAVE_GETHOSTBYNAME_R_3_REENTRANT = false,\n        .HAVE_GETHOSTBYNAME_R_5 = false,\n        .HAVE_GETHOSTBYNAME_R_5_REENTRANT = false,\n        .HAVE_GETHOSTBYNAME_R_6 = is_linux,\n        .HAVE_GETHOSTBYNAME_R_6_REENTRANT = is_linux,\n        .HAVE_GETHOSTNAME = true,\n        .HAVE_GETIFADDRS = if (is_windows) false else !is_android or target.result.os.versionRange().linux.android >= 24,\n        .HAVE_GETPASS_R = is_netbsd,\n        .HAVE_GETPEERNAME = true,\n        .HAVE_GETPWUID = !is_windows,\n        .HAVE_GETPWUID_R = !is_windows,\n        .HAVE_GETRLIMIT = !is_windows,\n        .HAVE_GETSOCKNAME = true,\n        .HAVE_IF_NAMETOINDEX = !is_windows,\n        .HAVE_INET_NTOP = !is_windows,\n        .HAVE_INET_PTON = !is_windows,\n        .HAVE_IOCTLSOCKET = is_windows,\n        .HAVE_IOCTLSOCKET_CAMEL = false,\n        .HAVE_IOCTLSOCKET_CAMEL_FIONBIO = false,\n        .HAVE_IOCTLSOCKET_FIONBIO = is_windows,\n        .HAVE_IOCTL_FIONBIO = !is_windows,\n        .HAVE_IOCTL_SIOCGIFADDR = !is_windows,\n        .HAVE_MSG_NOSIGNAL = !is_windows,\n        .HAVE_OPENDIR = true,\n        .HAVE_PIPE = !is_windows,\n        .HAVE_PIPE2 = is_linux or is_freebsd or is_netbsd or is_openbsd,\n        .HAVE_POLL = !is_windows,\n        .HAVE_REALPATH = !is_windows,\n        .HAVE_RECV = true,\n        .HAVE_SA_FAMILY_T = !is_windows,\n        .HAVE_SCHED_YIELD = !is_windows,\n        .HAVE_SELECT = true,\n        .HAVE_SEND = true,\n        .HAVE_SENDMMSG = !is_darwin and !is_windows,\n        .HAVE_SENDMSG = !is_windows,\n        .HAVE_SETMODE = !is_linux,\n        .HAVE_SETSOCKOPT_SO_NONBLOCK = false,\n        .HAVE_SOCKADDR_IN6_SIN6_ADDR = !is_windows,\n        .HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID = true,\n        .HAVE_SOCKET = true,\n        .HAVE_SOCKETPAIR = !is_windows,\n        .HAVE_STRUCT_SOCKADDR_STORAGE = true,\n        .HAVE_SUSECONDS_T = is_android or is_ios,\n        .USE_UNIX_SOCKETS = !is_windows,\n    };\n\n    const curl_config = b.addConfigHeader(.{\n        .include_path = \"curl_config.h\",\n        .style = .{ .cmake = dep.path(\"lib/curl_config-cmake.h.in\") },\n    }, .{\n        .CURL_EXTERN_SYMBOL = \"__attribute__ ((__visibility__ (\\\"default\\\"))\",\n    });\n    curl_config.addValues(config);\n\n    const lib = b.addLibrary(.{ .name = \"curl\", .root_module = mod });\n    lib.addConfigHeader(curl_config);\n    lib.installHeadersDirectory(dep.path(\"include/curl\"), \"curl\", .{});\n    lib.addCSourceFiles(.{\n        .root = dep.path(\"lib\"),\n        .flags = &.{\n            \"-D_GNU_SOURCE\",\n            \"-DHAVE_CONFIG_H\",\n            \"-DCURL_STATICLIB\",\n            \"-DBUILDING_LIBCURL\",\n        },\n        .files = &.{\n            // You can include all files from lib, libcurl uses #ifdef-guards to exclude code for disabled functions\n            \"altsvc.c\",              \"amigaos.c\",              \"asyn-ares.c\",\n            \"asyn-base.c\",           \"asyn-thrdd.c\",           \"bufq.c\",\n            \"bufref.c\",              \"cf-h1-proxy.c\",          \"cf-h2-proxy.c\",\n            \"cf-haproxy.c\",          \"cf-https-connect.c\",     \"cf-ip-happy.c\",\n            \"cf-socket.c\",           \"cfilters.c\",             \"conncache.c\",\n            \"connect.c\",             \"content_encoding.c\",     \"cookie.c\",\n            \"cshutdn.c\",             \"curl_addrinfo.c\",        \"curl_endian.c\",\n            \"curl_fnmatch.c\",        \"curl_fopen.c\",           \"curl_get_line.c\",\n            \"curl_gethostname.c\",    \"curl_gssapi.c\",          \"curl_memrchr.c\",\n            \"curl_ntlm_core.c\",      \"curl_range.c\",           \"curl_rtmp.c\",\n            \"curl_sasl.c\",           \"curl_sha512_256.c\",      \"curl_share.c\",\n            \"curl_sspi.c\",           \"curl_threads.c\",         \"curl_trc.c\",\n            \"curlx/base64.c\",        \"curlx/dynbuf.c\",         \"curlx/fopen.c\",\n            \"curlx/inet_ntop.c\",     \"curlx/inet_pton.c\",      \"curlx/multibyte.c\",\n            \"curlx/nonblock.c\",      \"curlx/strcopy.c\",        \"curlx/strerr.c\",\n            \"curlx/strparse.c\",      \"curlx/timediff.c\",       \"curlx/timeval.c\",\n            \"curlx/version_win32.c\", \"curlx/wait.c\",           \"curlx/warnless.c\",\n            \"curlx/winapi.c\",        \"cw-out.c\",               \"cw-pause.c\",\n            \"dict.c\",                \"dllmain.c\",              \"doh.c\",\n            \"dynhds.c\",              \"easy.c\",                 \"easygetopt.c\",\n            \"easyoptions.c\",         \"escape.c\",               \"fake_addrinfo.c\",\n            \"file.c\",                \"fileinfo.c\",             \"formdata.c\",\n            \"ftp.c\",                 \"ftplistparser.c\",        \"getenv.c\",\n            \"getinfo.c\",             \"gopher.c\",               \"hash.c\",\n            \"headers.c\",             \"hmac.c\",                 \"hostip.c\",\n            \"hostip4.c\",             \"hostip6.c\",              \"hsts.c\",\n            \"http.c\",                \"http1.c\",                \"http2.c\",\n            \"http_aws_sigv4.c\",      \"http_chunks.c\",          \"http_digest.c\",\n            \"http_negotiate.c\",      \"http_ntlm.c\",            \"http_proxy.c\",\n            \"httpsrr.c\",             \"idn.c\",                  \"if2ip.c\",\n            \"imap.c\",                \"ldap.c\",                 \"llist.c\",\n            \"macos.c\",               \"md4.c\",                  \"md5.c\",\n            \"memdebug.c\",            \"mime.c\",                 \"mprintf.c\",\n            \"mqtt.c\",                \"multi.c\",                \"multi_ev.c\",\n            \"multi_ntfy.c\",          \"netrc.c\",                \"noproxy.c\",\n            \"openldap.c\",            \"parsedate.c\",            \"pingpong.c\",\n            \"pop3.c\",                \"progress.c\",             \"psl.c\",\n            \"rand.c\",                \"ratelimit.c\",            \"request.c\",\n            \"rtsp.c\",                \"select.c\",               \"sendf.c\",\n            \"setopt.c\",              \"sha256.c\",               \"slist.c\",\n            \"smb.c\",                 \"smtp.c\",                 \"socketpair.c\",\n            \"socks.c\",               \"socks_gssapi.c\",         \"socks_sspi.c\",\n            \"splay.c\",               \"strcase.c\",              \"strdup.c\",\n            \"strequal.c\",            \"strerror.c\",             \"system_win32.c\",\n            \"telnet.c\",              \"tftp.c\",                 \"transfer.c\",\n            \"uint-bset.c\",           \"uint-hash.c\",            \"uint-spbset.c\",\n            \"uint-table.c\",          \"url.c\",                  \"urlapi.c\",\n            \"vauth/cleartext.c\",     \"vauth/cram.c\",           \"vauth/digest.c\",\n            \"vauth/digest_sspi.c\",   \"vauth/gsasl.c\",          \"vauth/krb5_gssapi.c\",\n            \"vauth/krb5_sspi.c\",     \"vauth/ntlm.c\",           \"vauth/ntlm_sspi.c\",\n            \"vauth/oauth2.c\",        \"vauth/spnego_gssapi.c\",  \"vauth/spnego_sspi.c\",\n            \"vauth/vauth.c\",         \"version.c\",              \"vquic/curl_ngtcp2.c\",\n            \"vquic/curl_osslq.c\",    \"vquic/curl_quiche.c\",    \"vquic/vquic-tls.c\",\n            \"vquic/vquic.c\",         \"vssh/libssh.c\",          \"vssh/libssh2.c\",\n            \"vssh/vssh.c\",           \"vtls/apple.c\",           \"vtls/cipher_suite.c\",\n            \"vtls/gtls.c\",           \"vtls/hostcheck.c\",       \"vtls/keylog.c\",\n            \"vtls/mbedtls.c\",        \"vtls/openssl.c\",         \"vtls/rustls.c\",\n            \"vtls/schannel.c\",       \"vtls/schannel_verify.c\", \"vtls/vtls.c\",\n            \"vtls/vtls_scache.c\",    \"vtls/vtls_spack.c\",      \"vtls/wolfssl.c\",\n            \"vtls/x509asn1.c\",       \"ws.c\",\n        },\n    });\n\n    return lib;\n}\n\nconst Manifest = struct {\n    version: []const u8,\n    minimum_zig_version: []const u8,\n\n    fn init(b: *std.Build) Manifest {\n        const input = @embedFile(\"build.zig.zon\");\n\n        var diagnostics: std.zon.parse.Diagnostics = .{};\n        defer diagnostics.deinit(b.allocator);\n\n        return std.zon.parse.fromSlice(Manifest, b.allocator, input, &diagnostics, .{\n            .free_on_error = true,\n            .ignore_unknown_fields = true,\n        }) catch |err| {\n            switch (err) {\n                error.OutOfMemory => @panic(\"OOM\"),\n                error.ParseZon => {\n                    std.debug.print(\"Parse diagnostics:\\n{f}\\n\", .{diagnostics});\n                    std.process.exit(1);\n                },\n            }\n        };\n    }\n};\n"
  },
  {
    "path": "build.zig.zon",
    "content": ".{\n    .name = .browser,\n    .version = \"0.0.0\",\n    .fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.\n    .minimum_zig_version = \"0.15.2\",\n    .dependencies = .{\n        .v8 = .{\n            .url = \"https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.4.tar.gz\",\n            .hash = \"v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup\",\n        },\n        // .v8 = .{ .path = \"../zig-v8-fork\" },\n        .brotli = .{\n            // v1.2.0\n            .url = \"https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz\",\n            .hash = \"N-V-__8AAJudKgCQCuIiH6MJjAiIJHfg_tT_Ew-0vZwVkCo_\",\n        },\n        .zlib = .{\n            .url = \"https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz\",\n            .hash = \"N-V-__8AAJ2cNgAgfBtAw33Bxfu1IWISDeKKSr3DAqoAysIJ\",\n        },\n        .nghttp2 = .{\n            .url = \"https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz\",\n            .hash = \"N-V-__8AAL15vQCI63ZL6Zaz5hJg6JTEgYXGbLnMFSnf7FT3\",\n        },\n        .@\"boringssl-zig\" = .{\n            .url = \"git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096\",\n            .hash = \"boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK\",\n        },\n        .curl = .{\n            .url = \"https://github.com/curl/curl/releases/download/curl-8_18_0/curl-8.18.0.tar.gz\",\n            .hash = \"N-V-__8AALp9QAGn6CCHZ6fK_FfMyGtG824LSHYHHasM3w-y\",\n        },\n    },\n    .paths = .{\"\"},\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"headless browser designed for AI and automation\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/release-25.05\";\n\n    zigPkgs.url = \"github:mitchellh/zig-overlay\";\n    zigPkgs.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    zlsPkg.url = \"github:zigtools/zls/0.15.0\";\n    zlsPkg.inputs.zig-overlay.follows = \"zigPkgs\";\n    zlsPkg.inputs.nixpkgs.follows = \"nixpkgs\";\n\n    fenix = {\n      url = \"github:nix-community/fenix\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs =\n    {\n      nixpkgs,\n      zigPkgs,\n      zlsPkg,\n      fenix,\n      flake-utils,\n      ...\n    }:\n    flake-utils.lib.eachDefaultSystem (\n      system:\n      let\n        overlays = [\n          (final: prev: {\n            zigpkgs = zigPkgs.packages.${prev.system};\n            zls = zlsPkg.packages.${prev.system}.default;\n          })\n        ];\n\n        pkgs = import nixpkgs {\n          inherit system overlays;\n        };\n\n        rustToolchain = fenix.packages.${system}.stable.toolchain;\n\n        # We need crtbeginS.o for building.\n        crtFiles = pkgs.runCommand \"crt-files\" { } ''\n          mkdir -p $out/lib\n          cp -r ${pkgs.gcc.cc}/lib/gcc $out/lib/gcc\n        '';\n\n        # This build pipeline is very unhappy without an FHS-compliant env.\n        fhs = pkgs.buildFHSEnv {\n          name = \"fhs-shell\";\n          multiArch = true;\n          targetPkgs =\n            pkgs: with pkgs; [\n              # Build Tools\n              zigpkgs.\"0.15.2\"\n              zls\n              rustToolchain\n              python3\n              pkg-config\n              cmake\n              gperf\n\n              # GCC\n              gcc\n              gcc.cc.lib\n              crtFiles\n\n              # Libaries\n              expat.dev\n              glib.dev\n              glibc.dev\n              zlib\n            ];\n        };\n      in\n      {\n        devShells.default = fhs.env;\n      }\n    );\n}\n"
  },
  {
    "path": "src/App.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Allocator = std.mem.Allocator;\n\nconst log = @import(\"log.zig\");\nconst Config = @import(\"Config.zig\");\nconst Snapshot = @import(\"browser/js/Snapshot.zig\");\nconst Platform = @import(\"browser/js/Platform.zig\");\nconst Telemetry = @import(\"telemetry/telemetry.zig\").Telemetry;\n\nconst Network = @import(\"network/Runtime.zig\");\npub const ArenaPool = @import(\"ArenaPool.zig\");\n\nconst App = @This();\n\nnetwork: Network,\nconfig: *const Config,\nplatform: Platform,\nsnapshot: Snapshot,\ntelemetry: Telemetry,\nallocator: Allocator,\narena_pool: ArenaPool,\napp_dir_path: ?[]const u8,\n\npub fn init(allocator: Allocator, config: *const Config) !*App {\n    const app = try allocator.create(App);\n    errdefer allocator.destroy(app);\n\n    app.* = .{\n        .config = config,\n        .allocator = allocator,\n        .network = undefined,\n        .platform = undefined,\n        .snapshot = undefined,\n        .app_dir_path = undefined,\n        .telemetry = undefined,\n        .arena_pool = undefined,\n    };\n\n    app.network = try Network.init(allocator, config);\n    errdefer app.network.deinit();\n\n    app.platform = try Platform.init();\n    errdefer app.platform.deinit();\n\n    app.snapshot = try Snapshot.load();\n    errdefer app.snapshot.deinit();\n\n    app.app_dir_path = getAndMakeAppDir(allocator);\n\n    app.telemetry = try Telemetry.init(app, config.mode);\n    errdefer app.telemetry.deinit(allocator);\n\n    app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);\n    errdefer app.arena_pool.deinit();\n\n    return app;\n}\n\npub fn shutdown(self: *const App) bool {\n    return self.network.shutdown.load(.acquire);\n}\n\npub fn deinit(self: *App) void {\n    const allocator = self.allocator;\n    if (self.app_dir_path) |app_dir_path| {\n        allocator.free(app_dir_path);\n        self.app_dir_path = null;\n    }\n    self.telemetry.deinit(allocator);\n    self.network.deinit();\n    self.snapshot.deinit();\n    self.platform.deinit();\n    self.arena_pool.deinit();\n\n    allocator.destroy(self);\n}\n\nfn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {\n    if (@import(\"builtin\").is_test) {\n        return allocator.dupe(u8, \"/tmp\") catch unreachable;\n    }\n    const app_dir_path = std.fs.getAppDataDir(allocator, \"lightpanda\") catch |err| {\n        log.warn(.app, \"get data dir\", .{ .err = err });\n        return null;\n    };\n\n    std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {\n        error.PathAlreadyExists => return app_dir_path,\n        else => {\n            allocator.free(app_dir_path);\n            log.warn(.app, \"create data dir\", .{ .err = err, .path = app_dir_path });\n            return null;\n        },\n    };\n    return app_dir_path;\n}\n"
  },
  {
    "path": "src/ArenaPool.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst ArenaPool = @This();\n\nallocator: Allocator,\nretain_bytes: usize,\nfree_list_len: u16 = 0,\nfree_list: ?*Entry = null,\nfree_list_max: u16,\nentry_pool: std.heap.MemoryPool(Entry),\nmutex: std.Thread.Mutex = .{},\n\nconst Entry = struct {\n    next: ?*Entry,\n    arena: ArenaAllocator,\n};\n\npub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {\n    return .{\n        .allocator = allocator,\n        .free_list_max = free_list_max,\n        .retain_bytes = retain_bytes,\n        .entry_pool = .init(allocator),\n    };\n}\n\npub fn deinit(self: *ArenaPool) void {\n    var entry = self.free_list;\n    while (entry) |e| {\n        entry = e.next;\n        e.arena.deinit();\n    }\n    self.entry_pool.deinit();\n}\n\npub fn acquire(self: *ArenaPool) !Allocator {\n    self.mutex.lock();\n    defer self.mutex.unlock();\n\n    if (self.free_list) |entry| {\n        self.free_list = entry.next;\n        self.free_list_len -= 1;\n        return entry.arena.allocator();\n    }\n\n    const entry = try self.entry_pool.create();\n    entry.* = .{\n        .next = null,\n        .arena = ArenaAllocator.init(self.allocator),\n    };\n\n    return entry.arena.allocator();\n}\n\npub fn release(self: *ArenaPool, allocator: Allocator) void {\n    const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));\n    const entry: *Entry = @fieldParentPtr(\"arena\", arena);\n\n    // Reset the arena before acquiring the lock to minimize lock hold time\n    _ = arena.reset(.{ .retain_with_limit = self.retain_bytes });\n\n    self.mutex.lock();\n    defer self.mutex.unlock();\n\n    const free_list_len = self.free_list_len;\n    if (free_list_len == self.free_list_max) {\n        arena.deinit();\n        self.entry_pool.destroy(entry);\n        return;\n    }\n\n    entry.next = self.free_list;\n    self.free_list_len = free_list_len + 1;\n    self.free_list = entry;\n}\n\npub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {\n    const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));\n    _ = arena.reset(.{ .retain_with_limit = retain });\n}\n\nconst testing = std.testing;\n\ntest \"arena pool - basic acquire and use\" {\n    var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);\n    defer pool.deinit();\n\n    const alloc = try pool.acquire();\n    const buf = try alloc.alloc(u8, 64);\n    @memset(buf, 0xAB);\n    try testing.expectEqual(@as(u8, 0xAB), buf[0]);\n\n    pool.release(alloc);\n}\n\ntest \"arena pool - reuse entry after release\" {\n    var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);\n    defer pool.deinit();\n\n    const alloc1 = try pool.acquire();\n    try testing.expectEqual(@as(u16, 0), pool.free_list_len);\n\n    pool.release(alloc1);\n    try testing.expectEqual(@as(u16, 1), pool.free_list_len);\n\n    // The same entry should be returned from the free list.\n    const alloc2 = try pool.acquire();\n    try testing.expectEqual(@as(u16, 0), pool.free_list_len);\n    try testing.expectEqual(alloc1.ptr, alloc2.ptr);\n\n    pool.release(alloc2);\n}\n\ntest \"arena pool - multiple concurrent arenas\" {\n    var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);\n    defer pool.deinit();\n\n    const a1 = try pool.acquire();\n    const a2 = try pool.acquire();\n    const a3 = try pool.acquire();\n\n    // All three must be distinct arenas.\n    try testing.expect(a1.ptr != a2.ptr);\n    try testing.expect(a2.ptr != a3.ptr);\n    try testing.expect(a1.ptr != a3.ptr);\n\n    _ = try a1.alloc(u8, 16);\n    _ = try a2.alloc(u8, 32);\n    _ = try a3.alloc(u8, 48);\n\n    pool.release(a1);\n    pool.release(a2);\n    pool.release(a3);\n\n    try testing.expectEqual(@as(u16, 3), pool.free_list_len);\n}\n\ntest \"arena pool - free list respects max limit\" {\n    // Cap the free list at 1 so the second release discards its arena.\n    var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);\n    defer pool.deinit();\n\n    const a1 = try pool.acquire();\n    const a2 = try pool.acquire();\n\n    pool.release(a1);\n    try testing.expectEqual(@as(u16, 1), pool.free_list_len);\n\n    // The free list is full; a2's arena should be destroyed, not queued.\n    pool.release(a2);\n    try testing.expectEqual(@as(u16, 1), pool.free_list_len);\n}\n\ntest \"arena pool - reset clears memory without releasing\" {\n    var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);\n    defer pool.deinit();\n\n    const alloc = try pool.acquire();\n\n    const buf = try alloc.alloc(u8, 128);\n    @memset(buf, 0xFF);\n\n    // reset() frees arena memory but keeps the allocator in-flight.\n    pool.reset(alloc, 0);\n\n    // The free list must stay empty; the allocator was not released.\n    try testing.expectEqual(@as(u16, 0), pool.free_list_len);\n\n    // Allocating again through the same arena must still work.\n    const buf2 = try alloc.alloc(u8, 64);\n    @memset(buf2, 0x00);\n    try testing.expectEqual(@as(u8, 0x00), buf2[0]);\n\n    pool.release(alloc);\n}\n\ntest \"arena pool - deinit with entries in free list\" {\n    // Verifies that deinit properly cleans up free-listed arenas (no leaks\n    // detected by the test allocator).\n    var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);\n\n    const a1 = try pool.acquire();\n    const a2 = try pool.acquire();\n    _ = try a1.alloc(u8, 256);\n    _ = try a2.alloc(u8, 512);\n    pool.release(a1);\n    pool.release(a2);\n    try testing.expectEqual(@as(u16, 2), pool.free_list_len);\n\n    pool.deinit();\n}\n"
  },
  {
    "path": "src/Config.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst Allocator = std.mem.Allocator;\n\nconst log = @import(\"log.zig\");\nconst dump = @import(\"browser/dump.zig\");\n\nconst WebBotAuthConfig = @import(\"network/WebBotAuth.zig\").Config;\n\npub const RunMode = enum {\n    help,\n    fetch,\n    serve,\n    version,\n    mcp,\n};\n\npub const MAX_LISTENERS = 16;\npub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;\n\n// max message size\n// +14 for max websocket payload overhead\n// +140 for the max control packet that might be interleaved in a message\npub const CDP_MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;\n\nmode: Mode,\nexec_name: []const u8,\nhttp_headers: HttpHeaders,\n\nconst Config = @This();\n\npub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {\n    var config = Config{\n        .mode = mode,\n        .exec_name = exec_name,\n        .http_headers = undefined,\n    };\n    config.http_headers = try HttpHeaders.init(allocator, &config);\n    return config;\n}\n\npub fn deinit(self: *const Config, allocator: Allocator) void {\n    self.http_headers.deinit(allocator);\n}\n\npub fn tlsVerifyHost(self: *const Config) bool {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.tls_verify_host,\n        else => unreachable,\n    };\n}\n\npub fn obeyRobots(self: *const Config) bool {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.obey_robots,\n        else => unreachable,\n    };\n}\n\npub fn httpProxy(self: *const Config) ?[:0]const u8 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.http_proxy,\n        else => unreachable,\n    };\n}\n\npub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.proxy_bearer_token,\n        .help, .version => null,\n    };\n}\n\npub fn httpMaxConcurrent(self: *const Config) u8 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.http_max_concurrent orelse 10,\n        else => unreachable,\n    };\n}\n\npub fn httpMaxHostOpen(self: *const Config) u8 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.http_max_host_open orelse 4,\n        else => unreachable,\n    };\n}\n\npub fn httpConnectTimeout(self: *const Config) u31 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.http_connect_timeout orelse 0,\n        else => unreachable,\n    };\n}\n\npub fn httpTimeout(self: *const Config) u31 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.http_timeout orelse 5000,\n        else => unreachable,\n    };\n}\n\npub fn httpMaxRedirects(_: *const Config) u8 {\n    return 10;\n}\n\npub fn httpMaxResponseSize(self: *const Config) ?usize {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.http_max_response_size,\n        else => unreachable,\n    };\n}\n\npub fn logLevel(self: *const Config) ?log.Level {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.log_level,\n        else => unreachable,\n    };\n}\n\npub fn logFormat(self: *const Config) ?log.Format {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.log_format,\n        else => unreachable,\n    };\n}\n\npub fn logFilterScopes(self: *const Config) ?[]const log.Scope {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.log_filter_scopes,\n        else => unreachable,\n    };\n}\n\npub fn userAgentSuffix(self: *const Config) ?[]const u8 {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| opts.common.user_agent_suffix,\n        .help, .version => null,\n    };\n}\n\npub fn cdpTimeout(self: *const Config) usize {\n    return switch (self.mode) {\n        .serve => |opts| if (opts.timeout > 604_800) 604_800_000 else @as(usize, opts.timeout) * 1000,\n        else => unreachable,\n    };\n}\n\npub fn webBotAuth(self: *const Config) ?WebBotAuthConfig {\n    return switch (self.mode) {\n        inline .serve, .fetch, .mcp => |opts| WebBotAuthConfig{\n            .key_file = opts.common.web_bot_auth_key_file orelse return null,\n            .keyid = opts.common.web_bot_auth_keyid orelse return null,\n            .domain = opts.common.web_bot_auth_domain orelse return null,\n        },\n        .help, .version => null,\n    };\n}\n\npub fn maxConnections(self: *const Config) u16 {\n    return switch (self.mode) {\n        .serve => |opts| opts.cdp_max_connections,\n        else => unreachable,\n    };\n}\n\npub fn maxPendingConnections(self: *const Config) u31 {\n    return switch (self.mode) {\n        .serve => |opts| opts.cdp_max_pending_connections,\n        else => unreachable,\n    };\n}\n\npub const Mode = union(RunMode) {\n    help: bool, // false when being printed because of an error\n    fetch: Fetch,\n    serve: Serve,\n    version: void,\n    mcp: Mcp,\n};\n\npub const Serve = struct {\n    host: []const u8 = \"127.0.0.1\",\n    port: u16 = 9222,\n    timeout: u31 = 10,\n    cdp_max_connections: u16 = 16,\n    cdp_max_pending_connections: u16 = 128,\n    common: Common = .{},\n};\n\npub const Mcp = struct {\n    common: Common = .{},\n};\n\npub const DumpFormat = enum {\n    html,\n    markdown,\n    wpt,\n    semantic_tree,\n    semantic_tree_text,\n};\n\npub const Fetch = struct {\n    url: [:0]const u8,\n    dump_mode: ?DumpFormat = null,\n    common: Common = .{},\n    with_base: bool = false,\n    with_frames: bool = false,\n    strip: dump.Opts.Strip = .{},\n};\n\npub const Common = struct {\n    obey_robots: bool = false,\n    proxy_bearer_token: ?[:0]const u8 = null,\n    http_proxy: ?[:0]const u8 = null,\n    http_max_concurrent: ?u8 = null,\n    http_max_host_open: ?u8 = null,\n    http_timeout: ?u31 = null,\n    http_connect_timeout: ?u31 = null,\n    http_max_response_size: ?usize = null,\n    tls_verify_host: bool = true,\n    log_level: ?log.Level = null,\n    log_format: ?log.Format = null,\n    log_filter_scopes: ?[]log.Scope = null,\n    user_agent_suffix: ?[]const u8 = null,\n\n    web_bot_auth_key_file: ?[]const u8 = null,\n    web_bot_auth_keyid: ?[]const u8 = null,\n    web_bot_auth_domain: ?[]const u8 = null,\n};\n\n/// Pre-formatted HTTP headers for reuse across Http and Client.\n/// Must be initialized with an allocator that outlives all HTTP connections.\npub const HttpHeaders = struct {\n    const user_agent_base: [:0]const u8 = \"Lightpanda/1.0\";\n\n    user_agent: [:0]const u8, // User agent value (e.g. \"Lightpanda/1.0\")\n    user_agent_header: [:0]const u8,\n\n    proxy_bearer_header: ?[:0]const u8,\n\n    pub fn init(allocator: Allocator, config: *const Config) !HttpHeaders {\n        const user_agent: [:0]const u8 = if (config.userAgentSuffix()) |suffix|\n            try std.fmt.allocPrintSentinel(allocator, \"{s} {s}\", .{ user_agent_base, suffix }, 0)\n        else\n            user_agent_base;\n        errdefer if (config.userAgentSuffix() != null) allocator.free(user_agent);\n\n        const user_agent_header = try std.fmt.allocPrintSentinel(allocator, \"User-Agent: {s}\", .{user_agent}, 0);\n        errdefer allocator.free(user_agent_header);\n\n        const proxy_bearer_header: ?[:0]const u8 = if (config.proxyBearerToken()) |token|\n            try std.fmt.allocPrintSentinel(allocator, \"Proxy-Authorization: Bearer {s}\", .{token}, 0)\n        else\n            null;\n\n        return .{\n            .user_agent = user_agent,\n            .user_agent_header = user_agent_header,\n            .proxy_bearer_header = proxy_bearer_header,\n        };\n    }\n\n    pub fn deinit(self: *const HttpHeaders, allocator: Allocator) void {\n        if (self.proxy_bearer_header) |hdr| {\n            allocator.free(hdr);\n        }\n        allocator.free(self.user_agent_header);\n        if (self.user_agent.ptr != user_agent_base.ptr) {\n            allocator.free(self.user_agent);\n        }\n    }\n};\n\npub fn printUsageAndExit(self: *const Config, success: bool) void {\n    //                                                                     MAX_HELP_LEN|\n    const common_options =\n        \\\\\n        \\\\--insecure_disable_tls_host_verification\n        \\\\                Disables host verification on all HTTP requests. This is an\n        \\\\                advanced option which should only be set if you understand\n        \\\\                and accept the risk of disabling host verification.\n        \\\\\n        \\\\--obey_robots\n        \\\\                Fetches and obeys the robots.txt (if available) of the web pages\n        \\\\                we make requests towards.\n        \\\\                Defaults to false.\n        \\\\\n        \\\\--http_proxy    The HTTP proxy to use for all HTTP requests.\n        \\\\                A username:password can be included for basic authentication.\n        \\\\                Defaults to none.\n        \\\\\n        \\\\--proxy_bearer_token\n        \\\\                The <token> to send for bearer authentication with the proxy\n        \\\\                Proxy-Authorization: Bearer <token>\n        \\\\\n        \\\\--http_max_concurrent\n        \\\\                The maximum number of concurrent HTTP requests.\n        \\\\                Defaults to 10.\n        \\\\\n        \\\\--http_max_host_open\n        \\\\                The maximum number of open connection to a given host:port.\n        \\\\                Defaults to 4.\n        \\\\\n        \\\\--http_connect_timeout\n        \\\\                The time, in milliseconds, for establishing an HTTP connection\n        \\\\                before timing out. 0 means it never times out.\n        \\\\                Defaults to 0.\n        \\\\\n        \\\\--http_timeout\n        \\\\                The maximum time, in milliseconds, the transfer is allowed\n        \\\\                to complete. 0 means it never times out.\n        \\\\                Defaults to 10000.\n        \\\\\n        \\\\--http_max_response_size\n        \\\\                Limits the acceptable response size for any request\n        \\\\                (e.g. XHR, fetch, script loading, ...).\n        \\\\                Defaults to no limit.\n        \\\\\n        \\\\--log_level     The log level: debug, info, warn, error or fatal.\n        \\\\                Defaults to\n    ++ (if (builtin.mode == .Debug) \" info.\" else \"warn.\") ++\n        \\\\\n        \\\\\n        \\\\--log_format    The log format: pretty or logfmt.\n        \\\\                Defaults to\n    ++ (if (builtin.mode == .Debug) \" pretty.\" else \" logfmt.\") ++\n        \\\\\n        \\\\\n        \\\\--log_filter_scopes\n        \\\\                Filter out too verbose logs per scope:\n        \\\\                http, unknown_prop, event, ...\n        \\\\\n        \\\\--user_agent_suffix\n        \\\\                Suffix to append to the Lightpanda/X.Y User-Agent\n        \\\\\n        \\\\--web_bot_auth_key_file\n        \\\\                Path to the Ed25519 private key PEM file.\n        \\\\\n        \\\\--web_bot_auth_keyid\n        \\\\                The JWK thumbprint of your public key.\n        \\\\\n        \\\\--web_bot_auth_domain\n        \\\\                Your domain e.g. yourdomain.com\n    ;\n\n    //                                                                     MAX_HELP_LEN|\n    const usage =\n        \\\\usage: {s} command [options] [URL]\n        \\\\\n        \\\\Command can be either 'fetch', 'serve', 'mcp' or 'help'\n        \\\\\n        \\\\fetch command\n        \\\\Fetches the specified URL\n        \\\\Example: {s} fetch --dump html https://lightpanda.io/\n        \\\\\n        \\\\Options:\n        \\\\--dump          Dumps document to stdout.\n        \\\\                Argument must be 'html', 'markdown', 'semantic_tree', or 'semantic_tree_text'.\n        \\\\                Defaults to no dump.\n        \\\\\n        \\\\--strip_mode    Comma separated list of tag groups to remove from dump\n        \\\\                the dump. e.g. --strip_mode js,css\n        \\\\                  - \"js\" script and link[as=script, rel=preload]\n        \\\\                  - \"ui\" includes img, picture, video, css and svg\n        \\\\                  - \"css\" includes style and link[rel=stylesheet]\n        \\\\                  - \"full\" includes js, ui and css\n        \\\\\n        \\\\--with_base     Add a <base> tag in dump. Defaults to false.\n        \\\\\n        \\\\--with_frames   Includes the contents of iframes. Defaults to false.\n        \\\\\n    ++ common_options ++\n        \\\\\n        \\\\serve command\n        \\\\Starts a websocket CDP server\n        \\\\Example: {s} serve --host 127.0.0.1 --port 9222\n        \\\\\n        \\\\Options:\n        \\\\--host          Host of the CDP server\n        \\\\                Defaults to \"127.0.0.1\"\n        \\\\\n        \\\\--port          Port of the CDP server\n        \\\\                Defaults to 9222\n        \\\\\n        \\\\--timeout       Inactivity timeout in seconds before disconnecting clients\n        \\\\                Defaults to 10 (seconds). Limited to 604800 (1 week).\n        \\\\\n        \\\\--cdp_max_connections\n        \\\\                Maximum number of simultaneous CDP connections.\n        \\\\                Defaults to 16.\n        \\\\\n        \\\\--cdp_max_pending_connections\n        \\\\                Maximum pending connections in the accept queue.\n        \\\\                Defaults to 128.\n        \\\\\n    ++ common_options ++\n        \\\\\n        \\\\mcp command\n        \\\\Starts an MCP (Model Context Protocol) server over stdio\n        \\\\Example: {s} mcp\n        \\\\\n    ++ common_options ++\n        \\\\\n        \\\\version command\n        \\\\Displays the version of {s}\n        \\\\\n        \\\\help command\n        \\\\Displays this message\n        \\\\\n    ;\n    std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name, self.exec_name });\n    if (success) {\n        return std.process.cleanExit();\n    }\n    std.process.exit(1);\n}\n\npub fn parseArgs(allocator: Allocator) !Config {\n    var args = try std.process.argsWithAllocator(allocator);\n    defer args.deinit();\n\n    const exec_name = try allocator.dupe(u8, std.fs.path.basename(args.next().?));\n\n    const mode_string = args.next() orelse \"\";\n    const run_mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {\n        const inferred_mode = inferMode(mode_string) orelse\n            return init(allocator, exec_name, .{ .help = false });\n        // \"command\" wasn't a command but an option. We can't reset args, but\n        // we can create a new one. Not great, but this fallback is temporary\n        // as we transition to this command mode approach.\n        args.deinit();\n\n        args = try std.process.argsWithAllocator(allocator);\n        // skip the exec_name\n        _ = args.skip();\n\n        break :blk inferred_mode;\n    };\n\n    const mode: Mode = switch (run_mode) {\n        .help => .{ .help = true },\n        .serve => .{ .serve = parseServeArgs(allocator, &args) catch\n            return init(allocator, exec_name, .{ .help = false }) },\n        .fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch\n            return init(allocator, exec_name, .{ .help = false }) },\n        .mcp => .{ .mcp = parseMcpArgs(allocator, &args) catch\n            return init(allocator, exec_name, .{ .help = false }) },\n        .version => .{ .version = {} },\n    };\n    return init(allocator, exec_name, mode);\n}\n\nfn inferMode(opt: []const u8) ?RunMode {\n    if (opt.len == 0) {\n        return .serve;\n    }\n\n    if (std.mem.startsWith(u8, opt, \"--\") == false) {\n        return .fetch;\n    }\n\n    if (std.mem.eql(u8, opt, \"--dump\")) {\n        return .fetch;\n    }\n\n    if (std.mem.eql(u8, opt, \"--noscript\")) {\n        return .fetch;\n    }\n\n    if (std.mem.eql(u8, opt, \"--strip_mode\")) {\n        return .fetch;\n    }\n\n    if (std.mem.eql(u8, opt, \"--with_base\")) {\n        return .fetch;\n    }\n\n    if (std.mem.eql(u8, opt, \"--with_frames\")) {\n        return .fetch;\n    }\n\n    if (std.mem.eql(u8, opt, \"--host\")) {\n        return .serve;\n    }\n\n    if (std.mem.eql(u8, opt, \"--port\")) {\n        return .serve;\n    }\n\n    if (std.mem.eql(u8, opt, \"--timeout\")) {\n        return .serve;\n    }\n\n    return null;\n}\n\nfn parseServeArgs(\n    allocator: Allocator,\n    args: *std.process.ArgIterator,\n) !Serve {\n    var serve: Serve = .{};\n\n    while (args.next()) |opt| {\n        if (std.mem.eql(u8, \"--host\", opt)) {\n            const str = args.next() orelse {\n                log.fatal(.app, \"missing argument value\", .{ .arg = \"--host\" });\n                return error.InvalidArgument;\n            };\n            serve.host = try allocator.dupe(u8, str);\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--port\", opt)) {\n            const str = args.next() orelse {\n                log.fatal(.app, \"missing argument value\", .{ .arg = \"--port\" });\n                return error.InvalidArgument;\n            };\n\n            serve.port = std.fmt.parseInt(u16, str, 10) catch |err| {\n                log.fatal(.app, \"invalid argument value\", .{ .arg = \"--port\", .err = err });\n                return error.InvalidArgument;\n            };\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--timeout\", opt)) {\n            const str = args.next() orelse {\n                log.fatal(.app, \"missing argument value\", .{ .arg = \"--timeout\" });\n                return error.InvalidArgument;\n            };\n\n            serve.timeout = std.fmt.parseInt(u31, str, 10) catch |err| {\n                log.fatal(.app, \"invalid argument value\", .{ .arg = \"--timeout\", .err = err });\n                return error.InvalidArgument;\n            };\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--cdp_max_connections\", opt)) {\n            const str = args.next() orelse {\n                log.fatal(.app, \"missing argument value\", .{ .arg = \"--cdp_max_connections\" });\n                return error.InvalidArgument;\n            };\n\n            serve.cdp_max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {\n                log.fatal(.app, \"invalid argument value\", .{ .arg = \"--cdp_max_connections\", .err = err });\n                return error.InvalidArgument;\n            };\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--cdp_max_pending_connections\", opt)) {\n            const str = args.next() orelse {\n                log.fatal(.app, \"missing argument value\", .{ .arg = \"--cdp_max_pending_connections\" });\n                return error.InvalidArgument;\n            };\n\n            serve.cdp_max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {\n                log.fatal(.app, \"invalid argument value\", .{ .arg = \"--cdp_max_pending_connections\", .err = err });\n                return error.InvalidArgument;\n            };\n            continue;\n        }\n\n        if (try parseCommonArg(allocator, opt, args, &serve.common)) {\n            continue;\n        }\n\n        log.fatal(.app, \"unknown argument\", .{ .mode = \"serve\", .arg = opt });\n        return error.UnkownOption;\n    }\n\n    return serve;\n}\n\nfn parseMcpArgs(\n    allocator: Allocator,\n    args: *std.process.ArgIterator,\n) !Mcp {\n    var mcp: Mcp = .{};\n\n    while (args.next()) |opt| {\n        if (try parseCommonArg(allocator, opt, args, &mcp.common)) {\n            continue;\n        }\n\n        log.fatal(.mcp, \"unknown argument\", .{ .mode = \"mcp\", .arg = opt });\n        return error.UnkownOption;\n    }\n\n    return mcp;\n}\n\nfn parseFetchArgs(\n    allocator: Allocator,\n    args: *std.process.ArgIterator,\n) !Fetch {\n    var dump_mode: ?DumpFormat = null;\n    var with_base: bool = false;\n    var with_frames: bool = false;\n    var url: ?[:0]const u8 = null;\n    var common: Common = .{};\n    var strip: dump.Opts.Strip = .{};\n\n    while (args.next()) |opt| {\n        if (std.mem.eql(u8, \"--dump\", opt)) {\n            var peek_args = args.*;\n            if (peek_args.next()) |next_arg| {\n                if (std.meta.stringToEnum(DumpFormat, next_arg)) |mode| {\n                    dump_mode = mode;\n                    _ = args.next();\n                } else {\n                    dump_mode = .html;\n                }\n            } else {\n                dump_mode = .html;\n            }\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--noscript\", opt)) {\n            log.warn(.app, \"deprecation warning\", .{\n                .feature = \"--noscript argument\",\n                .hint = \"use '--strip_mode js' instead\",\n            });\n            strip.js = true;\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--with_base\", opt)) {\n            with_base = true;\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--with_frames\", opt)) {\n            with_frames = true;\n            continue;\n        }\n\n        if (std.mem.eql(u8, \"--strip_mode\", opt)) {\n            const str = args.next() orelse {\n                log.fatal(.app, \"missing argument value\", .{ .arg = \"--strip_mode\" });\n                return error.InvalidArgument;\n            };\n\n            var it = std.mem.splitScalar(u8, str, ',');\n            while (it.next()) |part| {\n                const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace);\n                if (std.mem.eql(u8, trimmed, \"js\")) {\n                    strip.js = true;\n                } else if (std.mem.eql(u8, trimmed, \"ui\")) {\n                    strip.ui = true;\n                } else if (std.mem.eql(u8, trimmed, \"css\")) {\n                    strip.css = true;\n                } else if (std.mem.eql(u8, trimmed, \"full\")) {\n                    strip.js = true;\n                    strip.ui = true;\n                    strip.css = true;\n                } else {\n                    log.fatal(.app, \"invalid option choice\", .{ .arg = \"--strip_mode\", .value = trimmed });\n                }\n            }\n            continue;\n        }\n\n        if (try parseCommonArg(allocator, opt, args, &common)) {\n            continue;\n        }\n\n        if (std.mem.startsWith(u8, opt, \"--\")) {\n            log.fatal(.app, \"unknown argument\", .{ .mode = \"fetch\", .arg = opt });\n            return error.UnkownOption;\n        }\n\n        if (url != null) {\n            log.fatal(.app, \"duplicate fetch url\", .{ .help = \"only 1 URL can be specified\" });\n            return error.TooManyURLs;\n        }\n        url = try allocator.dupeZ(u8, opt);\n    }\n\n    if (url == null) {\n        log.fatal(.app, \"missing fetch url\", .{ .help = \"URL to fetch must be provided\" });\n        return error.MissingURL;\n    }\n\n    return .{\n        .url = url.?,\n        .dump_mode = dump_mode,\n        .strip = strip,\n        .common = common,\n        .with_base = with_base,\n        .with_frames = with_frames,\n    };\n}\n\nfn parseCommonArg(\n    allocator: Allocator,\n    opt: []const u8,\n    args: *std.process.ArgIterator,\n    common: *Common,\n) !bool {\n    if (std.mem.eql(u8, \"--insecure_disable_tls_host_verification\", opt)) {\n        common.tls_verify_host = false;\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--obey_robots\", opt)) {\n        common.obey_robots = true;\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--http_proxy\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--http_proxy\" });\n            return error.InvalidArgument;\n        };\n        common.http_proxy = try allocator.dupeZ(u8, str);\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--proxy_bearer_token\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--proxy_bearer_token\" });\n            return error.InvalidArgument;\n        };\n        common.proxy_bearer_token = try allocator.dupeZ(u8, str);\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--http_max_concurrent\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--http_max_concurrent\" });\n            return error.InvalidArgument;\n        };\n\n        common.http_max_concurrent = std.fmt.parseInt(u8, str, 10) catch |err| {\n            log.fatal(.app, \"invalid argument value\", .{ .arg = \"--http_max_concurrent\", .err = err });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--http_max_host_open\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--http_max_host_open\" });\n            return error.InvalidArgument;\n        };\n\n        common.http_max_host_open = std.fmt.parseInt(u8, str, 10) catch |err| {\n            log.fatal(.app, \"invalid argument value\", .{ .arg = \"--http_max_host_open\", .err = err });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--http_connect_timeout\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--http_connect_timeout\" });\n            return error.InvalidArgument;\n        };\n\n        common.http_connect_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {\n            log.fatal(.app, \"invalid argument value\", .{ .arg = \"--http_connect_timeout\", .err = err });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--http_timeout\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--http_timeout\" });\n            return error.InvalidArgument;\n        };\n\n        common.http_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {\n            log.fatal(.app, \"invalid argument value\", .{ .arg = \"--http_timeout\", .err = err });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--http_max_response_size\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--http_max_response_size\" });\n            return error.InvalidArgument;\n        };\n\n        common.http_max_response_size = std.fmt.parseInt(usize, str, 10) catch |err| {\n            log.fatal(.app, \"invalid argument value\", .{ .arg = \"--http_max_response_size\", .err = err });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--log_level\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--log_level\" });\n            return error.InvalidArgument;\n        };\n\n        common.log_level = std.meta.stringToEnum(log.Level, str) orelse blk: {\n            if (std.mem.eql(u8, str, \"error\")) {\n                break :blk .err;\n            }\n            log.fatal(.app, \"invalid option choice\", .{ .arg = \"--log_level\", .value = str });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--log_format\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--log_format\" });\n            return error.InvalidArgument;\n        };\n\n        common.log_format = std.meta.stringToEnum(log.Format, str) orelse {\n            log.fatal(.app, \"invalid option choice\", .{ .arg = \"--log_format\", .value = str });\n            return error.InvalidArgument;\n        };\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--log_filter_scopes\", opt)) {\n        if (builtin.mode != .Debug) {\n            log.fatal(.app, \"experimental\", .{ .help = \"log scope filtering is only available in debug builds\" });\n            return false;\n        }\n\n        const str = args.next() orelse {\n            // disables the default filters\n            common.log_filter_scopes = &.{};\n            return true;\n        };\n\n        var arr: std.ArrayList(log.Scope) = .empty;\n\n        var it = std.mem.splitScalar(u8, str, ',');\n        while (it.next()) |part| {\n            try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {\n                log.fatal(.app, \"invalid option choice\", .{ .arg = \"--log_filter_scopes\", .value = part });\n                return false;\n            });\n        }\n        common.log_filter_scopes = arr.items;\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--user_agent_suffix\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--user_agent_suffix\" });\n            return error.InvalidArgument;\n        };\n        for (str) |c| {\n            if (!std.ascii.isPrint(c)) {\n                log.fatal(.app, \"not printable character\", .{ .arg = \"--user_agent_suffix\" });\n                return error.InvalidArgument;\n            }\n        }\n        common.user_agent_suffix = try allocator.dupe(u8, str);\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--web_bot_auth_key_file\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--web_bot_auth_key_file\" });\n            return error.InvalidArgument;\n        };\n        common.web_bot_auth_key_file = try allocator.dupe(u8, str);\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--web_bot_auth_keyid\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--web_bot_auth_keyid\" });\n            return error.InvalidArgument;\n        };\n        common.web_bot_auth_keyid = try allocator.dupe(u8, str);\n        return true;\n    }\n\n    if (std.mem.eql(u8, \"--web_bot_auth_domain\", opt)) {\n        const str = args.next() orelse {\n            log.fatal(.app, \"missing argument value\", .{ .arg = \"--web_bot_auth_domain\" });\n            return error.InvalidArgument;\n        };\n        common.web_bot_auth_domain = try allocator.dupe(u8, str);\n        return true;\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "src/Notification.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst log = @import(\"log.zig\");\nconst Page = @import(\"browser/Page.zig\");\nconst Transfer = @import(\"browser/HttpClient.zig\").Transfer;\n\nconst Allocator = std.mem.Allocator;\n\nconst List = std.DoublyLinkedList;\n\n// Allows code to register for and emit events.\n// Keeps two lists\n// 1 - for a given event type, a linked list of all the listeners\n// 2 - for a given listener, a list of all it's registration\n// The 2nd one is so that a listener can unregister all of it's listeners\n// (there's currently no need for a listener to unregister only 1 or more\n// specific listener).\n//\n// Scoping is important. Imagine we created a global singleton registry, and our\n// CDP code registers for the \"network_bytes_sent\" event, because it needs to\n// send messages to the client when this happens. Our HTTP client could then\n// emit a \"network_bytes_sent\" message. It would be easy, and it would work.\n// That is, it would work until multiple CDP clients connect, and because\n// everything's just one big global, events from one CDP session would be sent\n// to all CDP clients.\n//\n// To avoid this, one way or another, we need scoping. We could still have\n// a global registry but every \"register\" and every \"emit\" has some type of\n// \"scope\". This would have a run-time cost and still require some coordination\n// between components to share a common scope.\n//\n// Instead, the approach that we take is to have a notification instance per\n// CDP connection (BrowserContext). Each CDP connection has its own notification\n// that is shared across all Sessions (tabs) within that connection. This ensures\n// proper isolation between different CDP clients while allowing a single client\n// to receive events from all its tabs.\nconst Notification = @This();\n// Every event type (which are hard-coded), has a list of Listeners.\n// When the event happens, we dispatch to those listener.\nevent_listeners: EventListeners,\n\n// list of listeners for a specified receiver\n// @intFromPtr(receiver) -> [listener1, listener2, ...]\n// Used when `unregisterAll` is called.\nlisteners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),\n\nallocator: Allocator,\nmem_pool: std.heap.MemoryPool(Listener),\n\nconst EventListeners = struct {\n    page_remove: List = .{},\n    page_created: List = .{},\n    page_navigate: List = .{},\n    page_navigated: List = .{},\n    page_network_idle: List = .{},\n    page_network_almost_idle: List = .{},\n    page_frame_created: List = .{},\n    http_request_fail: List = .{},\n    http_request_start: List = .{},\n    http_request_intercept: List = .{},\n    http_request_done: List = .{},\n    http_request_auth_required: List = .{},\n    http_response_data: List = .{},\n    http_response_header_done: List = .{},\n};\n\nconst Events = union(enum) {\n    page_remove: PageRemove,\n    page_created: *Page,\n    page_navigate: *const PageNavigate,\n    page_navigated: *const PageNavigated,\n    page_network_idle: *const PageNetworkIdle,\n    page_network_almost_idle: *const PageNetworkAlmostIdle,\n    page_frame_created: *const PageFrameCreated,\n    http_request_fail: *const RequestFail,\n    http_request_start: *const RequestStart,\n    http_request_intercept: *const RequestIntercept,\n    http_request_auth_required: *const RequestAuthRequired,\n    http_request_done: *const RequestDone,\n    http_response_data: *const ResponseData,\n    http_response_header_done: *const ResponseHeaderDone,\n};\nconst EventType = std.meta.FieldEnum(Events);\n\npub const PageRemove = struct {};\n\npub const PageNavigate = struct {\n    req_id: u32,\n    frame_id: u32,\n    timestamp: u64,\n    url: [:0]const u8,\n    opts: Page.NavigateOpts,\n};\n\npub const PageNavigated = struct {\n    req_id: u32,\n    frame_id: u32,\n    timestamp: u64,\n    url: [:0]const u8,\n    opts: Page.NavigatedOpts,\n};\n\npub const PageNetworkIdle = struct {\n    req_id: u32,\n    frame_id: u32,\n    timestamp: u64,\n};\n\npub const PageNetworkAlmostIdle = struct {\n    req_id: u32,\n    frame_id: u32,\n    timestamp: u64,\n};\n\npub const PageFrameCreated = struct {\n    frame_id: u32,\n    parent_id: u32,\n    timestamp: u64,\n};\n\npub const RequestStart = struct {\n    transfer: *Transfer,\n};\n\npub const RequestIntercept = struct {\n    transfer: *Transfer,\n    wait_for_interception: *bool,\n};\n\npub const RequestAuthRequired = struct {\n    transfer: *Transfer,\n    wait_for_interception: *bool,\n};\n\npub const ResponseData = struct {\n    data: []const u8,\n    transfer: *Transfer,\n};\n\npub const ResponseHeaderDone = struct {\n    transfer: *Transfer,\n};\n\npub const RequestDone = struct {\n    transfer: *Transfer,\n};\n\npub const RequestFail = struct {\n    transfer: *Transfer,\n    err: anyerror,\n};\n\npub fn init(allocator: Allocator) !*Notification {\n    const notification = try allocator.create(Notification);\n    errdefer allocator.destroy(notification);\n\n    notification.* = .{\n        .listeners = .{},\n        .event_listeners = .{},\n        .allocator = allocator,\n        .mem_pool = std.heap.MemoryPool(Listener).init(allocator),\n    };\n\n    return notification;\n}\n\npub fn deinit(self: *Notification) void {\n    const allocator = self.allocator;\n\n    var it = self.listeners.valueIterator();\n    while (it.next()) |listener| {\n        listener.deinit(allocator);\n    }\n    self.listeners.deinit(allocator);\n    self.mem_pool.deinit();\n    allocator.destroy(self);\n}\n\npub fn register(self: *Notification, comptime event: EventType, receiver: anytype, func: EventFunc(event)) !void {\n    var list = &@field(self.event_listeners, @tagName(event));\n\n    var listener = try self.mem_pool.create();\n    errdefer self.mem_pool.destroy(listener);\n\n    listener.* = .{\n        .node = .{},\n        .list = list,\n        .receiver = receiver,\n        .event = event,\n        .func = @ptrCast(func),\n        .struct_name = @typeName(@typeInfo(@TypeOf(receiver)).pointer.child),\n    };\n\n    const allocator = self.allocator;\n    const gop = try self.listeners.getOrPut(allocator, @intFromPtr(receiver));\n    if (gop.found_existing == false) {\n        gop.value_ptr.* = .{};\n    }\n    try gop.value_ptr.append(allocator, listener);\n\n    // we don't add this until we've successfully added the entry to\n    // self.listeners\n    list.append(&listener.node);\n}\n\npub fn unregister(self: *Notification, comptime event: EventType, receiver: anytype) void {\n    var listeners = self.listeners.getPtr(@intFromPtr(receiver)) orelse return;\n\n    var i: usize = 0;\n    while (i < listeners.items.len) {\n        const listener = listeners.items[i];\n        if (listener.event != event) {\n            i += 1;\n            continue;\n        }\n        listener.list.remove(&listener.node);\n        self.mem_pool.destroy(listener);\n        _ = listeners.swapRemove(i);\n    }\n\n    if (listeners.items.len == 0) {\n        listeners.deinit(self.allocator);\n        const removed = self.listeners.remove(@intFromPtr(receiver));\n        lp.assert(removed == true, \"Notification.unregister\", .{ .type = event });\n    }\n}\n\npub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {\n    var kv = self.listeners.fetchRemove(@intFromPtr(receiver)) orelse return;\n    for (kv.value.items) |listener| {\n        listener.list.remove(&listener.node);\n        self.mem_pool.destroy(listener);\n    }\n    kv.value.deinit(self.allocator);\n}\n\npub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {\n    if (self.listeners.count() == 0) {\n        return;\n    }\n    const list = &@field(self.event_listeners, @tagName(event));\n\n    var node = list.first;\n    while (node) |n| {\n        const listener: *Listener = @fieldParentPtr(\"node\", n);\n        const func: EventFunc(event) = @ptrCast(@alignCast(listener.func));\n        func(listener.receiver, data) catch |err| {\n            log.err(.app, \"dispatch error\", .{\n                .err = err,\n                .event = event,\n                .source = \"notification\",\n                .listener = listener.struct_name,\n            });\n        };\n        node = n.next;\n    }\n}\n\n// Given an event type enum, returns the type of arg the event emits\nfn ArgType(comptime event: Notification.EventType) type {\n    inline for (std.meta.fields(Notification.Events)) |f| {\n        if (std.mem.eql(u8, f.name, @tagName(event))) {\n            return f.type;\n        }\n    }\n    unreachable;\n}\n\n// Given an event type enum, returns the listening function type\nfn EventFunc(comptime event: Notification.EventType) type {\n    return *const fn (*anyopaque, ArgType(event)) anyerror!void;\n}\n\n// A listener. This is 1 receiver, with its function, and the linked list\n// node that goes in the appropriate EventListeners list.\nconst Listener = struct {\n    // the receiver of the event, i.e. the self parameter to `func`\n    receiver: *anyopaque,\n\n    // the function to call\n    func: *const anyopaque,\n\n    // For logging slightly better error\n    struct_name: []const u8,\n\n    event: Notification.EventType,\n\n    // intrusive linked list node\n    node: List.Node,\n\n    // The event list this listener belongs to.\n    // We need this in order to be able to remove the node from the list\n    list: *List,\n};\n\nconst testing = std.testing;\ntest \"Notification\" {\n    var notifier = try Notification.init(testing.allocator);\n    defer notifier.deinit();\n\n    // noop\n    notifier.dispatch(.page_navigate, &.{\n        .frame_id = 0,\n        .req_id = 1,\n        .timestamp = 4,\n        .url = undefined,\n        .opts = .{},\n    });\n\n    var tc = TestClient{};\n\n    try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);\n    notifier.dispatch(.page_navigate, &.{\n        .frame_id = 0,\n        .req_id = 1,\n        .timestamp = 4,\n        .url = undefined,\n        .opts = .{},\n    });\n    try testing.expectEqual(4, tc.page_navigate);\n\n    notifier.unregisterAll(&tc);\n    notifier.dispatch(.page_navigate, &.{\n        .frame_id = 0,\n        .req_id = 1,\n        .timestamp = 10,\n        .url = undefined,\n        .opts = .{},\n    });\n    try testing.expectEqual(4, tc.page_navigate);\n\n    try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);\n    try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);\n    notifier.dispatch(.page_navigate, &.{\n        .frame_id = 0,\n        .req_id = 1,\n        .timestamp = 10,\n        .url = undefined,\n        .opts = .{},\n    });\n    notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });\n    try testing.expectEqual(14, tc.page_navigate);\n    try testing.expectEqual(6, tc.page_navigated);\n\n    notifier.unregisterAll(&tc);\n    notifier.dispatch(.page_navigate, &.{\n        .frame_id = 0,\n        .req_id = 1,\n        .timestamp = 100,\n        .url = undefined,\n        .opts = .{},\n    });\n    notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });\n    try testing.expectEqual(14, tc.page_navigate);\n    try testing.expectEqual(6, tc.page_navigated);\n\n    {\n        // unregister\n        try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);\n        try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);\n        notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });\n        notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });\n        try testing.expectEqual(114, tc.page_navigate);\n        try testing.expectEqual(1006, tc.page_navigated);\n\n        notifier.unregister(.page_navigate, &tc);\n        notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });\n        notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });\n        try testing.expectEqual(114, tc.page_navigate);\n        try testing.expectEqual(2006, tc.page_navigated);\n\n        notifier.unregister(.page_navigated, &tc);\n        notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });\n        notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });\n        try testing.expectEqual(114, tc.page_navigate);\n        try testing.expectEqual(2006, tc.page_navigated);\n\n        // already unregistered, try anyways\n        notifier.unregister(.page_navigated, &tc);\n        notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });\n        notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });\n        try testing.expectEqual(114, tc.page_navigate);\n        try testing.expectEqual(2006, tc.page_navigated);\n    }\n}\n\nconst TestClient = struct {\n    page_navigate: u64 = 0,\n    page_navigated: u64 = 0,\n\n    fn pageNavigate(ptr: *anyopaque, data: *const Notification.PageNavigate) !void {\n        const self: *TestClient = @ptrCast(@alignCast(ptr));\n        self.page_navigate += data.timestamp;\n    }\n\n    fn pageNavigated(ptr: *anyopaque, data: *const Notification.PageNavigated) !void {\n        const self: *TestClient = @ptrCast(@alignCast(ptr));\n        self.page_navigated += data.timestamp;\n    }\n};\n"
  },
  {
    "path": "src/SemanticTree.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  See <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"log.zig\");\nconst isAllWhitespace = @import(\"string.zig\").isAllWhitespace;\nconst Page = lp.Page;\nconst interactive = @import(\"browser/interactive.zig\");\n\nconst CData = @import(\"browser/webapi/CData.zig\");\nconst Element = @import(\"browser/webapi/Element.zig\");\nconst Node = @import(\"browser/webapi/Node.zig\");\nconst AXNode = @import(\"cdp/AXNode.zig\");\nconst CDPNode = @import(\"cdp/Node.zig\");\n\nconst Self = @This();\n\ndom_node: *Node,\nregistry: *CDPNode.Registry,\npage: *Page,\narena: std.mem.Allocator,\nprune: bool = true,\ninteractive_only: bool = false,\nmax_depth: u32 = std.math.maxInt(u32) - 1,\n\npub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {\n    var visitor = JsonVisitor{ .jw = jw, .tree = self };\n    var xpath_buffer: std.ArrayList(u8) = .{};\n    const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {\n        log.err(.app, \"listener map failed\", .{ .err = err });\n        return error.WriteFailed;\n    };\n    self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, 0) catch |err| {\n        log.err(.app, \"semantic tree json dump failed\", .{ .err = err });\n        return error.WriteFailed;\n    };\n}\n\npub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void {\n    var visitor = TextVisitor{ .writer = writer, .tree = self, .depth = 0 };\n    var xpath_buffer: std.ArrayList(u8) = .empty;\n    const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {\n        log.err(.app, \"listener map failed\", .{ .err = err });\n        return error.WriteFailed;\n    };\n    self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, 0) catch |err| {\n        log.err(.app, \"semantic tree text dump failed\", .{ .err = err });\n        return error.WriteFailed;\n    };\n}\n\nconst OptionData = struct {\n    value: []const u8,\n    text: []const u8,\n    selected: bool,\n};\n\nconst NodeData = struct {\n    id: CDPNode.Id,\n    axn: AXNode,\n    role: []const u8,\n    name: ?[]const u8,\n    value: ?[]const u8,\n    options: ?[]OptionData = null,\n    xpath: []const u8,\n    is_interactive: bool,\n    node_name: []const u8,\n};\n\nfn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, current_depth: u32) !void {\n    if (current_depth > self.max_depth) return;\n\n    // 1. Skip non-content nodes\n    if (node.is(Element)) |el| {\n        const tag = el.getTag();\n        if (tag.isMetadata() or tag == .svg) return;\n\n        // We handle options/optgroups natively inside their parents, skip them in the general walk\n        if (tag == .datalist or tag == .option or tag == .optgroup) return;\n\n        // Check visibility using the engine's checkVisibility which handles CSS display: none\n        if (!el.checkVisibility(self.page)) {\n            return;\n        }\n\n        if (el.is(Element.Html)) |html_el| {\n            if (html_el.getHidden()) return;\n        }\n    } else if (node.is(CData.Text)) |text_node| {\n        const text = text_node.getWholeText();\n        if (isAllWhitespace(text)) {\n            return;\n        }\n    } else if (node._type != .document and node._type != .document_fragment) {\n        return;\n    }\n\n    const cdp_node = try self.registry.register(node);\n    const axn = AXNode.fromNode(node);\n    const role = try axn.getRole();\n\n    var is_interactive = false;\n    var value: ?[]const u8 = null;\n    var options: ?[]OptionData = null;\n    var node_name: []const u8 = \"text\";\n\n    if (node.is(Element)) |el| {\n        node_name = el.getTagNameLower();\n\n        if (el.is(Element.Html.Input)) |input| {\n            value = input.getValue();\n            if (el.getAttributeSafe(comptime lp.String.wrap(\"list\"))) |list_id| {\n                options = try extractDataListOptions(list_id, self.page, self.arena);\n            }\n        } else if (el.is(Element.Html.TextArea)) |textarea| {\n            value = textarea.getValue();\n        } else if (el.is(Element.Html.Select)) |select| {\n            value = select.getValue(self.page);\n            options = try extractSelectOptions(el.asNode(), self.page, self.arena);\n        }\n\n        if (el.is(Element.Html)) |html_el| {\n            if (interactive.classifyInteractivity(el, html_el, listener_targets) != null) {\n                is_interactive = true;\n            }\n        }\n    } else if (node._type == .document or node._type == .document_fragment) {\n        node_name = \"root\";\n    }\n\n    const initial_xpath_len = xpath_buffer.items.len;\n    try appendXPathSegment(node, xpath_buffer.writer(self.arena), index);\n    const xpath = xpath_buffer.items;\n\n    var name = try axn.getName(self.page, self.arena);\n\n    const has_explicit_label = if (node.is(Element)) |el|\n        el.getAttributeSafe(.wrap(\"aria-label\")) != null or el.getAttributeSafe(.wrap(\"title\")) != null\n    else\n        false;\n\n    const structural = isStructuralRole(role);\n\n    // Filter out computed concatenated names for generic containers without explicit labels.\n    // This prevents token bloat and ensures their StaticText children aren't incorrectly pruned.\n    // We ignore interactivity because a generic wrapper with an event listener still shouldn't hoist all text.\n    if (name != null and structural and !has_explicit_label) {\n        name = null;\n    }\n\n    var data = NodeData{\n        .id = cdp_node.id,\n        .axn = axn,\n        .role = role,\n        .name = name,\n        .value = value,\n        .options = options,\n        .xpath = xpath,\n        .is_interactive = is_interactive,\n        .node_name = node_name,\n    };\n\n    var should_visit = true;\n    if (self.interactive_only) {\n        var keep = false;\n        if (interactive.isInteractiveRole(role)) {\n            keep = true;\n        } else if (interactive.isContentRole(role)) {\n            if (name != null and name.?.len > 0) {\n                keep = true;\n            }\n        } else if (std.mem.eql(u8, role, \"RootWebArea\")) {\n            keep = true;\n        } else if (is_interactive) {\n            keep = true;\n        }\n        if (!keep) {\n            should_visit = false;\n        }\n    } else if (self.prune) {\n        if (structural and !is_interactive and !has_explicit_label) {\n            should_visit = false;\n        }\n\n        if (std.mem.eql(u8, role, \"StaticText\") and node._parent != null) {\n            if (parent_name != null and name != null and std.mem.indexOf(u8, parent_name.?, name.?) != null) {\n                should_visit = false;\n            }\n        }\n    }\n\n    var did_visit = false;\n    var should_walk_children = true;\n    if (should_visit) {\n        should_walk_children = try visitor.visit(node, &data);\n        did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures\n    } else {\n        // If we skip the node, we must NOT tell the visitor to close it later\n        did_visit = false;\n    }\n\n    if (should_walk_children) {\n        // If we are printing this node normally OR skipping it and unrolling its children,\n        // we walk the children iterator.\n        var it = node.childrenIterator();\n        var tag_counts = std.StringArrayHashMap(usize).init(self.arena);\n        while (it.next()) |child| {\n            var tag: []const u8 = \"text()\";\n            if (child.is(Element)) |el| {\n                tag = el.getTagNameLower();\n            }\n\n            const gop = try tag_counts.getOrPut(tag);\n            if (!gop.found_existing) {\n                gop.value_ptr.* = 0;\n            }\n            gop.value_ptr.* += 1;\n\n            try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, current_depth + 1);\n        }\n    }\n\n    if (did_visit) {\n        try visitor.leave();\n    }\n\n    xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);\n}\n\nfn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {\n    var options = std.ArrayListUnmanaged(OptionData){};\n    var it = node.childrenIterator();\n    while (it.next()) |child| {\n        if (child.is(Element)) |el| {\n            if (el.getTag() == .option) {\n                if (el.is(Element.Html.Option)) |opt| {\n                    const text = opt.getText(page);\n                    const value = opt.getValue(page);\n                    const selected = opt.getSelected();\n                    try options.append(arena, .{ .text = text, .value = value, .selected = selected });\n                }\n            } else if (el.getTag() == .optgroup) {\n                var group_it = child.childrenIterator();\n                while (group_it.next()) |group_child| {\n                    if (group_child.is(Element.Html.Option)) |opt| {\n                        const text = opt.getText(page);\n                        const value = opt.getValue(page);\n                        const selected = opt.getSelected();\n                        try options.append(arena, .{ .text = text, .value = value, .selected = selected });\n                    }\n                }\n            }\n        }\n    }\n    return options.toOwnedSlice(arena);\n}\n\nfn extractDataListOptions(list_id: []const u8, page: *Page, arena: std.mem.Allocator) !?[]OptionData {\n    if (page.document.getElementById(list_id, page)) |referenced_el| {\n        if (referenced_el.getTag() == .datalist) {\n            return try extractSelectOptions(referenced_el.asNode(), page, arena);\n        }\n    }\n    return null;\n}\n\nfn appendXPathSegment(node: *Node, writer: anytype, index: usize) !void {\n    if (node.is(Element)) |el| {\n        const tag = el.getTagNameLower();\n        try std.fmt.format(writer, \"/{s}[{d}]\", .{ tag, index });\n    } else if (node.is(CData.Text)) |_| {\n        try std.fmt.format(writer, \"/text()[{d}]\", .{index});\n    }\n}\n\nconst JsonVisitor = struct {\n    jw: *std.json.Stringify,\n    tree: Self,\n\n    pub fn visit(self: *JsonVisitor, node: *Node, data: *NodeData) !bool {\n        try self.jw.beginObject();\n\n        try self.jw.objectField(\"nodeId\");\n        try self.jw.write(try std.fmt.allocPrint(self.tree.arena, \"{d}\", .{data.id}));\n\n        try self.jw.objectField(\"backendDOMNodeId\");\n        try self.jw.write(data.id);\n\n        try self.jw.objectField(\"nodeName\");\n        try self.jw.write(data.node_name);\n\n        try self.jw.objectField(\"xpath\");\n        try self.jw.write(data.xpath);\n\n        if (node.is(Element)) |el| {\n            try self.jw.objectField(\"nodeType\");\n            try self.jw.write(1);\n\n            try self.jw.objectField(\"isInteractive\");\n            try self.jw.write(data.is_interactive);\n\n            try self.jw.objectField(\"role\");\n            try self.jw.write(data.role);\n\n            if (data.name) |name| {\n                if (name.len > 0) {\n                    try self.jw.objectField(\"name\");\n                    try self.jw.write(name);\n                }\n            }\n\n            if (data.value) |value| {\n                try self.jw.objectField(\"value\");\n                try self.jw.write(value);\n            }\n\n            if (el._attributes) |attrs| {\n                try self.jw.objectField(\"attributes\");\n                try self.jw.beginObject();\n                var iter = attrs.iterator();\n                while (iter.next()) |attr| {\n                    try self.jw.objectField(attr._name.str());\n                    try self.jw.write(attr._value.str());\n                }\n                try self.jw.endObject();\n            }\n\n            if (data.options) |options| {\n                try self.jw.objectField(\"options\");\n                try self.jw.beginArray();\n                for (options) |opt| {\n                    try self.jw.beginObject();\n                    try self.jw.objectField(\"value\");\n                    try self.jw.write(opt.value);\n                    try self.jw.objectField(\"text\");\n                    try self.jw.write(opt.text);\n                    try self.jw.objectField(\"selected\");\n                    try self.jw.write(opt.selected);\n                    try self.jw.endObject();\n                }\n                try self.jw.endArray();\n            }\n        } else if (node.is(CData.Text)) |text_node| {\n            try self.jw.objectField(\"nodeType\");\n            try self.jw.write(3);\n            try self.jw.objectField(\"nodeValue\");\n            try self.jw.write(text_node.getWholeText());\n        } else {\n            try self.jw.objectField(\"nodeType\");\n            try self.jw.write(9);\n        }\n\n        try self.jw.objectField(\"children\");\n        try self.jw.beginArray();\n\n        if (data.options != null) {\n            // Signal to not walk children, as we handled them natively\n            return false;\n        }\n\n        return true;\n    }\n\n    pub fn leave(self: *JsonVisitor) !void {\n        try self.jw.endArray();\n        try self.jw.endObject();\n    }\n};\n\nfn isStructuralRole(role: []const u8) bool {\n    const structural_roles = std.StaticStringMap(void).initComptime(.{\n        .{ \"none\", {} },\n        .{ \"generic\", {} },\n        .{ \"InlineTextBox\", {} },\n        .{ \"banner\", {} },\n        .{ \"navigation\", {} },\n        .{ \"main\", {} },\n        .{ \"list\", {} },\n        .{ \"listitem\", {} },\n        .{ \"table\", {} },\n        .{ \"rowgroup\", {} },\n        .{ \"row\", {} },\n        .{ \"cell\", {} },\n        .{ \"region\", {} },\n    });\n    return structural_roles.has(role);\n}\n\nconst TextVisitor = struct {\n    writer: *std.Io.Writer,\n    tree: Self,\n    depth: usize,\n\n    pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {\n        for (0..self.depth) |_| {\n            try self.writer.writeByte(' ');\n        }\n\n        var name_to_print: ?[]const u8 = null;\n        if (data.name) |n| {\n            if (n.len > 0) {\n                name_to_print = n;\n            }\n        } else if (node.is(CData.Text)) |text_node| {\n            const trimmed = std.mem.trim(u8, text_node.getWholeText(), \" \\t\\r\\n\");\n            if (trimmed.len > 0) {\n                name_to_print = trimmed;\n            }\n        }\n\n        const is_text_only = std.mem.eql(u8, data.role, \"StaticText\") or std.mem.eql(u8, data.role, \"none\") or std.mem.eql(u8, data.role, \"generic\");\n\n        try self.writer.print(\"{d}\", .{data.id});\n        if (!is_text_only) {\n            try self.writer.print(\" {s}\", .{data.role});\n        }\n        if (name_to_print) |n| {\n            try self.writer.print(\" '{s}'\", .{n});\n        }\n\n        if (data.value) |v| {\n            if (v.len > 0) {\n                try self.writer.print(\" value='{s}'\", .{v});\n            }\n        }\n\n        if (data.options) |options| {\n            try self.writer.writeAll(\" options=[\");\n            for (options, 0..) |opt, i| {\n                if (i > 0) try self.writer.writeAll(\",\");\n                try self.writer.print(\"'{s}'\", .{opt.value});\n                if (opt.selected) {\n                    try self.writer.writeAll(\"*\");\n                }\n            }\n            try self.writer.writeAll(\"]\\n\");\n            self.depth += 1;\n            return false; // Native handling complete, do not walk children\n        }\n\n        try self.writer.writeByte('\\n');\n        self.depth += 1;\n\n        // If this is a leaf-like semantic node and we already have a name,\n        // skip children to avoid redundant StaticText or noise.\n        const is_leaf_semantic = std.mem.eql(u8, data.role, \"link\") or\n            std.mem.eql(u8, data.role, \"button\") or\n            std.mem.eql(u8, data.role, \"heading\") or\n            std.mem.eql(u8, data.role, \"code\");\n        if (is_leaf_semantic and data.name != null and data.name.?.len > 0) {\n            return false;\n        }\n\n        return true;\n    }\n\n    pub fn leave(self: *TextVisitor) !void {\n        if (self.depth > 0) {\n            self.depth -= 1;\n        }\n    }\n};\n\nconst testing = @import(\"testing.zig\");\n\ntest \"SemanticTree backendDOMNodeId\" {\n    var registry: CDPNode.Registry = .init(testing.allocator);\n    defer registry.deinit();\n\n    var page = try testing.pageTest(\"cdp/registry1.html\");\n    defer testing.reset();\n    defer page._session.removePage();\n\n    const st: Self = .{\n        .dom_node = page.window._document.asNode(),\n        .registry = &registry,\n        .page = page,\n        .arena = testing.arena_allocator,\n        .prune = false,\n        .interactive_only = false,\n        .max_depth = std.math.maxInt(u32) - 1,\n    };\n\n    const json_str = try std.json.Stringify.valueAlloc(testing.allocator, st, .{});\n    defer testing.allocator.free(json_str);\n\n    try testing.expect(std.mem.indexOf(u8, json_str, \"\\\"backendDOMNodeId\\\":\") != null);\n}\n\ntest \"SemanticTree max_depth\" {\n    var registry: CDPNode.Registry = .init(testing.allocator);\n    defer registry.deinit();\n\n    var page = try testing.pageTest(\"cdp/registry1.html\");\n    defer testing.reset();\n    defer page._session.removePage();\n\n    const st: Self = .{\n        .dom_node = page.window._document.asNode(),\n        .registry = &registry,\n        .page = page,\n        .arena = testing.arena_allocator,\n        .prune = false,\n        .interactive_only = false,\n        .max_depth = 1,\n    };\n\n    var aw: std.Io.Writer.Allocating = .init(testing.allocator);\n    defer aw.deinit();\n\n    try st.textStringify(&aw.writer);\n    const text_str = aw.written();\n\n    try testing.expect(std.mem.indexOf(u8, text_str, \"other\") == null);\n}\n"
  },
  {
    "path": "src/Server.zig",
    "content": "// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst net = std.net;\nconst posix = std.posix;\n\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst log = @import(\"log.zig\");\nconst App = @import(\"App.zig\");\nconst Config = @import(\"Config.zig\");\nconst CDP = @import(\"cdp/cdp.zig\").CDP;\nconst Net = @import(\"network/websocket.zig\");\nconst HttpClient = @import(\"browser/HttpClient.zig\");\n\nconst Server = @This();\n\napp: *App,\nallocator: Allocator,\njson_version_response: []const u8,\n\n// Thread management\nactive_threads: std.atomic.Value(u32) = .init(0),\nclients: std.ArrayList(*Client) = .{},\nclient_mutex: std.Thread.Mutex = .{},\nclients_pool: std.heap.MemoryPool(Client),\n\npub fn init(app: *App, address: net.Address) !*Server {\n    const allocator = app.allocator;\n    const json_version_response = try buildJSONVersionResponse(allocator, address);\n    errdefer allocator.free(json_version_response);\n\n    const self = try allocator.create(Server);\n    errdefer allocator.destroy(self);\n\n    self.* = .{\n        .app = app,\n        .allocator = allocator,\n        .json_version_response = json_version_response,\n        .clients_pool = std.heap.MemoryPool(Client).init(allocator),\n    };\n\n    try self.app.network.bind(address, self, onAccept);\n    log.info(.app, \"server running\", .{ .address = address });\n\n    return self;\n}\n\npub fn shutdown(self: *Server) void {\n    self.client_mutex.lock();\n    defer self.client_mutex.unlock();\n\n    for (self.clients.items) |client| {\n        client.stop();\n    }\n}\n\npub fn deinit(self: *Server) void {\n    self.shutdown();\n    self.joinThreads();\n    self.clients.deinit(self.allocator);\n    self.clients_pool.deinit();\n    self.allocator.free(self.json_version_response);\n    self.allocator.destroy(self);\n}\n\nfn onAccept(ctx: *anyopaque, socket: posix.socket_t) void {\n    const self: *Server = @ptrCast(@alignCast(ctx));\n    const timeout_ms: u32 = @intCast(self.app.config.cdpTimeout());\n    self.spawnWorker(socket, timeout_ms) catch |err| {\n        log.err(.app, \"CDP spawn\", .{ .err = err });\n        posix.close(socket);\n    };\n}\n\nfn handleConnection(self: *Server, socket: posix.socket_t, timeout_ms: u32) void {\n    defer posix.close(socket);\n\n    // Client is HUGE (> 512KB) because it has a large read buffer.\n    // V8 crashes if this is on the stack (likely related to its size).\n    const client = self.getClient() catch |err| {\n        log.err(.app, \"CDP client create\", .{ .err = err });\n        return;\n    };\n    defer self.releaseClient(client);\n\n    client.* = Client.init(\n        socket,\n        self.allocator,\n        self.app,\n        self.json_version_response,\n        timeout_ms,\n    ) catch |err| {\n        log.err(.app, \"CDP client init\", .{ .err = err });\n        return;\n    };\n    defer client.deinit();\n\n    self.registerClient(client);\n    defer self.unregisterClient(client);\n\n    // Check shutdown after registering to avoid missing the stop signal.\n    // If deinit() already iterated over clients, this client won't receive stop()\n    // and would block joinThreads() indefinitely.\n    if (self.app.shutdown()) {\n        return;\n    }\n\n    client.start();\n}\n\nfn getClient(self: *Server) !*Client {\n    self.client_mutex.lock();\n    defer self.client_mutex.unlock();\n    return self.clients_pool.create();\n}\n\nfn releaseClient(self: *Server, client: *Client) void {\n    self.client_mutex.lock();\n    defer self.client_mutex.unlock();\n    self.clients_pool.destroy(client);\n}\n\nfn registerClient(self: *Server, client: *Client) void {\n    self.client_mutex.lock();\n    defer self.client_mutex.unlock();\n    self.clients.append(self.allocator, client) catch {};\n}\n\nfn unregisterClient(self: *Server, client: *Client) void {\n    self.client_mutex.lock();\n    defer self.client_mutex.unlock();\n    for (self.clients.items, 0..) |c, i| {\n        if (c == client) {\n            _ = self.clients.swapRemove(i);\n            break;\n        }\n    }\n}\n\nfn spawnWorker(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {\n    if (self.app.shutdown()) {\n        return error.ShuttingDown;\n    }\n\n    // Atomically increment active_threads only if below max_connections.\n    // Uses CAS loop to avoid race between checking the limit and incrementing.\n    //\n    // cmpxchgWeak may fail for two reasons:\n    // 1. Another thread changed the value (increment or decrement)\n    // 2. Spurious failure on some architectures (e.g. ARM)\n    //\n    // We use Weak instead of Strong because we need a retry loop anyway:\n    // if CAS fails because a thread finished (counter decreased), we should\n    // retry rather than return an error - there may now be room for a new connection.\n    //\n    // On failure, cmpxchgWeak returns the actual value, which we reuse to avoid\n    // an extra load on the next iteration.\n    const max_connections = self.app.config.maxConnections();\n    var current = self.active_threads.load(.monotonic);\n    while (current < max_connections) {\n        current = self.active_threads.cmpxchgWeak(current, current + 1, .monotonic, .monotonic) orelse break;\n    } else {\n        return error.MaxThreadsReached;\n    }\n    errdefer _ = self.active_threads.fetchSub(1, .monotonic);\n\n    const thread = try std.Thread.spawn(.{}, runWorker, .{ self, socket, timeout_ms });\n    thread.detach();\n}\n\nfn runWorker(self: *Server, socket: posix.socket_t, timeout_ms: u32) void {\n    defer _ = self.active_threads.fetchSub(1, .monotonic);\n    handleConnection(self, socket, timeout_ms);\n}\n\nfn joinThreads(self: *Server) void {\n    while (self.active_threads.load(.monotonic) > 0) {\n        std.Thread.sleep(10 * std.time.ns_per_ms);\n    }\n}\n\n// Handle exactly one TCP connection.\npub const Client = struct {\n    // The client is initially serving HTTP requests but, under normal circumstances\n    // should eventually be upgraded to a websocket connections\n    mode: union(enum) {\n        http: void,\n        cdp: CDP,\n    },\n\n    allocator: Allocator,\n    app: *App,\n    http: *HttpClient,\n    ws: Net.WsConnection,\n\n    fn init(\n        socket: posix.socket_t,\n        allocator: Allocator,\n        app: *App,\n        json_version_response: []const u8,\n        timeout_ms: u32,\n    ) !Client {\n        var ws = try Net.WsConnection.init(socket, allocator, json_version_response, timeout_ms);\n        errdefer ws.deinit();\n\n        if (log.enabled(.app, .info)) {\n            const client_address = ws.getAddress() catch null;\n            log.info(.app, \"client connected\", .{ .ip = client_address });\n        }\n\n        const http = try HttpClient.init(allocator, &app.network);\n        errdefer http.deinit();\n\n        return .{\n            .allocator = allocator,\n            .app = app,\n            .http = http,\n            .ws = ws,\n            .mode = .{ .http = {} },\n        };\n    }\n\n    fn stop(self: *Client) void {\n        switch (self.mode) {\n            .http => {},\n            .cdp => |*cdp| {\n                cdp.browser.env.terminate();\n                self.ws.sendClose();\n            },\n        }\n        self.ws.shutdown();\n    }\n\n    fn deinit(self: *Client) void {\n        switch (self.mode) {\n            .cdp => |*cdp| cdp.deinit(),\n            .http => {},\n        }\n        self.ws.deinit();\n        self.http.deinit();\n    }\n\n    fn start(self: *Client) void {\n        const http = self.http;\n        http.cdp_client = .{\n            .socket = self.ws.socket,\n            .ctx = self,\n            .blocking_read_start = Client.blockingReadStart,\n            .blocking_read = Client.blockingRead,\n            .blocking_read_end = Client.blockingReadStop,\n        };\n        defer http.cdp_client = null;\n\n        self.httpLoop(http) catch |err| {\n            log.err(.app, \"CDP client loop\", .{ .err = err });\n        };\n    }\n\n    fn httpLoop(self: *Client, http: *HttpClient) !void {\n        lp.assert(self.mode == .http, \"Client.httpLoop invalid mode\", .{});\n\n        while (true) {\n            const status = http.tick(self.ws.timeout_ms) catch |err| {\n                log.err(.app, \"http tick\", .{ .err = err });\n                return;\n            };\n            if (status != .cdp_socket) {\n                log.info(.app, \"CDP timeout\", .{});\n                return;\n            }\n\n            if (self.readSocket() == false) {\n                return;\n            }\n\n            if (self.mode == .cdp) {\n                break;\n            }\n        }\n\n        var cdp = &self.mode.cdp;\n        var last_message = milliTimestamp(.monotonic);\n        var ms_remaining = self.ws.timeout_ms;\n\n        while (true) {\n            switch (cdp.pageWait(ms_remaining)) {\n                .cdp_socket => {\n                    if (self.readSocket() == false) {\n                        return;\n                    }\n                    last_message = milliTimestamp(.monotonic);\n                    ms_remaining = self.ws.timeout_ms;\n                },\n                .no_page => {\n                    const status = http.tick(ms_remaining) catch |err| {\n                        log.err(.app, \"http tick\", .{ .err = err });\n                        return;\n                    };\n                    if (status != .cdp_socket) {\n                        log.info(.app, \"CDP timeout\", .{});\n                        return;\n                    }\n                    if (self.readSocket() == false) {\n                        return;\n                    }\n                    last_message = milliTimestamp(.monotonic);\n                    ms_remaining = self.ws.timeout_ms;\n                },\n                .done => {\n                    const now = milliTimestamp(.monotonic);\n                    const elapsed = now - last_message;\n                    if (elapsed >= ms_remaining) {\n                        log.info(.app, \"CDP timeout\", .{});\n                        return;\n                    }\n                    ms_remaining -= @intCast(elapsed);\n                    last_message = now;\n                },\n            }\n        }\n    }\n\n    fn blockingReadStart(ctx: *anyopaque) bool {\n        const self: *Client = @ptrCast(@alignCast(ctx));\n        self.ws.setBlocking(true) catch |err| {\n            log.warn(.app, \"CDP blockingReadStart\", .{ .err = err });\n            return false;\n        };\n        return true;\n    }\n\n    fn blockingRead(ctx: *anyopaque) bool {\n        const self: *Client = @ptrCast(@alignCast(ctx));\n        return self.readSocket();\n    }\n\n    fn blockingReadStop(ctx: *anyopaque) bool {\n        const self: *Client = @ptrCast(@alignCast(ctx));\n        self.ws.setBlocking(false) catch |err| {\n            log.warn(.app, \"CDP blockingReadStop\", .{ .err = err });\n            return false;\n        };\n        return true;\n    }\n\n    fn readSocket(self: *Client) bool {\n        const n = self.ws.read() catch |err| {\n            log.warn(.app, \"CDP read\", .{ .err = err });\n            return false;\n        };\n\n        if (n == 0) {\n            log.info(.app, \"CDP disconnect\", .{});\n            return false;\n        }\n\n        return self.processData() catch false;\n    }\n\n    fn processData(self: *Client) !bool {\n        switch (self.mode) {\n            .cdp => |*cdp| return self.processWebsocketMessage(cdp),\n            .http => return self.processHTTPRequest(),\n        }\n    }\n\n    fn processHTTPRequest(self: *Client) !bool {\n        lp.assert(self.ws.reader.pos == 0, \"Client.HTTP pos\", .{ .pos = self.ws.reader.pos });\n        const request = self.ws.reader.buf[0..self.ws.reader.len];\n\n        if (request.len > Config.CDP_MAX_HTTP_REQUEST_SIZE) {\n            self.writeHTTPErrorResponse(413, \"Request too large\");\n            return error.RequestTooLarge;\n        }\n\n        // we're only expecting [body-less] GET requests.\n        if (std.mem.endsWith(u8, request, \"\\r\\n\\r\\n\") == false) {\n            // we need more data, put any more data here\n            return true;\n        }\n\n        // the next incoming data can go to the front of our buffer\n        defer self.ws.reader.len = 0;\n        return self.handleHTTPRequest(request) catch |err| {\n            switch (err) {\n                error.NotFound => self.writeHTTPErrorResponse(404, \"Not found\"),\n                error.InvalidRequest => self.writeHTTPErrorResponse(400, \"Invalid request\"),\n                error.InvalidProtocol => self.writeHTTPErrorResponse(400, \"Invalid HTTP protocol\"),\n                error.MissingHeaders => self.writeHTTPErrorResponse(400, \"Missing required header\"),\n                error.InvalidUpgradeHeader => self.writeHTTPErrorResponse(400, \"Unsupported upgrade type\"),\n                error.InvalidVersionHeader => self.writeHTTPErrorResponse(400, \"Invalid websocket version\"),\n                error.InvalidConnectionHeader => self.writeHTTPErrorResponse(400, \"Invalid connection header\"),\n                else => {\n                    log.err(.app, \"server 500\", .{ .err = err, .req = request[0..@min(100, request.len)] });\n                    self.writeHTTPErrorResponse(500, \"Internal Server Error\");\n                },\n            }\n            return err;\n        };\n    }\n\n    fn handleHTTPRequest(self: *Client, request: []u8) !bool {\n        if (request.len < 18) {\n            // 18 is [generously] the smallest acceptable HTTP request\n            return error.InvalidRequest;\n        }\n\n        if (std.mem.eql(u8, request[0..4], \"GET \") == false) {\n            return error.NotFound;\n        }\n\n        const url_end = std.mem.indexOfScalarPos(u8, request, 4, ' ') orelse {\n            return error.InvalidRequest;\n        };\n\n        const url = request[4..url_end];\n\n        if (std.mem.eql(u8, url, \"/\")) {\n            try self.upgradeConnection(request);\n            return true;\n        }\n\n        if (std.mem.eql(u8, url, \"/json/version\") or std.mem.eql(u8, url, \"/json/version/\")) {\n            try self.ws.send(self.ws.json_version_response);\n            // Chromedp (a Go driver) does an http request to /json/version\n            // then to / (websocket upgrade) using a different connection.\n            // Since we only allow 1 connection at a time, the 2nd one (the\n            // websocket upgrade) blocks until the first one times out.\n            // We can avoid that by closing the connection. json_version_response\n            // has a Connection: Close header too.\n            self.ws.shutdown();\n            return false;\n        }\n\n        return error.NotFound;\n    }\n\n    fn upgradeConnection(self: *Client, request: []u8) !void {\n        try self.ws.upgrade(request);\n        self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) };\n    }\n\n    fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {\n        self.ws.sendHttpError(status, body);\n    }\n\n    fn processWebsocketMessage(self: *Client, cdp: *CDP) !bool {\n        return self.ws.processMessages(cdp);\n    }\n\n    pub fn sendAllocator(self: *Client) Allocator {\n        return self.ws.send_arena.allocator();\n    }\n\n    pub fn sendJSON(self: *Client, message: anytype, opts: std.json.Stringify.Options) !void {\n        return self.ws.sendJSON(message, opts);\n    }\n\n    pub fn sendJSONRaw(self: *Client, buf: std.ArrayList(u8)) !void {\n        return self.ws.sendJSONRaw(buf);\n    }\n};\n\n// Utils\n// --------\n\nfn buildJSONVersionResponse(\n    allocator: Allocator,\n    address: net.Address,\n) ![]const u8 {\n    const body_format = \"{{\\\"webSocketDebuggerUrl\\\": \\\"ws://{f}/\\\"}}\";\n    const body_len = std.fmt.count(body_format, .{address});\n\n    // We send a Connection: Close (and actually close the connection)\n    // because chromedp (Go driver) sends a request to /json/version and then\n    // does an upgrade request, on a different connection. Since we only allow\n    // 1 connection at a time, the upgrade connection doesn't proceed until we\n    // timeout the /json/version. So, instead of waiting for that, we just\n    // always close HTTP requests.\n    const response_format =\n        \"HTTP/1.1 200 OK\\r\\n\" ++\n        \"Content-Length: {d}\\r\\n\" ++\n        \"Connection: Close\\r\\n\" ++\n        \"Content-Type: application/json; charset=UTF-8\\r\\n\\r\\n\" ++\n        body_format;\n    return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address });\n}\n\npub const timestamp = @import(\"datetime.zig\").timestamp;\npub const milliTimestamp = @import(\"datetime.zig\").milliTimestamp;\n\nconst testing = std.testing;\ntest \"server: buildJSONVersionResponse\" {\n    const address = try net.Address.parseIp4(\"127.0.0.1\", 9001);\n    const res = try buildJSONVersionResponse(testing.allocator, address);\n    defer testing.allocator.free(res);\n\n    try testing.expectEqualStrings(\"HTTP/1.1 200 OK\\r\\n\" ++\n        \"Content-Length: 48\\r\\n\" ++\n        \"Connection: Close\\r\\n\" ++\n        \"Content-Type: application/json; charset=UTF-8\\r\\n\\r\\n\" ++\n        \"{\\\"webSocketDebuggerUrl\\\": \\\"ws://127.0.0.1:9001/\\\"}\", res);\n}\n\ntest \"Client: http invalid request\" {\n    var c = try createTestClient();\n    defer c.deinit();\n\n    const res = try c.httpRequest(\"GET /over/9000 HTTP/1.1\\r\\n\" ++ \"Header: \" ++ (\"a\" ** 4100) ++ \"\\r\\n\\r\\n\");\n    try testing.expectEqualStrings(\"HTTP/1.1 413 \\r\\n\" ++\n        \"Connection: Close\\r\\n\" ++\n        \"Content-Length: 17\\r\\n\\r\\n\" ++\n        \"Request too large\", res);\n}\n\ntest \"Client: http invalid handshake\" {\n    try assertHTTPError(\n        400,\n        \"Invalid request\",\n        \"\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        404,\n        \"Not found\",\n        \"GET /over/9000 HTTP/1.1\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        404,\n        \"Not found\",\n        \"POST / HTTP/1.1\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        400,\n        \"Invalid HTTP protocol\",\n        \"GET / HTTP/1.0\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        400,\n        \"Missing required header\",\n        \"GET / HTTP/1.1\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        400,\n        \"Missing required header\",\n        \"GET / HTTP/1.1\\r\\nConnection:  upgrade\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        400,\n        \"Missing required header\",\n        \"GET / HTTP/1.1\\r\\nConnection: upgrade\\r\\nUpgrade: websocket\\r\\n\\r\\n\",\n    );\n\n    try assertHTTPError(\n        400,\n        \"Missing required header\",\n        \"GET / HTTP/1.1\\r\\nConnection: upgrade\\r\\nUpgrade: websocket\\r\\nsec-websocket-version:13\\r\\n\\r\\n\",\n    );\n}\n\ntest \"Client: http valid handshake\" {\n    var c = try createTestClient();\n    defer c.deinit();\n\n    const request =\n        \"GET /   HTTP/1.1\\r\\n\" ++\n        \"Connection: upgrade\\r\\n\" ++\n        \"Upgrade: websocket\\r\\n\" ++\n        \"sec-websocket-version:13\\r\\n\" ++\n        \"sec-websocket-key: this is my key\\r\\n\" ++\n        \"Custom:  Header-Value\\r\\n\\r\\n\";\n\n    const res = try c.httpRequest(request);\n    try testing.expectEqualStrings(\"HTTP/1.1 101 Switching Protocols\\r\\n\" ++\n        \"Upgrade: websocket\\r\\n\" ++\n        \"Connection: upgrade\\r\\n\" ++\n        \"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\\r\\n\\r\\n\", res);\n}\n\ntest \"Client: read invalid websocket message\" {\n    // 131 = 128 (fin) | 3  where 3 isn't a valid type\n    try assertWebSocketError(\n        1002,\n        &.{ 131, 128, 'm', 'a', 's', 'k' },\n    );\n\n    for ([_]u8{ 16, 32, 64 }) |rsv| {\n        // none of the reserve flags should be set\n        try assertWebSocketError(\n            1002,\n            &.{ rsv, 128, 'm', 'a', 's', 'k' },\n        );\n\n        // as a bitmask\n        try assertWebSocketError(\n            1002,\n            &.{ rsv + 4, 128, 'm', 'a', 's', 'k' },\n        );\n    }\n\n    // client->server messages must be masked\n    try assertWebSocketError(\n        1002,\n        &.{ 129, 1, 'a' },\n    );\n\n    // control types (ping/ping/close) can't be > 125 bytes\n    for ([_]u8{ 136, 137, 138 }) |op| {\n        try assertWebSocketError(\n            1002,\n            &.{ op, 254, 1, 1 },\n        );\n    }\n\n    // length of message is 0000 0810, i.e: 1024 * 512 + 265\n    try assertWebSocketError(1009, &.{ 129, 255, 0, 0, 0, 0, 0, 8, 1, 0, 'm', 'a', 's', 'k' });\n\n    // continuation type message must come after a normal message\n    // even when not a fin frame\n    try assertWebSocketError(\n        1002,\n        &.{ 0, 129, 'm', 'a', 's', 'k', 'd' },\n    );\n\n    // continuation type message must come after a normal message\n    // even as a fin frame\n    try assertWebSocketError(\n        1002,\n        &.{ 128, 129, 'm', 'a', 's', 'k', 'd' },\n    );\n\n    // text (non-fin) - text (non-fin)\n    try assertWebSocketError(\n        1002,\n        &.{ 1, 129, 'm', 'a', 's', 'k', 'd', 1, 128, 'k', 's', 'a', 'm' },\n    );\n\n    // text (non-fin) - text (fin) should always been continuation after non-fin\n    try assertWebSocketError(\n        1002,\n        &.{ 1, 129, 'm', 'a', 's', 'k', 'd', 129, 128, 'k', 's', 'a', 'm' },\n    );\n\n    // close must be fin\n    try assertWebSocketError(\n        1002,\n        &.{\n            8, 129, 'm', 'a', 's', 'k', 'd',\n        },\n    );\n\n    // ping must be fin\n    try assertWebSocketError(\n        1002,\n        &.{\n            9, 129, 'm', 'a', 's', 'k', 'd',\n        },\n    );\n\n    // pong must be fin\n    try assertWebSocketError(\n        1002,\n        &.{\n            10, 129, 'm', 'a', 's', 'k', 'd',\n        },\n    );\n}\n\ntest \"Client: ping reply\" {\n    try assertWebSocketMessage(\n        // fin | pong, len\n        &.{ 138, 0 },\n\n        // fin | ping, masked | len, 4-byte mask\n        &.{ 137, 128, 0, 0, 0, 0 },\n    );\n\n    try assertWebSocketMessage(\n        // fin | pong, len, payload\n        &.{ 138, 5, 100, 96, 97, 109, 104 },\n\n        // fin | ping, masked | len, 4-byte mask, 5 byte payload\n        &.{ 137, 133, 0, 5, 7, 10, 100, 101, 102, 103, 104 },\n    );\n}\n\ntest \"Client: close message\" {\n    try assertWebSocketMessage(\n        // fin | close, len, close code (normal)\n        &.{ 136, 2, 3, 232 },\n\n        // fin | close, masked | len, 4-byte mask\n        &.{ 136, 128, 0, 0, 0, 0 },\n    );\n}\n\ntest \"server: 404\" {\n    var c = try createTestClient();\n    defer c.deinit();\n\n    const res = try c.httpRequest(\"GET /unknown HTTP/1.1\\r\\n\\r\\n\");\n    try testing.expectEqualStrings(\"HTTP/1.1 404 \\r\\n\" ++\n        \"Connection: Close\\r\\n\" ++\n        \"Content-Length: 9\\r\\n\\r\\n\" ++\n        \"Not found\", res);\n}\n\ntest \"server: get /json/version\" {\n    const expected_response =\n        \"HTTP/1.1 200 OK\\r\\n\" ++\n        \"Content-Length: 48\\r\\n\" ++\n        \"Connection: Close\\r\\n\" ++\n        \"Content-Type: application/json; charset=UTF-8\\r\\n\\r\\n\" ++\n        \"{\\\"webSocketDebuggerUrl\\\": \\\"ws://127.0.0.1:9583/\\\"}\";\n\n    {\n        // twice on the same connection\n        var c = try createTestClient();\n        defer c.deinit();\n\n        const res1 = try c.httpRequest(\"GET /json/version HTTP/1.1\\r\\n\\r\\n\");\n        try testing.expectEqualStrings(expected_response, res1);\n    }\n\n    {\n        // again on a new connection\n        var c = try createTestClient();\n        defer c.deinit();\n\n        const res1 = try c.httpRequest(\"GET /json/version HTTP/1.1\\r\\n\\r\\n\");\n        try testing.expectEqualStrings(expected_response, res1);\n    }\n}\n\nfn assertHTTPError(\n    comptime expected_status: u16,\n    comptime expected_body: []const u8,\n    input: []const u8,\n) !void {\n    var c = try createTestClient();\n    defer c.deinit();\n\n    const res = try c.httpRequest(input);\n    const expected_response = std.fmt.comptimePrint(\n        \"HTTP/1.1 {d} \\r\\nConnection: Close\\r\\nContent-Length: {d}\\r\\n\\r\\n{s}\",\n        .{ expected_status, expected_body.len, expected_body },\n    );\n\n    try testing.expectEqualStrings(expected_response, res);\n}\n\nfn assertWebSocketError(close_code: u16, input: []const u8) !void {\n    var c = try createTestClient();\n    defer c.deinit();\n\n    try c.handshake();\n    try c.stream.writeAll(input);\n\n    const msg = try c.readWebsocketMessage() orelse return error.NoMessage;\n    defer if (msg.cleanup_fragment) {\n        c.reader.cleanup();\n    };\n\n    try testing.expectEqual(.close, msg.type);\n    try testing.expectEqual(2, msg.data.len);\n    try testing.expectEqual(close_code, std.mem.readInt(u16, msg.data[0..2], .big));\n}\n\nfn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {\n    var c = try createTestClient();\n    defer c.deinit();\n\n    try c.handshake();\n    try c.stream.writeAll(input);\n\n    const msg = try c.readWebsocketMessage() orelse return error.NoMessage;\n    defer if (msg.cleanup_fragment) {\n        c.reader.cleanup();\n    };\n\n    const actual = c.reader.buf[0 .. msg.data.len + 2];\n    try testing.expectEqualSlices(u8, expected, actual);\n}\n\nconst MockCDP = struct {\n    messages: std.ArrayList([]const u8) = .{},\n\n    allocator: Allocator = testing.allocator,\n\n    fn init(_: Allocator, client: anytype) MockCDP {\n        _ = client;\n        return .{};\n    }\n\n    fn deinit(self: *MockCDP) void {\n        const allocator = self.allocator;\n        for (self.messages.items) |msg| {\n            allocator.free(msg);\n        }\n        self.messages.deinit(allocator);\n    }\n\n    fn handleMessage(self: *MockCDP, message: []const u8) bool {\n        const owned = self.allocator.dupe(u8, message) catch unreachable;\n        self.messages.append(self.allocator, owned) catch unreachable;\n        return true;\n    }\n};\n\nfn createTestClient() !TestClient {\n    const address = std.net.Address.initIp4([_]u8{ 127, 0, 0, 1 }, 9583);\n    const stream = try std.net.tcpConnectToAddress(address);\n\n    const timeout = std.mem.toBytes(posix.timeval{\n        .sec = 2,\n        .usec = 0,\n    });\n    try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);\n    try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);\n    return .{\n        .stream = stream,\n        .reader = .{\n            .allocator = testing.allocator,\n            .buf = try testing.allocator.alloc(u8, 1024 * 16),\n        },\n    };\n}\n\nconst TestClient = struct {\n    stream: std.net.Stream,\n    buf: [1024]u8 = undefined,\n    reader: Net.Reader(false),\n\n    fn deinit(self: *TestClient) void {\n        self.stream.close();\n        self.reader.deinit();\n    }\n\n    fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 {\n        try self.stream.writeAll(req);\n\n        var pos: usize = 0;\n        var total_length: ?usize = null;\n        while (true) {\n            pos += try self.stream.read(self.buf[pos..]);\n            if (pos == 0) {\n                return error.NoMoreData;\n            }\n            const response = self.buf[0..pos];\n            if (total_length == null) {\n                const header_end = std.mem.indexOf(u8, response, \"\\r\\n\\r\\n\") orelse continue;\n                const header = response[0 .. header_end + 4];\n\n                const cl = blk: {\n                    const cl_header = \"Content-Length: \";\n                    const start = (std.mem.indexOf(u8, header, cl_header) orelse {\n                        break :blk 0;\n                    }) + cl_header.len;\n\n                    const end = std.mem.indexOfScalarPos(u8, header, start, '\\r') orelse {\n                        return error.InvalidContentLength;\n                    };\n\n                    break :blk std.fmt.parseInt(usize, header[start..end], 10) catch {\n                        return error.InvalidContentLength;\n                    };\n                };\n\n                total_length = cl + header.len;\n            }\n\n            if (total_length) |tl| {\n                if (pos == tl) {\n                    return response;\n                }\n                if (pos > tl) {\n                    return error.DataExceedsContentLength;\n                }\n            }\n        }\n    }\n\n    fn handshake(self: *TestClient) !void {\n        const request =\n            \"GET /   HTTP/1.1\\r\\n\" ++\n            \"Connection: upgrade\\r\\n\" ++\n            \"Upgrade: websocket\\r\\n\" ++\n            \"sec-websocket-version:13\\r\\n\" ++\n            \"sec-websocket-key: this is my key\\r\\n\" ++\n            \"Custom:  Header-Value\\r\\n\\r\\n\";\n\n        const res = try self.httpRequest(request);\n        try testing.expectEqualStrings(\"HTTP/1.1 101 Switching Protocols\\r\\n\" ++\n            \"Upgrade: websocket\\r\\n\" ++\n            \"Connection: upgrade\\r\\n\" ++\n            \"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\\r\\n\\r\\n\", res);\n    }\n\n    fn readWebsocketMessage(self: *TestClient) !?Net.Message {\n        while (true) {\n            const n = try self.stream.read(self.reader.readBuf());\n            if (n == 0) {\n                return error.Closed;\n            }\n            self.reader.len += n;\n            if (try self.reader.next()) |msg| {\n                return msg;\n            }\n        }\n    }\n};\n"
  },
  {
    "path": "src/Sighandler.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n//! This structure processes operating system signals (SIGINT, SIGTERM)\n//! and runs callbacks to clean up the system gracefully.\n//!\n//! The structure does not clear the memory allocated in the arena,\n//! clear the entire arena when exiting the program.\nconst std = @import(\"std\");\nconst assert = std.debug.assert;\nconst Allocator = std.mem.Allocator;\nconst lp = @import(\"lightpanda\");\n\nconst log = lp.log;\n\nconst SigHandler = @This();\n\narena: Allocator,\n\nsigset: std.posix.sigset_t = undefined,\nhandle_thread: ?std.Thread = null,\n\nattempt: u32 = 0,\nlisteners: std.ArrayList(Listener) = .empty,\n\npub const Listener = struct {\n    args: []const u8,\n    start: *const fn (context: *const anyopaque) void,\n};\n\npub fn install(self: *SigHandler) !void {\n    // Block SIGINT and SIGTERM for the current thread and all created from it\n    self.sigset = std.posix.sigemptyset();\n    std.posix.sigaddset(&self.sigset, std.posix.SIG.INT);\n    std.posix.sigaddset(&self.sigset, std.posix.SIG.TERM);\n    std.posix.sigaddset(&self.sigset, std.posix.SIG.QUIT);\n    std.posix.sigprocmask(std.posix.SIG.BLOCK, &self.sigset, null);\n\n    self.handle_thread = try std.Thread.spawn(.{ .allocator = self.arena }, SigHandler.sighandle, .{self});\n    self.handle_thread.?.detach();\n}\n\npub fn on(self: *SigHandler, func: anytype, args: std.meta.ArgsTuple(@TypeOf(func))) !void {\n    assert(@typeInfo(@TypeOf(func)).@\"fn\".return_type.? == void);\n\n    const Args = @TypeOf(args);\n    const TypeErased = struct {\n        fn start(context: *const anyopaque) void {\n            const args_casted: *const Args = @ptrCast(@alignCast(context));\n            @call(.auto, func, args_casted.*);\n        }\n    };\n\n    const buffer = try self.arena.alignedAlloc(u8, .of(Args), @sizeOf(Args));\n    errdefer self.arena.free(buffer);\n\n    const bytes: []const u8 = @ptrCast((&args)[0..1]);\n    @memcpy(buffer, bytes);\n\n    try self.listeners.append(self.arena, .{\n        .args = buffer,\n        .start = TypeErased.start,\n    });\n}\n\nfn sighandle(self: *SigHandler) noreturn {\n    while (true) {\n        var sig: c_int = 0;\n\n        const rc = std.c.sigwait(&self.sigset, &sig);\n        if (rc != 0) {\n            log.err(.app, \"Unable to process signal {}\", .{rc});\n            std.process.exit(1);\n        }\n\n        switch (sig) {\n            std.posix.SIG.INT, std.posix.SIG.TERM => {\n                if (self.attempt > 1) {\n                    std.process.exit(1);\n                }\n                self.attempt += 1;\n\n                log.info(.app, \"Received termination signal...\", .{});\n                for (self.listeners.items) |*item| {\n                    item.start(item.args.ptr);\n                }\n                continue;\n            },\n            else => continue,\n        }\n    }\n}\n"
  },
  {
    "path": "src/TestHTTPServer.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst URL = @import(\"browser/URL.zig\");\n\nconst TestHTTPServer = @This();\n\nshutdown: std.atomic.Value(bool),\nlistener: ?std.net.Server,\nhandler: Handler,\n\nconst Handler = *const fn (req: *std.http.Server.Request) anyerror!void;\n\npub fn init(handler: Handler) TestHTTPServer {\n    return .{\n        .shutdown = .init(true),\n        .listener = null,\n        .handler = handler,\n    };\n}\n\npub fn deinit(self: *TestHTTPServer) void {\n    self.listener = null;\n}\n\npub fn stop(self: *TestHTTPServer) void {\n    self.shutdown.store(true, .release);\n    if (self.listener) |*listener| {\n        switch (@import(\"builtin\").target.os.tag) {\n            .linux => std.posix.shutdown(listener.stream.handle, .recv) catch {},\n            else => std.posix.close(listener.stream.handle),\n        }\n    }\n}\n\npub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {\n    const address = try std.net.Address.parseIp(\"127.0.0.1\", 9582);\n\n    self.listener = try address.listen(.{ .reuse_address = true });\n    var listener = &self.listener.?;\n    self.shutdown.store(false, .release);\n\n    wg.finish();\n\n    while (true) {\n        const conn = listener.accept() catch |err| {\n            if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {\n                return;\n            }\n            return err;\n        };\n        const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });\n        thrd.detach();\n    }\n}\n\nfn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {\n    defer conn.stream.close();\n\n    var req_buf: [2048]u8 = undefined;\n    var conn_reader = conn.stream.reader(&req_buf);\n    var conn_writer = conn.stream.writer(&req_buf);\n\n    var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);\n\n    while (true) {\n        var req = http_server.receiveHead() catch |err| switch (err) {\n            error.ReadFailed => continue,\n            error.HttpConnectionClosing => continue,\n            else => {\n                std.debug.print(\"Test HTTP Server error: {}\\n\", .{err});\n                return err;\n            },\n        };\n\n        self.handler(&req) catch |err| {\n            std.debug.print(\"test http error '{s}': {}\\n\", .{ req.head.target, err });\n            try req.respond(\"server error\", .{ .status = .internal_server_error });\n            return;\n        };\n    }\n}\n\npub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {\n    var url_buf: [1024]u8 = undefined;\n    var fba = std.heap.FixedBufferAllocator.init(&url_buf);\n    const unescaped_file_path = try URL.unescape(fba.allocator(), file_path);\n    var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) {\n        error.FileNotFound => return req.respond(\"server error\", .{ .status = .not_found }),\n        else => return err,\n    };\n    defer file.close();\n\n    const stat = try file.stat();\n    var send_buffer: [4096]u8 = undefined;\n\n    var res = try req.respondStreaming(&send_buffer, .{\n        .content_length = stat.size,\n        .respond_options = .{\n            .extra_headers = &.{\n                .{ .name = \"content-type\", .value = getContentType(file_path) },\n            },\n        },\n    });\n\n    var read_buffer: [4096]u8 = undefined;\n    var reader = file.reader(&read_buffer);\n    _ = try res.writer.sendFileAll(&reader, .unlimited);\n    try res.writer.flush();\n    try res.end();\n}\n\nfn getContentType(file_path: []const u8) []const u8 {\n    if (std.mem.endsWith(u8, file_path, \".js\")) {\n        return \"application/json\";\n    }\n\n    if (std.mem.endsWith(u8, file_path, \".html\")) {\n        return \"text/html\";\n    }\n\n    if (std.mem.endsWith(u8, file_path, \".htm\")) {\n        return \"text/html\";\n    }\n\n    if (std.mem.endsWith(u8, file_path, \".xml\")) {\n        // some wpt tests do this\n        return \"text/xml\";\n    }\n\n    if (std.mem.endsWith(u8, file_path, \".mjs\")) {\n        // mjs are ECMAScript modules\n        return \"application/json\";\n    }\n\n    std.debug.print(\"TestHTTPServer asked to serve an unknown file type: {s}\\n\", .{file_path});\n    return \"text/html\";\n}\n"
  },
  {
    "path": "src/browser/Browser.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst js = @import(\"js/js.zig\");\nconst log = @import(\"../log.zig\");\nconst App = @import(\"../App.zig\");\nconst HttpClient = @import(\"HttpClient.zig\");\n\nconst ArenaPool = App.ArenaPool;\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Session = @import(\"Session.zig\");\nconst Notification = @import(\"../Notification.zig\");\n\n// Browser is an instance of the browser.\n// You can create multiple browser instances.\n// A browser contains only one session.\nconst Browser = @This();\n\nenv: js.Env,\napp: *App,\nsession: ?Session,\nallocator: Allocator,\narena_pool: *ArenaPool,\nhttp_client: *HttpClient,\n\nconst InitOpts = struct {\n    env: js.Env.InitOpts = .{},\n    http_client: *HttpClient,\n};\n\npub fn init(app: *App, opts: InitOpts) !Browser {\n    const allocator = app.allocator;\n\n    var env = try js.Env.init(app, opts.env);\n    errdefer env.deinit();\n\n    return .{\n        .app = app,\n        .env = env,\n        .session = null,\n        .allocator = allocator,\n        .arena_pool = &app.arena_pool,\n        .http_client = opts.http_client,\n    };\n}\n\npub fn deinit(self: *Browser) void {\n    self.closeSession();\n    self.env.deinit();\n}\n\npub fn newSession(self: *Browser, notification: *Notification) !*Session {\n    self.closeSession();\n    self.session = @as(Session, undefined);\n    const session = &self.session.?;\n    try Session.init(session, self, notification);\n    return session;\n}\n\npub fn closeSession(self: *Browser) void {\n    if (self.session) |*session| {\n        session.deinit();\n        self.session = null;\n        self.env.memoryPressureNotification(.critical);\n    }\n}\n\npub fn runMicrotasks(self: *Browser) void {\n    self.env.runMicrotasks();\n}\n\npub fn runMacrotasks(self: *Browser) !void {\n    const env = &self.env;\n\n    try self.env.runMacrotasks();\n    env.pumpMessageLoop();\n\n    // either of the above could have queued more microtasks\n    env.runMicrotasks();\n}\n\npub fn hasBackgroundTasks(self: *Browser) bool {\n    return self.env.hasBackgroundTasks();\n}\n\npub fn waitForBackgroundTasks(self: *Browser) void {\n    self.env.waitForBackgroundTasks();\n}\n\npub fn msToNextMacrotask(self: *Browser) ?u64 {\n    return self.env.msToNextMacrotask();\n}\n\npub fn msTo(self: *Browser) bool {\n    return self.env.hasBackgroundTasks();\n}\n\npub fn runIdleTasks(self: *const Browser) void {\n    self.env.runIdleTasks();\n}\n"
  },
  {
    "path": "src/browser/EventManager.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst log = @import(\"../log.zig\");\nconst String = @import(\"../string.zig\").String;\n\nconst js = @import(\"js/js.zig\");\nconst Page = @import(\"Page.zig\");\n\nconst Node = @import(\"webapi/Node.zig\");\nconst Event = @import(\"webapi/Event.zig\");\nconst EventTarget = @import(\"webapi/EventTarget.zig\");\nconst Element = @import(\"webapi/Element.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\nconst EventKey = struct {\n    event_target: usize,\n    type_string: String,\n};\n\nconst EventKeyContext = struct {\n    pub fn hash(_: @This(), key: EventKey) u64 {\n        var hasher = std.hash.Wyhash.init(0);\n        hasher.update(std.mem.asBytes(&key.event_target));\n        hasher.update(key.type_string.str());\n        return hasher.final();\n    }\n\n    pub fn eql(_: @This(), a: EventKey, b: EventKey) bool {\n        return a.event_target == b.event_target and a.type_string.eql(b.type_string);\n    }\n};\n\npub const EventManager = @This();\n\npage: *Page,\narena: Allocator,\n// Used as an optimization in Page._documentIsComplete. If we know there are no\n// 'load' listeners in the document, we can skip dispatching the per-resource\n// 'load' event (e.g. amazon product page has no listener and ~350 resources)\nhas_dom_load_listener: bool,\nlistener_pool: std.heap.MemoryPool(Listener),\nignore_list: std.ArrayList(*Listener),\nlist_pool: std.heap.MemoryPool(std.DoublyLinkedList),\nlookup: std.HashMapUnmanaged(\n    EventKey,\n    *std.DoublyLinkedList,\n    EventKeyContext,\n    std.hash_map.default_max_load_percentage,\n),\ndispatch_depth: usize,\ndeferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),\n\npub fn init(arena: Allocator, page: *Page) EventManager {\n    return .{\n        .page = page,\n        .lookup = .{},\n        .arena = arena,\n        .ignore_list = .{},\n        .list_pool = .init(arena),\n        .listener_pool = .init(arena),\n        .dispatch_depth = 0,\n        .deferred_removals = .{},\n        .has_dom_load_listener = false,\n    };\n}\n\npub const RegisterOptions = struct {\n    once: bool = false,\n    capture: bool = false,\n    passive: bool = false,\n    signal: ?*@import(\"webapi/AbortSignal.zig\") = null,\n};\n\npub const Callback = union(enum) {\n    function: js.Function,\n    object: js.Object,\n};\n\npub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {\n    if (comptime IS_DEBUG) {\n        log.debug(.event, \"eventManager.register\", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target.toString() });\n    }\n\n    // If a signal is provided and already aborted, don't register the listener\n    if (opts.signal) |signal| {\n        if (signal.getAborted()) {\n            return;\n        }\n    }\n\n    // Allocate the type string we'll use in both listener and key\n    const type_string = try String.init(self.arena, typ, .{});\n\n    if (type_string.eql(comptime .wrap(\"load\")) and target._type == .node) {\n        self.has_dom_load_listener = true;\n    }\n\n    const gop = try self.lookup.getOrPut(self.arena, .{\n        .type_string = type_string,\n        .event_target = @intFromPtr(target),\n    });\n    if (gop.found_existing) {\n        // check for duplicate callbacks already registered\n        var node = gop.value_ptr.*.first;\n        while (node) |n| {\n            const listener: *Listener = @alignCast(@fieldParentPtr(\"node\", n));\n            const is_duplicate = switch (callback) {\n                .object => |obj| listener.function.eqlObject(obj),\n                .function => |func| listener.function.eqlFunction(func),\n            };\n            if (is_duplicate and listener.capture == opts.capture) {\n                return;\n            }\n            node = n.next;\n        }\n    } else {\n        gop.value_ptr.* = try self.list_pool.create();\n        gop.value_ptr.*.* = .{};\n    }\n\n    const func = switch (callback) {\n        .function => |f| Function{ .value = try f.persist() },\n        .object => |o| Function{ .object = try o.persist() },\n    };\n\n    const listener = try self.listener_pool.create();\n    listener.* = .{\n        .node = .{},\n        .once = opts.once,\n        .capture = opts.capture,\n        .passive = opts.passive,\n        .function = func,\n        .signal = opts.signal,\n        .typ = type_string,\n    };\n    // append the listener to the list of listeners for this target\n    gop.value_ptr.*.append(&listener.node);\n\n    // Track load listeners for script execution ignore list\n    if (type_string.eql(comptime .wrap(\"load\"))) {\n        try self.ignore_list.append(self.arena, listener);\n    }\n}\n\npub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {\n    const list = self.lookup.get(.{\n        .type_string = .wrap(typ),\n        .event_target = @intFromPtr(target),\n    }) orelse return;\n    if (findListener(list, callback, use_capture)) |listener| {\n        self.removeListener(list, listener);\n    }\n}\n\npub fn clearIgnoreList(self: *EventManager) void {\n    self.ignore_list.clearRetainingCapacity();\n}\n\n// Dispatching can be recursive from the compiler's point of view, so we need to\n// give it an explicit error set so that other parts of the code can use and\n// inferred error.\nconst DispatchError = error{\n    OutOfMemory,\n    StringTooLarge,\n    JSExecCallback,\n    CompilationError,\n    ExecutionError,\n    JsException,\n};\n\npub const DispatchOpts = struct {\n    // A \"load\" event triggered by a script (in ScriptManager) should not trigger\n    // a \"load\" listener added within that script. Therefore, any \"load\" listener\n    // that we add go into an ignore list until after the script finishes executing.\n    // The ignore list is only checked when apply_ignore  == true, which is only\n    // set by the ScriptManager when raising the script's \"load\" event.\n    apply_ignore: bool = false,\n};\n\npub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {\n    return self.dispatchOpts(target, event, .{});\n}\n\npub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {\n    event.acquireRef();\n    defer event.deinit(false, self.page._session);\n\n    if (comptime IS_DEBUG) {\n        log.debug(.event, \"eventManager.dispatch\", .{ .type = event._type_string.str(), .bubbles = event._bubbles });\n    }\n\n    switch (target._type) {\n        .node => |node| try self.dispatchNode(node, event, opts),\n        else => try self.dispatchDirect(target, event, null, .{ .context = \"dispatch\" }),\n    }\n}\n\n// There are a lot of events that can be attached via addEventListener or as\n// a property, like the XHR events, or window.onload. You might think that the\n// property is just a shortcut for calling addEventListener, but they are distinct.\n// An event set via property cannot be removed by removeEventListener. If you\n// set both the property and add a listener, they both execute.\nconst DispatchDirectOptions = struct {\n    context: []const u8,\n    inject_target: bool = true,\n};\n\n// Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with\n// property handlers. No propagation - just calls the handler and registered listeners.\n// Handler can be: null, ?js.Function.Global, ?js.Function.Temp, or js.Function\npub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {\n    const page = self.page;\n\n    // Set window.event to the currently dispatching event (WHATWG spec)\n    const window = page.window;\n    const prev_event = window._current_event;\n    window._current_event = event;\n    defer window._current_event = prev_event;\n\n    event.acquireRef();\n    defer event.deinit(false, page._session);\n\n    if (comptime IS_DEBUG) {\n        log.debug(.event, \"dispatchDirect\", .{ .type = event._type_string, .context = opts.context });\n    }\n\n    if (comptime opts.inject_target) {\n        event._target = target;\n        event._dispatch_target = target; // Store original target for composedPath()\n    }\n\n    var was_dispatched = false;\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer {\n        ls.local.runMicrotasks();\n        ls.deinit();\n    }\n\n    if (getFunction(handler, &ls.local)) |func| {\n        event._current_target = target;\n        if (func.callWithThis(void, target, .{event})) {\n            was_dispatched = true;\n        } else |err| {\n            // a non-JS error\n            log.warn(.event, opts.context, .{ .err = err });\n        }\n    }\n\n    // listeners reigstered via addEventListener\n    const list = self.lookup.get(.{\n        .event_target = @intFromPtr(target),\n        .type_string = event._type_string,\n    }) orelse return;\n\n    // This is a slightly simplified version of what you'll find in dispatchPhase\n    // It is simpler because, for direct dispatching, we know there's no ancestors\n    // and only the single target phase.\n\n    // Track dispatch depth for deferred removal\n    self.dispatch_depth += 1;\n    defer {\n        const dispatch_depth = self.dispatch_depth;\n        // Only destroy deferred listeners when we exit the outermost dispatch\n        if (dispatch_depth == 1) {\n            for (self.deferred_removals.items) |removal| {\n                removal.list.remove(&removal.listener.node);\n                self.listener_pool.destroy(removal.listener);\n            }\n            self.deferred_removals.clearRetainingCapacity();\n        } else {\n            self.dispatch_depth = dispatch_depth - 1;\n        }\n    }\n\n    // Use the last listener in the list as sentinel - listeners added during dispatch will be after it\n    const last_node = list.last orelse return;\n    const last_listener: *Listener = @alignCast(@fieldParentPtr(\"node\", last_node));\n\n    // Iterate through the list, stopping after we've encountered the last_listener\n    var node = list.first;\n    var is_done = false;\n    while (node) |n| {\n        if (is_done) {\n            break;\n        }\n\n        const listener: *Listener = @alignCast(@fieldParentPtr(\"node\", n));\n        is_done = (listener == last_listener);\n        node = n.next;\n\n        // Skip removed listeners\n        if (listener.removed) {\n            continue;\n        }\n\n        // If the listener has an aborted signal, remove it and skip\n        if (listener.signal) |signal| {\n            if (signal.getAborted()) {\n                self.removeListener(list, listener);\n                continue;\n            }\n        }\n\n        // Remove \"once\" listeners BEFORE calling them so nested dispatches don't see them\n        if (listener.once) {\n            self.removeListener(list, listener);\n        }\n\n        was_dispatched = true;\n        event._current_target = target;\n\n        switch (listener.function) {\n            .value => |value| try ls.toLocal(value).callWithThis(void, target, .{event}),\n            .string => |string| {\n                const str = try page.call_arena.dupeZ(u8, string.str());\n                try ls.local.eval(str, null);\n            },\n            .object => |obj_global| {\n                const obj = ls.toLocal(obj_global);\n                if (try obj.getFunction(\"handleEvent\")) |handleEvent| {\n                    try handleEvent.callWithThis(void, obj, .{event});\n                }\n            },\n        }\n\n        if (event._stop_immediate_propagation) {\n            return;\n        }\n    }\n}\n\nfn getFunction(handler: anytype, local: *const js.Local) ?js.Function {\n    const T = @TypeOf(handler);\n    const ti = @typeInfo(T);\n\n    if (ti == .null) {\n        return null;\n    }\n    if (ti == .optional) {\n        return getFunction(handler orelse return null, local);\n    }\n    return switch (T) {\n        js.Function => handler,\n        js.Function.Temp => local.toLocal(handler),\n        js.Function.Global => local.toLocal(handler),\n        else => @compileError(\"handler must be null or \\\\??js.Function(\\\\.(Temp|Global))?\"),\n    };\n}\n\n/// Check if there are any listeners for a direct dispatch (non-DOM target).\n/// Use this to avoid creating an event when there are no listeners.\npub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []const u8, handler: anytype) bool {\n    if (hasHandler(handler)) {\n        return true;\n    }\n    return self.lookup.get(.{\n        .event_target = @intFromPtr(target),\n        .type_string = .wrap(typ),\n    }) != null;\n}\n\nfn hasHandler(handler: anytype) bool {\n    const ti = @typeInfo(@TypeOf(handler));\n    if (ti == .null) {\n        return false;\n    }\n    if (ti == .optional) {\n        return handler != null;\n    }\n    return true;\n}\n\nfn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {\n    const ShadowRoot = @import(\"webapi/ShadowRoot.zig\");\n\n    {\n        const et = target.asEventTarget();\n        event._target = et;\n        event._dispatch_target = et; // Store original target for composedPath()\n    }\n\n    const page = self.page;\n\n    // Set window.event to the currently dispatching event (WHATWG spec)\n    const window = page.window;\n    const prev_event = window._current_event;\n    window._current_event = event;\n    defer window._current_event = prev_event;\n\n    var was_handled = false;\n\n    // Create a single scope for all event handlers in this dispatch.\n    // This ensures function handles passed to queueMicrotask remain valid\n    // throughout the entire dispatch, preventing crashes when microtasks run.\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer {\n        if (was_handled) {\n            ls.local.runMicrotasks();\n        }\n        ls.deinit();\n    }\n\n    const activation_state = ActivationState.create(event, target, page);\n\n    // Defer runs even on early return - ensures event phase is reset\n    // and default actions execute (unless prevented)\n    defer {\n        event._event_phase = .none;\n        event._stop_propagation = false;\n        event._stop_immediate_propagation = false;\n        // Handle checkbox/radio activation rollback or commit\n        if (activation_state) |state| {\n            state.restore(event, page);\n        }\n\n        // Execute default action if not prevented\n        if (event._prevent_default) {\n            // can't return in a defer (╯°□°)╯︵ ┻━┻\n        } else if (event._type_string.eql(comptime .wrap(\"click\"))) {\n            page.handleClick(target) catch |err| {\n                log.warn(.event, \"page.click\", .{ .err = err });\n            };\n        } else if (event._type_string.eql(comptime .wrap(\"keydown\"))) {\n            page.handleKeydown(target, event) catch |err| {\n                log.warn(.event, \"page.keydown\", .{ .err = err });\n            };\n        }\n    }\n\n    var path_len: usize = 0;\n    var path_buffer: [128]*EventTarget = undefined;\n\n    var node: ?*Node = target;\n    while (node) |n| {\n        if (path_len >= path_buffer.len) break;\n        path_buffer[path_len] = n.asEventTarget();\n        path_len += 1;\n\n        // Check if this node is a shadow root\n        if (n.is(ShadowRoot)) |shadow| {\n            event._needs_retargeting = true;\n\n            // If event is not composed, stop at shadow boundary\n            if (!event._composed) {\n                break;\n            }\n\n            // Otherwise, jump to the shadow host and continue\n            node = shadow._host.asNode();\n            continue;\n        }\n\n        node = n._parent;\n    }\n\n    // Even though the window isn't part of the DOM, most events propagate\n    // through it in the capture phase (unless we stopped at a shadow boundary)\n    // The only explicit exception is \"load\"\n    if (event._type_string.eql(comptime .wrap(\"load\")) == false) {\n        if (path_len < path_buffer.len) {\n            path_buffer[path_len] = page.window.asEventTarget();\n            path_len += 1;\n        }\n    }\n\n    const path = path_buffer[0..path_len];\n\n    // Phase 1: Capturing phase (root → target, excluding target)\n    // This happens for all events, regardless of bubbling\n    event._event_phase = .capturing_phase;\n    var i: usize = path_len;\n    while (i > 1) {\n        i -= 1;\n        if (event._stop_propagation) return;\n        const current_target = path[i];\n        if (self.lookup.get(.{\n            .event_target = @intFromPtr(current_target),\n            .type_string = event._type_string,\n        })) |list| {\n            try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(true, opts));\n        }\n    }\n\n    // Phase 2: At target\n    if (event._stop_propagation) return;\n    event._event_phase = .at_target;\n    const target_et = target.asEventTarget();\n\n    blk: {\n        // Get inline handler (e.g., onclick property) for this target\n        if (self.getInlineHandler(target_et, event)) |inline_handler| {\n            was_handled = true;\n            event._current_target = target_et;\n\n            try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});\n\n            if (event._stop_propagation) {\n                return;\n            }\n\n            if (event._stop_immediate_propagation) {\n                break :blk;\n            }\n        }\n\n        if (self.lookup.get(.{\n            .type_string = event._type_string,\n            .event_target = @intFromPtr(target_et),\n        })) |list| {\n            try self.dispatchPhase(list, target_et, event, &was_handled, &ls.local, comptime .init(null, opts));\n            if (event._stop_propagation) {\n                return;\n            }\n        }\n    }\n\n    // Phase 3: Bubbling phase (target → root, excluding target)\n    // This only happens if the event bubbles\n    if (event._bubbles) {\n        event._event_phase = .bubbling_phase;\n        for (path[1..]) |current_target| {\n            if (event._stop_propagation) break;\n            if (self.lookup.get(.{\n                .type_string = event._type_string,\n                .event_target = @intFromPtr(current_target),\n            })) |list| {\n                try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(false, opts));\n            }\n        }\n    }\n}\n\nconst DispatchPhaseOpts = struct {\n    capture_only: ?bool = null,\n    apply_ignore: bool = false,\n\n    fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {\n        return .{\n            .capture_only = capture_only,\n            .apply_ignore = opts.apply_ignore,\n        };\n    }\n};\n\nfn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, local: *const js.Local, comptime opts: DispatchPhaseOpts) !void {\n    const page = self.page;\n\n    // Track dispatch depth for deferred removal\n    self.dispatch_depth += 1;\n    defer {\n        const dispatch_depth = self.dispatch_depth;\n        // Only destroy deferred listeners when we exit the outermost dispatch\n        if (dispatch_depth == 1) {\n            for (self.deferred_removals.items) |removal| {\n                removal.list.remove(&removal.listener.node);\n                self.listener_pool.destroy(removal.listener);\n            }\n            self.deferred_removals.clearRetainingCapacity();\n        } else {\n            self.dispatch_depth = dispatch_depth - 1;\n        }\n    }\n\n    // Use the last listener in the list as sentinel - listeners added during dispatch will be after it\n    const last_node = list.last orelse return;\n    const last_listener: *Listener = @alignCast(@fieldParentPtr(\"node\", last_node));\n\n    // Iterate through the list, stopping after we've encountered the last_listener\n    var node = list.first;\n    var is_done = false;\n    node_loop: while (node) |n| {\n        if (is_done) {\n            break;\n        }\n\n        const listener: *Listener = @alignCast(@fieldParentPtr(\"node\", n));\n        is_done = (listener == last_listener);\n        node = n.next;\n\n        // Skip non-matching listeners\n        if (comptime opts.capture_only) |capture| {\n            if (listener.capture != capture) {\n                continue;\n            }\n        }\n\n        // Skip removed listeners\n        if (listener.removed) {\n            continue;\n        }\n\n        // If the listener has an aborted signal, remove it and skip\n        if (listener.signal) |signal| {\n            if (signal.getAborted()) {\n                self.removeListener(list, listener);\n                continue;\n            }\n        }\n\n        if (comptime opts.apply_ignore) {\n            for (self.ignore_list.items) |ignored| {\n                if (ignored == listener) {\n                    continue :node_loop;\n                }\n            }\n        }\n\n        // Remove \"once\" listeners BEFORE calling them so nested dispatches don't see them\n        if (listener.once) {\n            self.removeListener(list, listener);\n        }\n\n        was_handled.* = true;\n        event._current_target = current_target;\n\n        // Compute adjusted target for shadow DOM retargeting (only if needed)\n        const original_target = event._target;\n        if (event._needs_retargeting) {\n            event._target = getAdjustedTarget(original_target, current_target);\n        }\n\n        switch (listener.function) {\n            .value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}),\n            .string => |string| {\n                const str = try page.call_arena.dupeZ(u8, string.str());\n                try local.eval(str, null);\n            },\n            .object => |obj_global| {\n                const obj = local.toLocal(obj_global);\n                if (try obj.getFunction(\"handleEvent\")) |handleEvent| {\n                    try handleEvent.callWithThis(void, obj, .{event});\n                }\n            },\n        }\n\n        // Restore original target (only if we changed it)\n        if (event._needs_retargeting) {\n            event._target = original_target;\n        }\n\n        if (event._stop_immediate_propagation) {\n            return;\n        }\n    }\n}\n\nfn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {\n    const global_event_handlers = @import(\"webapi/global_event_handlers.zig\");\n    const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;\n\n    // Look up the inline handler for this target\n    const html_element = switch (target._type) {\n        .node => |n| n.is(Element.Html) orelse return null,\n        else => return null,\n    };\n\n    return html_element.getAttributeFunction(handler_type, self.page) catch |err| {\n        log.warn(.event, \"inline html callback\", .{ .type = handler_type, .err = err });\n        return null;\n    };\n}\n\nfn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {\n    // If we're in a dispatch, defer removal to avoid invalidating iteration\n    if (self.dispatch_depth > 0) {\n        listener.removed = true;\n        self.deferred_removals.append(self.arena, .{ .list = list, .listener = listener }) catch unreachable;\n    } else {\n        // Outside dispatch, remove immediately\n        list.remove(&listener.node);\n        self.listener_pool.destroy(listener);\n    }\n}\n\nfn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture: bool) ?*Listener {\n    var node = list.first;\n    while (node) |n| {\n        node = n.next;\n        const listener: *Listener = @alignCast(@fieldParentPtr(\"node\", n));\n        const matches = switch (callback) {\n            .object => |obj| listener.function.eqlObject(obj),\n            .function => |func| listener.function.eqlFunction(func),\n        };\n        if (!matches) {\n            continue;\n        }\n        if (listener.capture != capture) {\n            continue;\n        }\n        return listener;\n    }\n    return null;\n}\n\nconst Listener = struct {\n    typ: String,\n    once: bool,\n    capture: bool,\n    passive: bool,\n    function: Function,\n    signal: ?*@import(\"webapi/AbortSignal.zig\") = null,\n    node: std.DoublyLinkedList.Node,\n    removed: bool = false,\n};\n\nconst Function = union(enum) {\n    value: js.Function.Global,\n    string: String,\n    object: js.Object.Global,\n\n    fn eqlFunction(self: Function, func: js.Function) bool {\n        return switch (self) {\n            .value => |v| v.isEqual(func),\n            else => false,\n        };\n    }\n\n    fn eqlObject(self: Function, obj: js.Object) bool {\n        return switch (self) {\n            .object => |o| return o.isEqual(obj),\n            else => false,\n        };\n    }\n};\n\n// Computes the adjusted target for shadow DOM event retargeting\n// Returns the lowest shadow-including ancestor of original_target that is\n// also an ancestor-or-self of current_target\nfn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {\n    const ShadowRoot = @import(\"webapi/ShadowRoot.zig\");\n\n    const orig_node = switch ((original_target orelse return null)._type) {\n        .node => |n| n,\n        else => return original_target,\n    };\n    const curr_node = switch (current_target._type) {\n        .node => |n| n,\n        else => return original_target,\n    };\n\n    // Walk up from original target, checking if we can reach current target\n    var node: ?*Node = orig_node;\n    while (node) |n| {\n        // Check if current_target is an ancestor of n (or n itself)\n        if (isAncestorOrSelf(curr_node, n)) {\n            return n.asEventTarget();\n        }\n\n        // Cross shadow boundary if needed\n        if (n.is(ShadowRoot)) |shadow| {\n            node = shadow._host.asNode();\n            continue;\n        }\n\n        node = n._parent;\n    }\n\n    return original_target;\n}\n\n// Check if ancestor is an ancestor of (or the same as) node\n// WITHOUT crossing shadow boundaries (just regular DOM tree)\nfn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {\n    if (ancestor == node) {\n        return true;\n    }\n\n    var current: ?*Node = node._parent;\n    while (current) |n| {\n        if (n == ancestor) {\n            return true;\n        }\n        current = n._parent;\n    }\n\n    return false;\n}\n\n// Handles the default action for clicking on input checked/radio. Maybe this\n// could be generalized if needed, but I'm not sure. This wasn't obvious to me\n// but when an input is clicked, it's important to think about both the intent\n// and the actual result. Imagine you have an unchecked checkbox. When clicked,\n// the checkbox immediately becomes checked, and event handlers see this \"checked\"\n// intent. But a listener can preventDefault() in which case the check we did at\n// the start will be undone.\n// This is a bit more complicated for radio buttons, as the checking/unchecking\n// and the rollback can impact a different radio input. So if you \"check\" a radio\n// the intent is that it becomes checked and whatever was checked before becomes\n// unchecked, so that if you have to rollback (because of a preventDefault())\n// then both inputs have to revert to their original values.\nconst ActivationState = struct {\n    old_checked: bool,\n    input: *Element.Html.Input,\n    previously_checked_radio: ?*Input,\n\n    const Input = Element.Html.Input;\n\n    fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState {\n        if (event._type_string.eql(comptime .wrap(\"click\")) == false) {\n            return null;\n        }\n\n        const input = target.is(Element.Html.Input) orelse return null;\n        if (input._input_type != .checkbox and input._input_type != .radio) {\n            return null;\n        }\n\n        const old_checked = input._checked;\n        var previously_checked_radio: ?*Element.Html.Input = null;\n\n        // For radio buttons, find the currently checked radio in the group\n        if (input._input_type == .radio and !old_checked) {\n            previously_checked_radio = try findCheckedRadioInGroup(input, page);\n        }\n\n        // Toggle checkbox or check radio (which unchecks others in group)\n        const new_checked = if (input._input_type == .checkbox) !old_checked else true;\n        try input.setChecked(new_checked, page);\n\n        return .{\n            .input = input,\n            .old_checked = old_checked,\n            .previously_checked_radio = previously_checked_radio,\n        };\n    }\n\n    fn restore(self: *const ActivationState, event: *const Event, page: *Page) void {\n        const input = self.input;\n        if (event._prevent_default) {\n            // Rollback: restore previous state\n            input._checked = self.old_checked;\n            input._checked_dirty = true;\n            if (self.previously_checked_radio) |prev_radio| {\n                prev_radio._checked = true;\n                prev_radio._checked_dirty = true;\n            }\n            return;\n        }\n\n        // Commit: fire input and change events only if state actually changed\n        // and the element is connected to a document (detached elements don't fire).\n        // For checkboxes, state always changes. For radios, only if was unchecked.\n        const state_changed = (input._input_type == .checkbox) or !self.old_checked;\n        if (state_changed and input.asElement().asNode().isConnected()) {\n            fireEvent(page, input, \"input\") catch |err| {\n                log.warn(.event, \"input event\", .{ .err = err });\n            };\n            fireEvent(page, input, \"change\") catch |err| {\n                log.warn(.event, \"change event\", .{ .err = err });\n            };\n        }\n    }\n\n    fn findCheckedRadioInGroup(input: *Input, page: *Page) !?*Input {\n        const elem = input.asElement();\n\n        const name = elem.getAttributeSafe(comptime .wrap(\"name\")) orelse return null;\n        if (name.len == 0) {\n            return null;\n        }\n\n        const form = input.getForm(page);\n\n        // Walk from the root of the tree containing this element\n        // This handles both document-attached and orphaned elements\n        const root = elem.asNode().getRootNode(null);\n\n        const TreeWalker = @import(\"webapi/TreeWalker.zig\");\n        var walker = TreeWalker.Full.init(root, .{});\n\n        while (walker.next()) |node| {\n            const other_element = node.is(Element) orelse continue;\n            const other_input = other_element.is(Input) orelse continue;\n\n            if (other_input._input_type != .radio) {\n                continue;\n            }\n\n            // Skip the input we're checking from\n            if (other_input == input) {\n                continue;\n            }\n\n            const other_name = other_element.getAttributeSafe(comptime .wrap(\"name\")) orelse continue;\n            if (!std.mem.eql(u8, name, other_name)) {\n                continue;\n            }\n\n            // Check if same form context\n            const other_form = other_input.getForm(page);\n            if (form) |f| {\n                const of = other_form orelse continue;\n                if (f != of) {\n                    continue; // Different forms\n                }\n            } else if (other_form != null) {\n                continue; // form is null but other has a form\n            }\n\n            if (other_input._checked) {\n                return other_input;\n            }\n        }\n\n        return null;\n    }\n\n    // Fire input or change event\n    fn fireEvent(page: *Page, input: *Input, comptime typ: []const u8) !void {\n        const event = try Event.initTrusted(comptime .wrap(typ), .{\n            .bubbles = true,\n            .cancelable = false,\n        }, page);\n\n        const target = input.asElement().asEventTarget();\n        try page._event_manager.dispatch(target, event);\n    }\n};\n"
  },
  {
    "path": "src/browser/Factory.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst reflect = @import(\"reflect.zig\");\n\nconst log = @import(\"../log.zig\");\nconst String = @import(\"../string.zig\").String;\n\nconst SlabAllocator = @import(\"../slab.zig\").SlabAllocator;\n\nconst Page = @import(\"Page.zig\");\nconst Node = @import(\"webapi/Node.zig\");\nconst Event = @import(\"webapi/Event.zig\");\nconst UIEvent = @import(\"webapi/event/UIEvent.zig\");\nconst MouseEvent = @import(\"webapi/event/MouseEvent.zig\");\nconst Element = @import(\"webapi/Element.zig\");\nconst Document = @import(\"webapi/Document.zig\");\nconst EventTarget = @import(\"webapi/EventTarget.zig\");\nconst XMLHttpRequestEventTarget = @import(\"webapi/net/XMLHttpRequestEventTarget.zig\");\nconst Blob = @import(\"webapi/Blob.zig\");\nconst AbstractRange = @import(\"webapi/AbstractRange.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst IS_DEBUG = builtin.mode == .Debug;\nconst assert = std.debug.assert;\n\n// Shared across all frames of a Page.\nconst Factory = @This();\n\n_arena: Allocator,\n_slab: SlabAllocator,\n\npub fn init(arena: Allocator) Factory {\n    return .{\n        ._arena = arena,\n        ._slab = SlabAllocator.init(arena, 128),\n    };\n}\n\n// this is a root object\npub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {\n    return self.eventTargetWithAllocator(self._slab.allocator(), child);\n}\n\npub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {\n    const chain = try PrototypeChain(\n        &.{ EventTarget, @TypeOf(child) },\n    ).allocate(allocator);\n\n    const event_ptr = chain.get(0);\n    event_ptr.* = .{\n        ._type = unionInit(EventTarget.Type, chain.get(1)),\n    };\n    chain.setLeaf(1, child);\n\n    return chain.get(1);\n}\n\npub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {\n    const allocator = self._slab.allocator();\n    const et = try allocator.create(EventTarget);\n    et.* = .{ ._type = unionInit(EventTarget.Type, child) };\n    return et;\n}\n\n// this is a root object\npub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {\n    const chain = try PrototypeChain(\n        &.{ Event, @TypeOf(child) },\n    ).allocate(arena);\n\n    // Special case: Event has a _type_string field, so we need manual setup\n    const event_ptr = chain.get(0);\n    event_ptr.* = try eventInit(arena, typ, chain.get(1));\n    chain.setLeaf(1, child);\n\n    return chain.get(1);\n}\n\npub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {\n    const chain = try PrototypeChain(\n        &.{ Event, UIEvent, @TypeOf(child) },\n    ).allocate(arena);\n\n    // Special case: Event has a _type_string field, so we need manual setup\n    const event_ptr = chain.get(0);\n    event_ptr.* = try eventInit(arena, typ, chain.get(1));\n    chain.setMiddle(1, UIEvent.Type);\n    chain.setLeaf(2, child);\n\n    return chain.get(2);\n}\n\npub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {\n    const chain = try PrototypeChain(\n        &.{ Event, UIEvent, MouseEvent, @TypeOf(child) },\n    ).allocate(arena);\n\n    // Special case: Event has a _type_string field, so we need manual setup\n    const event_ptr = chain.get(0);\n    event_ptr.* = try eventInit(arena, typ, chain.get(1));\n    chain.setMiddle(1, UIEvent.Type);\n\n    // Set MouseEvent with all its fields\n    const mouse_ptr = chain.get(2);\n    mouse_ptr.* = mouse;\n    mouse_ptr._proto = chain.get(1);\n    mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));\n\n    chain.setLeaf(3, child);\n\n    return chain.get(3);\n}\n\nfn PrototypeChain(comptime types: []const type) type {\n    return struct {\n        const Self = @This();\n        memory: []u8,\n\n        fn totalSize() usize {\n            var size: usize = 0;\n            for (types) |T| {\n                size = std.mem.alignForward(usize, size, @alignOf(T));\n                size += @sizeOf(T);\n            }\n            return size;\n        }\n\n        fn maxAlign() std.mem.Alignment {\n            var alignment: std.mem.Alignment = .@\"1\";\n\n            for (types) |T| {\n                alignment = std.mem.Alignment.max(alignment, std.mem.Alignment.of(T));\n            }\n\n            return alignment;\n        }\n\n        fn getType(comptime index: usize) type {\n            return types[index];\n        }\n\n        fn allocate(allocator: std.mem.Allocator) !Self {\n            const size = comptime Self.totalSize();\n            const alignment = comptime Self.maxAlign();\n\n            const memory = try allocator.alignedAlloc(u8, alignment, size);\n            return .{ .memory = memory };\n        }\n\n        fn get(self: *const Self, comptime index: usize) *getType(index) {\n            var offset: usize = 0;\n            inline for (types, 0..) |T, i| {\n                offset = std.mem.alignForward(usize, offset, @alignOf(T));\n\n                if (i == index) {\n                    return @as(*T, @ptrCast(@alignCast(self.memory.ptr + offset)));\n                }\n                offset += @sizeOf(T);\n            }\n            unreachable;\n        }\n\n        fn set(self: *const Self, comptime index: usize, value: getType(index)) void {\n            const ptr = self.get(index);\n            ptr.* = value;\n        }\n\n        fn setRoot(self: *const Self, comptime T: type) void {\n            const ptr = self.get(0);\n            ptr.* = .{ ._type = unionInit(T, self.get(1)) };\n        }\n\n        fn setMiddle(self: *const Self, comptime index: usize, comptime T: type) void {\n            assert(index >= 1);\n            assert(index < types.len);\n\n            const ptr = self.get(index);\n            ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, self.get(index + 1)) };\n        }\n\n        fn setMiddleWithValue(self: *const Self, comptime index: usize, comptime T: type, value: anytype) void {\n            assert(index >= 1);\n\n            const ptr = self.get(index);\n            ptr.* = .{ ._proto = self.get(index - 1), ._type = unionInit(T, value) };\n        }\n\n        fn setLeaf(self: *const Self, comptime index: usize, value: anytype) void {\n            assert(index >= 1);\n\n            const ptr = self.get(index);\n            ptr.* = value;\n            ptr._proto = self.get(index - 1);\n        }\n    };\n}\n\nfn AutoPrototypeChain(comptime types: []const type) type {\n    return struct {\n        fn create(allocator: std.mem.Allocator, leaf_value: anytype) !*@TypeOf(leaf_value) {\n            const chain = try PrototypeChain(types).allocate(allocator);\n\n            const RootType = types[0];\n            chain.setRoot(RootType.Type);\n\n            inline for (1..types.len - 1) |i| {\n                const MiddleType = types[i];\n                chain.setMiddle(i, MiddleType.Type);\n            }\n\n            chain.setLeaf(types.len - 1, leaf_value);\n            return chain.get(types.len - 1);\n        }\n    };\n}\n\nfn eventInit(arena: Allocator, typ: String, value: anytype) !Event {\n    // Round to 2ms for privacy (browsers do this)\n    const raw_timestamp = @import(\"../datetime.zig\").milliTimestamp(.monotonic);\n    const time_stamp = (raw_timestamp / 2) * 2;\n\n    return .{\n        ._rc = 0,\n        ._arena = arena,\n        ._type = unionInit(Event.Type, value),\n        ._type_string = typ,\n        ._time_stamp = time_stamp,\n    };\n}\n\npub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child) {\n    // Special case: Blob has slice and mime fields, so we need manual setup\n    const chain = try PrototypeChain(\n        &.{ Blob, @TypeOf(child) },\n    ).allocate(arena);\n\n    const blob_ptr = chain.get(0);\n    blob_ptr.* = .{\n        ._arena = arena,\n        ._type = unionInit(Blob.Type, chain.get(1)),\n        ._slice = \"\",\n        ._mime = \"\",\n    };\n    chain.setLeaf(1, child);\n\n    return chain.get(1);\n}\n\npub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) {\n    const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena);\n\n    const doc = page.document.asNode();\n    const abstract_range = chain.get(0);\n    abstract_range.* = AbstractRange{\n        ._rc = 0,\n        ._arena = arena,\n        ._page_id = page.id,\n        ._type = unionInit(AbstractRange.Type, chain.get(1)),\n        ._end_offset = 0,\n        ._start_offset = 0,\n        ._end_container = doc,\n        ._start_container = doc,\n    };\n    chain.setLeaf(1, child);\n    page._live_ranges.append(&abstract_range._range_link);\n    return chain.get(1);\n}\n\npub fn node(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    return try AutoPrototypeChain(\n        &.{ EventTarget, Node, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    return try AutoPrototypeChain(\n        &.{ EventTarget, Node, Document, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn documentFragment(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    return try AutoPrototypeChain(\n        &.{ EventTarget, Node, Node.DocumentFragment, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    return try AutoPrototypeChain(\n        &.{ EventTarget, Node, Element, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    return try AutoPrototypeChain(\n        &.{ EventTarget, Node, Element, Element.Html, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn htmlMediaElement(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    return try AutoPrototypeChain(\n        &.{ EventTarget, Node, Element, Element.Html, Element.Html.Media, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    const ChildT = @TypeOf(child);\n\n    if (ChildT == Element.Svg) {\n        return self.element(child);\n    }\n\n    const chain = try PrototypeChain(\n        &.{ EventTarget, Node, Element, Element.Svg, ChildT },\n    ).allocate(allocator);\n\n    chain.setRoot(EventTarget.Type);\n    chain.setMiddle(1, Node.Type);\n    chain.setMiddle(2, Element.Type);\n\n    // will never allocate, can't fail\n    const tag_name_str = String.init(self._arena, tag_name, .{}) catch unreachable;\n\n    // Manually set Element.Svg with the tag_name\n    chain.set(3, .{\n        ._proto = chain.get(2),\n        ._tag_name = tag_name_str,\n        ._type = unionInit(Element.Svg.Type, chain.get(4)),\n    });\n\n    chain.setLeaf(4, child);\n    return chain.get(4);\n}\n\npub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {\n    return try AutoPrototypeChain(\n        &.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {\n    const allocator = self._slab.allocator();\n    const TextTrackCue = @import(\"webapi/media/TextTrackCue.zig\");\n\n    return try AutoPrototypeChain(\n        &.{ EventTarget, TextTrackCue, @TypeOf(child) },\n    ).create(allocator, child);\n}\n\npub fn destroy(self: *Factory, value: anytype) void {\n    const S = reflect.Struct(@TypeOf(value));\n\n    if (comptime IS_DEBUG) {\n        // We should always destroy from the leaf down.\n        if (@hasDecl(S, \"_prototype_root\")) {\n            // A Event{._type == .generic} (or any other similar types)\n            // _should_ be destoyed directly. The _type = .generic is a pseudo\n            // child\n            if (S != Event or value._type != .generic) {\n                log.fatal(.bug, \"factory.destroy.event\", .{ .type = @typeName(S) });\n                unreachable;\n            }\n        }\n    }\n\n    if (comptime @hasField(S, \"_proto\")) {\n        self.destroyChain(value, 0, std.mem.Alignment.@\"1\");\n    } else {\n        self.destroyStandalone(value);\n    }\n}\n\npub fn destroyStandalone(self: *Factory, value: anytype) void {\n    const allocator = self._slab.allocator();\n    allocator.destroy(value);\n}\n\nfn destroyChain(\n    self: *Factory,\n    value: anytype,\n    old_size: usize,\n    old_align: std.mem.Alignment,\n) void {\n    const S = reflect.Struct(@TypeOf(value));\n    const allocator = self._slab.allocator();\n\n    // aligns the old size to the alignment of this element\n    const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));\n    const new_size = current_size + @sizeOf(S);\n    const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));\n\n    if (@hasField(S, \"_proto\")) {\n        self.destroyChain(value._proto, new_size, new_align);\n    } else {\n        // no proto so this is the head of the chain.\n        // we use this as the ptr to the start of the chain.\n        // and we have summed up the length.\n        assert(@hasDecl(S, \"_prototype_root\"));\n\n        const memory_ptr: [*]u8 = @ptrCast(@constCast(value));\n        const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());\n        allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());\n    }\n}\n\npub fn createT(self: *Factory, comptime T: type) !*T {\n    const allocator = self._slab.allocator();\n    return try allocator.create(T);\n}\n\npub fn create(self: *Factory, value: anytype) !*@TypeOf(value) {\n    const ptr = try self.createT(@TypeOf(value));\n    ptr.* = value;\n    return ptr;\n}\n\nfn unionInit(comptime T: type, value: anytype) T {\n    const V = @TypeOf(value);\n    const field_name = comptime unionFieldName(T, V);\n    return @unionInit(T, field_name, value);\n}\n\n// There can be friction between comptime and runtime. Comptime has to\n// account for all possible types, even if some runtime flow makes certain\n// cases impossible. At runtime, we always call `unionFieldName` with the\n// correct struct or pointer type. But at comptime time, `unionFieldName`\n// is called with both variants (S and *S). So we use reflect.Struct().\n// This only works because we never have a union with a field S and another\n// field *S.\nfn unionFieldName(comptime T: type, comptime V: type) []const u8 {\n    inline for (@typeInfo(T).@\"union\".fields) |field| {\n        if (reflect.Struct(field.type) == reflect.Struct(V)) {\n            return field.name;\n        }\n    }\n    @compileError(@typeName(V) ++ \" is not a valid type for \" ++ @typeName(T) ++ \".type\");\n}\n"
  },
  {
    "path": "src/browser/HttpClient.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst posix = std.posix;\n\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"../log.zig\");\nconst Net = @import(\"../network/http.zig\");\nconst Network = @import(\"../network/Runtime.zig\");\nconst Config = @import(\"../Config.zig\");\nconst URL = @import(\"../browser/URL.zig\");\nconst Notification = @import(\"../Notification.zig\");\nconst CookieJar = @import(\"../browser/webapi/storage/Cookie.zig\").Jar;\nconst Robots = @import(\"../network/Robots.zig\");\nconst RobotStore = Robots.RobotStore;\nconst WebBotAuth = @import(\"../network/WebBotAuth.zig\");\n\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\npub const Method = Net.Method;\npub const Headers = Net.Headers;\npub const ResponseHead = Net.ResponseHead;\npub const HeaderIterator = Net.HeaderIterator;\n\n// This is loosely tied to a browser Page. Loading all the <scripts>, doing\n// XHR requests, and loading imports all happens through here. Sine the app\n// currently supports 1 browser and 1 page at-a-time, we only have 1 Client and\n// re-use it from page to page. This allows us better re-use of the various\n// buffers/caches (including keepalive connections) that libcurl has.\n//\n// The app has other secondary http needs, like telemetry. While we want to\n// share some things (namely the ca blob, and maybe some configuration\n// (TODO: ??? should proxy settings be global ???)), we're able to do call\n// client.abort() to abort the transfers being made by a page, without impacting\n// those other http requests.\npub const Client = @This();\n\n// Count of active requests\nactive: usize,\n\n// Count of intercepted requests. This is to help deal with intercepted requests.\n// The client doesn't track intercepted transfers. If a request is intercepted,\n// the client forgets about it and requires the interceptor to continue or abort\n// it. That works well, except if we only rely on active, we might think there's\n// no more network activity when, with interecepted requests, there might be more\n// in the future. (We really only need this to properly emit a 'networkIdle' and\n// 'networkAlmostIdle' Page.lifecycleEvent in CDP).\nintercepted: usize,\n\n// Our curl multi handle.\nhandles: Net.Handles,\n\n// Connections currently in this client's curl_multi.\nin_use: std.DoublyLinkedList = .{},\n\n// Connections that failed to be removed from curl_multi during perform.\ndirty: std.DoublyLinkedList = .{},\n\n// Whether we're currently inside a curl_multi_perform call.\nperforming: bool = false,\n\n// Use to generate the next request ID\nnext_request_id: u32 = 0,\n\n// When handles has no more available easys, requests get queued.\nqueue: TransferQueue,\n\n// The main app allocator\nallocator: Allocator,\n\nnetwork: *Network,\n// Queue of requests that depend on a robots.txt.\n// Allows us to fetch the robots.txt just once.\npending_robots_queue: std.StringHashMapUnmanaged(std.ArrayList(Request)) = .empty,\n\n// Once we have a handle/easy to process a request with, we create a Transfer\n// which contains the Request as well as any state we need to process the\n// request. These wil come and go with each request.\ntransfer_pool: std.heap.MemoryPool(Transfer),\n\n// The current proxy. CDP can change it, restoreOriginalProxy restores\n// from config.\nhttp_proxy: ?[:0]const u8 = null,\n\n// track if the client use a proxy for connections.\n// We can't use http_proxy because we want also to track proxy configured via\n// CDP.\nuse_proxy: bool,\n\n// Current TLS verification state, applied per-connection in makeRequest.\ntls_verify: bool = true,\n\nobey_robots: bool,\n\ncdp_client: ?CDPClient = null,\n\n// libcurl can monitor arbitrary sockets, this lets us use libcurl to poll\n// both HTTP data as well as messages from an CDP connection.\n// Furthermore, we have some tension between blocking scripts and request\n// interception. For non-blocking scripts, because nothing blocks, we can\n// just queue the scripts until we receive a response to the interception\n// notification. But for blocking scripts (which block the parser), it's hard\n// to return control back to the CDP loop. So the `read` function pointer is\n// used by the Client to have the CDP client read more data from the socket,\n// specifically when we're waiting for a request interception response to\n// a blocking script.\npub const CDPClient = struct {\n    socket: posix.socket_t,\n    ctx: *anyopaque,\n    blocking_read_start: *const fn (*anyopaque) bool,\n    blocking_read: *const fn (*anyopaque) bool,\n    blocking_read_end: *const fn (*anyopaque) bool,\n};\n\nconst TransferQueue = std.DoublyLinkedList;\n\npub fn init(allocator: Allocator, network: *Network) !*Client {\n    var transfer_pool = std.heap.MemoryPool(Transfer).init(allocator);\n    errdefer transfer_pool.deinit();\n\n    const client = try allocator.create(Client);\n    errdefer allocator.destroy(client);\n\n    var handles = try Net.Handles.init(network.config);\n    errdefer handles.deinit();\n\n    const http_proxy = network.config.httpProxy();\n\n    client.* = .{\n        .queue = .{},\n        .active = 0,\n        .intercepted = 0,\n        .handles = handles,\n        .allocator = allocator,\n        .network = network,\n        .http_proxy = http_proxy,\n        .use_proxy = http_proxy != null,\n        .tls_verify = network.config.tlsVerifyHost(),\n        .obey_robots = network.config.obeyRobots(),\n        .transfer_pool = transfer_pool,\n    };\n\n    return client;\n}\n\npub fn deinit(self: *Client) void {\n    self.abort();\n    self.handles.deinit();\n\n    self.transfer_pool.deinit();\n\n    var robots_iter = self.pending_robots_queue.iterator();\n    while (robots_iter.next()) |entry| {\n        entry.value_ptr.deinit(self.allocator);\n    }\n    self.pending_robots_queue.deinit(self.allocator);\n\n    self.allocator.destroy(self);\n}\n\npub fn newHeaders(self: *const Client) !Net.Headers {\n    return Net.Headers.init(self.network.config.http_headers.user_agent_header);\n}\n\npub fn abort(self: *Client) void {\n    self._abort(true, 0);\n}\n\npub fn abortFrame(self: *Client, frame_id: u32) void {\n    self._abort(false, frame_id);\n}\n\n// Written this way so that both abort and abortFrame can share the same code\n// but abort can avoid the frame_id check at comptime.\nfn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {\n    {\n        var q = &self.in_use;\n        var n = q.first;\n        while (n) |node| {\n            n = node.next;\n            const conn: *Net.Connection = @fieldParentPtr(\"node\", node);\n            var transfer = Transfer.fromConnection(conn) catch |err| {\n                // Let's cleanup what we can\n                self.removeConn(conn);\n                log.err(.http, \"get private info\", .{ .err = err, .source = \"abort\" });\n                continue;\n            };\n            if (comptime abort_all) {\n                transfer.kill();\n            } else if (transfer.req.frame_id == frame_id) {\n                q.remove(node);\n                transfer.kill();\n            }\n        }\n    }\n\n    if (comptime IS_DEBUG and abort_all) {\n        std.debug.assert(self.active == 0);\n    }\n\n    {\n        var q = &self.queue;\n        var n = q.first;\n        while (n) |node| {\n            n = node.next;\n            const transfer: *Transfer = @fieldParentPtr(\"_node\", node);\n            if (comptime abort_all) {\n                transfer.kill();\n            } else if (transfer.req.frame_id == frame_id) {\n                q.remove(node);\n                transfer.kill();\n            }\n        }\n    }\n\n    if (comptime abort_all) {\n        self.queue = .{};\n    }\n\n    if (comptime IS_DEBUG and abort_all) {\n        std.debug.assert(self.in_use.first == null);\n\n        const running = self.handles.perform() catch |err| {\n            lp.assert(false, \"multi perform in abort\", .{ .err = err });\n        };\n        std.debug.assert(running == 0);\n    }\n}\n\npub fn tick(self: *Client, timeout_ms: u32) !PerformStatus {\n    while (self.queue.popFirst()) |queue_node| {\n        const conn = self.network.getConnection() orelse {\n            self.queue.prepend(queue_node);\n            break;\n        };\n        const transfer: *Transfer = @fieldParentPtr(\"_node\", queue_node);\n        try self.makeRequest(conn, transfer);\n    }\n    return self.perform(@intCast(timeout_ms));\n}\n\npub fn request(self: *Client, req: Request) !void {\n    if (self.obey_robots == false) {\n        return self.processRequest(req);\n    }\n\n    const robots_url = try URL.getRobotsUrl(self.allocator, req.url);\n    errdefer self.allocator.free(robots_url);\n\n    // If we have this robots cached, we can take a fast path.\n    if (self.network.robot_store.get(robots_url)) |robot_entry| {\n        defer self.allocator.free(robots_url);\n\n        switch (robot_entry) {\n            // If we have a found robots entry, we check it.\n            .present => |robots| {\n                const path = URL.getPathname(req.url);\n                if (!robots.isAllowed(path)) {\n                    req.error_callback(req.ctx, error.RobotsBlocked);\n                    return;\n                }\n            },\n            // Otherwise, we assume we won't find it again.\n            .absent => {},\n        }\n\n        return self.processRequest(req);\n    }\n    return self.fetchRobotsThenProcessRequest(robots_url, req);\n}\n\nfn processRequest(self: *Client, req: Request) !void {\n    const transfer = try self.makeTransfer(req);\n\n    transfer.req.notification.dispatch(.http_request_start, &.{ .transfer = transfer });\n\n    var wait_for_interception = false;\n    transfer.req.notification.dispatch(.http_request_intercept, &.{\n        .transfer = transfer,\n        .wait_for_interception = &wait_for_interception,\n    });\n    if (wait_for_interception == false) {\n        // request not intercepted, process it normally\n        return self.process(transfer);\n    }\n\n    self.intercepted += 1;\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"wait for interception\", .{ .intercepted = self.intercepted });\n    }\n    transfer._intercept_state = .pending;\n\n    if (req.blocking == false) {\n        // The request was interecepted, but it isn't a blocking request, so we\n        // dont' need to block this call. The request will be unblocked\n        // asynchronously via either continueTransfer or abortTransfer\n        return;\n    }\n\n    if (try self.waitForInterceptedResponse(transfer)) {\n        return self.process(transfer);\n    }\n}\n\nconst RobotsRequestContext = struct {\n    client: *Client,\n    req: Request,\n    robots_url: [:0]const u8,\n    buffer: std.ArrayList(u8),\n    status: u16 = 0,\n\n    pub fn deinit(self: *RobotsRequestContext) void {\n        self.client.allocator.free(self.robots_url);\n        self.buffer.deinit(self.client.allocator);\n        self.client.allocator.destroy(self);\n    }\n};\n\nfn fetchRobotsThenProcessRequest(self: *Client, robots_url: [:0]const u8, req: Request) !void {\n    const entry = try self.pending_robots_queue.getOrPut(self.allocator, robots_url);\n\n    if (!entry.found_existing) {\n        errdefer self.allocator.free(robots_url);\n\n        // If we aren't already fetching this robots,\n        // we want to create a new queue for it and add this request into it.\n        entry.value_ptr.* = .empty;\n\n        const ctx = try self.allocator.create(RobotsRequestContext);\n        errdefer self.allocator.destroy(ctx);\n        ctx.* = .{ .client = self, .req = req, .robots_url = robots_url, .buffer = .empty };\n        const headers = try self.newHeaders();\n\n        log.debug(.browser, \"fetching robots.txt\", .{ .robots_url = robots_url });\n        try self.processRequest(.{\n            .ctx = ctx,\n            .url = robots_url,\n            .method = .GET,\n            .headers = headers,\n            .blocking = false,\n            .frame_id = req.frame_id,\n            .cookie_jar = req.cookie_jar,\n            .notification = req.notification,\n            .resource_type = .fetch,\n            .header_callback = robotsHeaderCallback,\n            .data_callback = robotsDataCallback,\n            .done_callback = robotsDoneCallback,\n            .error_callback = robotsErrorCallback,\n            .shutdown_callback = robotsShutdownCallback,\n        });\n    } else {\n        // Not using our own robots URL, only using the one from the first request.\n        self.allocator.free(robots_url);\n    }\n\n    try entry.value_ptr.append(self.allocator, req);\n}\n\nfn robotsHeaderCallback(transfer: *Transfer) !bool {\n    const ctx: *RobotsRequestContext = @ptrCast(@alignCast(transfer.ctx));\n\n    if (transfer.response_header) |hdr| {\n        log.debug(.browser, \"robots status\", .{ .status = hdr.status, .robots_url = ctx.robots_url });\n        ctx.status = hdr.status;\n    }\n\n    if (transfer.getContentLength()) |cl| {\n        try ctx.buffer.ensureTotalCapacity(ctx.client.allocator, cl);\n    }\n\n    return true;\n}\n\nfn robotsDataCallback(transfer: *Transfer, data: []const u8) !void {\n    const ctx: *RobotsRequestContext = @ptrCast(@alignCast(transfer.ctx));\n    try ctx.buffer.appendSlice(ctx.client.allocator, data);\n}\n\nfn robotsDoneCallback(ctx_ptr: *anyopaque) !void {\n    const ctx: *RobotsRequestContext = @ptrCast(@alignCast(ctx_ptr));\n    defer ctx.deinit();\n\n    var allowed = true;\n\n    switch (ctx.status) {\n        200 => {\n            if (ctx.buffer.items.len > 0) {\n                const robots: ?Robots = ctx.client.network.robot_store.robotsFromBytes(\n                    ctx.client.network.config.http_headers.user_agent,\n                    ctx.buffer.items,\n                ) catch blk: {\n                    log.warn(.browser, \"failed to parse robots\", .{ .robots_url = ctx.robots_url });\n                    // If we fail to parse, we just insert it as absent and ignore.\n                    try ctx.client.network.robot_store.putAbsent(ctx.robots_url);\n                    break :blk null;\n                };\n\n                if (robots) |r| {\n                    try ctx.client.network.robot_store.put(ctx.robots_url, r);\n                    const path = URL.getPathname(ctx.req.url);\n                    allowed = r.isAllowed(path);\n                }\n            }\n        },\n        404 => {\n            log.debug(.http, \"robots not found\", .{ .url = ctx.robots_url });\n            // If we get a 404, we just insert it as absent.\n            try ctx.client.network.robot_store.putAbsent(ctx.robots_url);\n        },\n        else => {\n            log.debug(.http, \"unexpected status on robots\", .{ .url = ctx.robots_url, .status = ctx.status });\n            // If we get an unexpected status, we just insert as absent.\n            try ctx.client.network.robot_store.putAbsent(ctx.robots_url);\n        },\n    }\n\n    var queued = ctx.client.pending_robots_queue.fetchRemove(\n        ctx.robots_url,\n    ) orelse @panic(\"Client.robotsDoneCallbacke empty queue\");\n    defer queued.value.deinit(ctx.client.allocator);\n\n    for (queued.value.items) |queued_req| {\n        if (!allowed) {\n            log.warn(.http, \"blocked by robots\", .{ .url = queued_req.url });\n            queued_req.error_callback(queued_req.ctx, error.RobotsBlocked);\n        } else {\n            ctx.client.processRequest(queued_req) catch |e| {\n                queued_req.error_callback(queued_req.ctx, e);\n            };\n        }\n    }\n}\n\nfn robotsErrorCallback(ctx_ptr: *anyopaque, err: anyerror) void {\n    const ctx: *RobotsRequestContext = @ptrCast(@alignCast(ctx_ptr));\n    defer ctx.deinit();\n\n    log.warn(.http, \"robots fetch failed\", .{ .err = err });\n\n    var queued = ctx.client.pending_robots_queue.fetchRemove(\n        ctx.robots_url,\n    ) orelse @panic(\"Client.robotsErrorCallback empty queue\");\n    defer queued.value.deinit(ctx.client.allocator);\n\n    // On error, allow all queued requests to proceed\n    for (queued.value.items) |queued_req| {\n        ctx.client.processRequest(queued_req) catch |e| {\n            queued_req.error_callback(queued_req.ctx, e);\n        };\n    }\n}\n\nfn robotsShutdownCallback(ctx_ptr: *anyopaque) void {\n    const ctx: *RobotsRequestContext = @ptrCast(@alignCast(ctx_ptr));\n    defer ctx.deinit();\n\n    log.debug(.http, \"robots fetch shutdown\", .{});\n\n    var queued = ctx.client.pending_robots_queue.fetchRemove(\n        ctx.robots_url,\n    ) orelse @panic(\"Client.robotsErrorCallback empty queue\");\n    defer queued.value.deinit(ctx.client.allocator);\n\n    for (queued.value.items) |queued_req| {\n        if (queued_req.shutdown_callback) |shutdown_cb| {\n            shutdown_cb(queued_req.ctx);\n        }\n    }\n}\n\nfn waitForInterceptedResponse(self: *Client, transfer: *Transfer) !bool {\n    // The request was intercepted and is blocking. This is messy, but our\n    // callers, the ScriptManager -> Page, don't have a great way to stop the\n    // parser and return control to the CDP server to wait for the interception\n    // response. We have some information on the CDPClient, so we'll do the\n    // blocking here. (This is a bit of a legacy thing. Initially the Client\n    // had a 'extra_socket' that it could monitor. It was named 'extra_socket'\n    // to appear generic, but really, that 'extra_socket' was always the CDP\n    // socket. Because we already had the \"extra_socket\" here, it was easier to\n    // make it even more CDP- aware and turn `extra_socket: socket_t` into the\n    // current CDPClient and do the blocking here).\n    const cdp_client = self.cdp_client.?;\n    const ctx = cdp_client.ctx;\n\n    if (cdp_client.blocking_read_start(ctx) == false) {\n        return error.BlockingInterceptFailure;\n    }\n\n    defer _ = cdp_client.blocking_read_end(ctx);\n\n    while (true) {\n        if (cdp_client.blocking_read(ctx) == false) {\n            return error.BlockingInterceptFailure;\n        }\n\n        switch (transfer._intercept_state) {\n            .pending => continue, // keep waiting\n            .@\"continue\" => return true,\n            .abort => |err| {\n                transfer.abort(err);\n                return false;\n            },\n            .fulfilled => {\n                // callbacks already called, just need to cleanups\n                transfer.deinit();\n                return false;\n            },\n            .not_intercepted => unreachable,\n        }\n    }\n}\n\n// Above, request will not process if there's an interception request. In such\n// cases, the interecptor is expected to call resume to continue the transfer\n// or transfer.abort() to abort it.\nfn process(self: *Client, transfer: *Transfer) !void {\n    // libcurl doesn't allow recursive calls, if we're in a `perform()` operation\n    // then we _have_ to queue this.\n    if (self.performing == false) {\n        if (self.network.getConnection()) |conn| {\n            return self.makeRequest(conn, transfer);\n        }\n    }\n\n    self.queue.append(&transfer._node);\n}\n\n// For an intercepted request\npub fn continueTransfer(self: *Client, transfer: *Transfer) !void {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(transfer._intercept_state != .not_intercepted);\n        log.debug(.http, \"continue transfer\", .{ .intercepted = self.intercepted });\n    }\n    self.intercepted -= 1;\n\n    if (!transfer.req.blocking) {\n        return self.process(transfer);\n    }\n    transfer._intercept_state = .@\"continue\";\n}\n\n// For an intercepted request\npub fn abortTransfer(self: *Client, transfer: *Transfer) void {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(transfer._intercept_state != .not_intercepted);\n        log.debug(.http, \"abort transfer\", .{ .intercepted = self.intercepted });\n    }\n    self.intercepted -= 1;\n\n    if (!transfer.req.blocking) {\n        transfer.abort(error.Abort);\n    }\n    transfer._intercept_state = .{ .abort = error.Abort };\n}\n\n// For an intercepted request\npub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers: []const Net.Header, body: ?[]const u8) !void {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(transfer._intercept_state != .not_intercepted);\n        log.debug(.http, \"filfull transfer\", .{ .intercepted = self.intercepted });\n    }\n    self.intercepted -= 1;\n\n    try transfer.fulfill(status, headers, body);\n    if (!transfer.req.blocking) {\n        transfer.deinit();\n        return;\n    }\n    transfer._intercept_state = .fulfilled;\n}\n\npub fn nextReqId(self: *Client) u32 {\n    return self.next_request_id +% 1;\n}\n\npub fn incrReqId(self: *Client) u32 {\n    const id = self.next_request_id +% 1;\n    self.next_request_id = id;\n    return id;\n}\n\nfn makeTransfer(self: *Client, req: Request) !*Transfer {\n    errdefer req.headers.deinit();\n\n    const transfer = try self.transfer_pool.create();\n    errdefer self.transfer_pool.destroy(transfer);\n\n    const id = self.incrReqId();\n    transfer.* = .{\n        .arena = ArenaAllocator.init(self.allocator),\n        .id = id,\n        .url = req.url,\n        .req = req,\n        .ctx = req.ctx,\n        .client = self,\n        .max_response_size = self.network.config.httpMaxResponseSize(),\n    };\n    return transfer;\n}\n\nfn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback: bool) void {\n    if (transfer._notified_fail) {\n        // we can force a failed request within a callback, which will eventually\n        // result in this being called again in the more general loop. We do this\n        // because we can raise a more specific error inside a callback in some cases\n        return;\n    }\n\n    transfer._notified_fail = true;\n\n    transfer.req.notification.dispatch(.http_request_fail, &.{\n        .transfer = transfer,\n        .err = err,\n    });\n\n    if (execute_callback) {\n        transfer.req.error_callback(transfer.ctx, err);\n    } else if (transfer.req.shutdown_callback) |cb| {\n        cb(transfer.ctx);\n    }\n}\n\n// Restrictive since it'll only work if there are no inflight requests. In some\n// cases, the libcurl documentation is clear that changing settings while a\n// connection is inflight is undefined. It doesn't say anything about CURLOPT_PROXY,\n// but better to be safe than sorry.\n// For now, this restriction is ok, since it's only called by CDP on\n// createBrowserContext, at which point, if we do have an active connection,\n// that's probably a bug (a previous abort failed?). But if we need to call this\n// at any point in time, it could be worth digging into libcurl to see if this\n// can be changed at any point in the easy's lifecycle.\npub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {\n    try self.ensureNoActiveConnection();\n    self.http_proxy = proxy;\n    self.use_proxy = true;\n}\n\n// Same restriction as changeProxy. Should be ok since this is only called on\n// BrowserContext deinit.\npub fn restoreOriginalProxy(self: *Client) !void {\n    try self.ensureNoActiveConnection();\n\n    self.http_proxy = self.network.config.httpProxy();\n    self.use_proxy = self.http_proxy != null;\n}\n\n// Enable TLS verification on all connections.\npub fn setTlsVerify(self: *Client, verify: bool) !void {\n    // Remove inflight connections check on enable TLS b/c chromiumoxide calls\n    // the command during navigate and Curl seems to accept it...\n\n    var it = self.in_use.first;\n    while (it) |node| : (it = node.next) {\n        const conn: *Net.Connection = @fieldParentPtr(\"node\", node);\n        try conn.setTlsVerify(verify, self.use_proxy);\n    }\n    self.tls_verify = verify;\n}\n\nfn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerror!void {\n    const req = &transfer.req;\n\n    {\n        transfer._conn = conn;\n        errdefer {\n            transfer._conn = null;\n            transfer.deinit();\n            self.releaseConn(conn);\n        }\n\n        // Set callbacks and per-client settings on the pooled connection.\n        try conn.setCallbacks(Transfer.headerCallback, Transfer.dataCallback);\n        try conn.setProxy(self.http_proxy);\n        try conn.setTlsVerify(self.tls_verify, self.use_proxy);\n\n        try conn.setURL(req.url);\n        try conn.setMethod(req.method);\n        if (req.body) |b| {\n            try conn.setBody(b);\n        } else {\n            try conn.setGetMode();\n        }\n\n        var header_list = req.headers;\n        try conn.secretHeaders(&header_list, &self.network.config.http_headers); // Add headers that must be hidden from intercepts\n        try conn.setHeaders(&header_list);\n\n        // If we have WebBotAuth, sign our request.\n        if (self.network.web_bot_auth) |*wba| {\n            const authority = URL.getHost(req.url);\n            try wba.signRequest(transfer.arena.allocator(), &header_list, authority);\n        }\n\n        // Add cookies.\n        if (header_list.cookies) |cookies| {\n            try conn.setCookies(cookies);\n        }\n\n        try conn.setPrivate(transfer);\n\n        // add credentials\n        if (req.credentials) |creds| {\n            if (transfer._auth_challenge != null and transfer._auth_challenge.?.source == .proxy) {\n                try conn.setProxyCredentials(creds);\n            } else {\n                try conn.setCredentials(creds);\n            }\n        }\n    }\n\n    // As soon as this is called, our \"perform\" loop is responsible for\n    // cleaning things up. That's why the above code is in a block. If anything\n    // fails BEFORE `curl_multi_add_handle` succeeds, the we still need to do\n    // cleanup. But if things fail after `curl_multi_add_handle`, we expect\n    // perfom to pickup the failure and cleanup.\n    self.in_use.append(&conn.node);\n    self.handles.add(conn) catch |err| {\n        transfer._conn = null;\n        transfer.deinit();\n        self.in_use.remove(&conn.node);\n        self.releaseConn(conn);\n        return err;\n    };\n\n    if (req.start_callback) |cb| {\n        cb(transfer) catch |err| {\n            transfer.deinit();\n            return err;\n        };\n    }\n\n    self.active += 1;\n    _ = try self.perform(0);\n}\n\npub const PerformStatus = enum {\n    cdp_socket,\n    normal,\n};\n\nfn perform(self: *Client, timeout_ms: c_int) !PerformStatus {\n    const running = blk: {\n        self.performing = true;\n        defer self.performing = false;\n\n        break :blk try self.handles.perform();\n    };\n\n    // Process dirty connections — return them to Runtime pool.\n    while (self.dirty.popFirst()) |node| {\n        const conn: *Net.Connection = @fieldParentPtr(\"node\", node);\n        self.handles.remove(conn) catch |err| {\n            log.fatal(.http, \"multi remove handle\", .{ .err = err, .src = \"perform\" });\n            @panic(\"multi_remove_handle\");\n        };\n        self.releaseConn(conn);\n    }\n\n    // We're potentially going to block for a while until we get data. Process\n    // whatever messages we have waiting ahead of time.\n    if (try self.processMessages()) {\n        return .normal;\n    }\n\n    var status = PerformStatus.normal;\n    if (self.cdp_client) |cdp_client| {\n        var wait_fds = [_]Net.WaitFd{.{\n            .fd = cdp_client.socket,\n            .events = .{ .pollin = true },\n            .revents = .{},\n        }};\n        try self.handles.poll(&wait_fds, timeout_ms);\n        if (wait_fds[0].revents.pollin or wait_fds[0].revents.pollpri or wait_fds[0].revents.pollout) {\n            status = .cdp_socket;\n        }\n    } else if (running > 0) {\n        try self.handles.poll(&.{}, timeout_ms);\n    }\n\n    _ = try self.processMessages();\n    return status;\n}\n\nfn processMessages(self: *Client) !bool {\n    var processed = false;\n    while (self.handles.readMessage()) |msg| {\n        const transfer = try Transfer.fromConnection(&msg.conn);\n\n        // In case of auth challenge\n        // TODO give a way to configure the number of auth retries.\n        if (transfer._auth_challenge != null and transfer._tries < 10) {\n            var wait_for_interception = false;\n            transfer.req.notification.dispatch(.http_request_auth_required, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });\n            if (wait_for_interception) {\n                self.intercepted += 1;\n                if (comptime IS_DEBUG) {\n                    log.debug(.http, \"wait for auth interception\", .{ .intercepted = self.intercepted });\n                }\n                transfer._intercept_state = .pending;\n\n                // Wether or not this is a blocking request, we're not going\n                // to process it now. We can end the transfer, which will\n                // release the easy handle back into the pool. The transfer\n                // is still valid/alive (just has no handle).\n                self.endTransfer(transfer);\n                if (!transfer.req.blocking) {\n                    // In the case of an async request, we can just \"forget\"\n                    // about this transfer until it gets updated asynchronously\n                    // from some CDP command.\n                    continue;\n                }\n\n                // In the case of a sync request, we need to block until we\n                // get the CDP command for handling this case.\n                if (try self.waitForInterceptedResponse(transfer)) {\n                    // we've been asked to continue with the request\n                    // we can't process it here, since we're already inside\n                    // a process, so we need to queue it and wait for the\n                    // next tick (this is why it was safe to endTransfer\n                    // above, because even in the \"blocking\" path, we still\n                    // only process it on the next tick).\n                    self.queue.append(&transfer._node);\n                } else {\n                    // aborted, already cleaned up\n                }\n\n                continue;\n            }\n        }\n\n        // release it ASAP so that it's available; some done_callbacks\n        // will load more resources.\n        self.endTransfer(transfer);\n\n        defer transfer.deinit();\n\n        if (msg.err) |err| {\n            requestFailed(transfer, err, true);\n        } else blk: {\n            // make sure the transfer can't be immediately aborted from a callback\n            // since we still need it here.\n            transfer._performing = true;\n            defer transfer._performing = false;\n\n            if (!transfer._header_done_called) {\n                // In case of request w/o data, we need to call the header done\n                // callback now.\n                const proceed = transfer.headerDoneCallback(&msg.conn) catch |err| {\n                    log.err(.http, \"header_done_callback2\", .{ .err = err });\n                    requestFailed(transfer, err, true);\n                    continue;\n                };\n                if (!proceed) {\n                    requestFailed(transfer, error.Abort, true);\n                    break :blk;\n                }\n            }\n            transfer.req.done_callback(transfer.ctx) catch |err| {\n                // transfer isn't valid at this point, don't use it.\n                log.err(.http, \"done_callback\", .{ .err = err });\n                requestFailed(transfer, err, true);\n                continue;\n            };\n\n            transfer.req.notification.dispatch(.http_request_done, &.{\n                .transfer = transfer,\n            });\n            processed = true;\n        }\n    }\n    return processed;\n}\n\nfn endTransfer(self: *Client, transfer: *Transfer) void {\n    const conn = transfer._conn.?;\n    self.removeConn(conn);\n    transfer._conn = null;\n    self.active -= 1;\n}\n\nfn removeConn(self: *Client, conn: *Net.Connection) void {\n    self.in_use.remove(&conn.node);\n    if (self.handles.remove(conn)) {\n        self.releaseConn(conn);\n    } else |_| {\n        // Can happen if we're in a perform() call, so we'll queue this\n        // for cleanup later.\n        self.dirty.append(&conn.node);\n    }\n}\n\nfn releaseConn(self: *Client, conn: *Net.Connection) void {\n    self.network.releaseConnection(conn);\n}\n\nfn ensureNoActiveConnection(self: *const Client) !void {\n    if (self.active > 0) {\n        return error.InflightConnection;\n    }\n}\n\npub const RequestCookie = struct {\n    is_http: bool,\n    jar: *CookieJar,\n    is_navigation: bool,\n    origin: [:0]const u8,\n\n    pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url: [:0]const u8, headers: *Net.Headers) !void {\n        var arr: std.ArrayList(u8) = .{};\n        try self.jar.forRequest(url, arr.writer(temp), .{\n            .is_http = self.is_http,\n            .is_navigation = self.is_navigation,\n            .origin_url = self.origin,\n        });\n\n        if (arr.items.len > 0) {\n            try arr.append(temp, 0); //null terminate\n            headers.cookies = @as([*c]const u8, @ptrCast(arr.items.ptr));\n        }\n    }\n};\n\npub const Request = struct {\n    frame_id: u32,\n    method: Method,\n    url: [:0]const u8,\n    headers: Net.Headers,\n    body: ?[]const u8 = null,\n    cookie_jar: ?*CookieJar,\n    resource_type: ResourceType,\n    credentials: ?[:0]const u8 = null,\n    notification: *Notification,\n    max_response_size: ?usize = null,\n\n    // This is only relevant for intercepted requests. If a request is flagged\n    // as blocking AND is intercepted, then it'll be up to us to wait until\n    // we receive a response to the interception. This probably isn't ideal,\n    // but it's harder for our caller (ScriptManager) to deal with this. One\n    // reason for that is the Http Client is already a bit CDP-aware.\n    blocking: bool = false,\n\n    // arbitrary data that can be associated with this request\n    ctx: *anyopaque = undefined,\n\n    start_callback: ?*const fn (transfer: *Transfer) anyerror!void = null,\n    header_callback: *const fn (transfer: *Transfer) anyerror!bool,\n    data_callback: *const fn (transfer: *Transfer, data: []const u8) anyerror!void,\n    done_callback: *const fn (ctx: *anyopaque) anyerror!void,\n    error_callback: *const fn (ctx: *anyopaque, err: anyerror) void,\n    shutdown_callback: ?*const fn (ctx: *anyopaque) void = null,\n\n    const ResourceType = enum {\n        document,\n        xhr,\n        script,\n        fetch,\n\n        // Allowed Values: Document, Stylesheet, Image, Media, Font, Script,\n        // TextTrack, XHR, Fetch, Prefetch, EventSource, WebSocket, Manifest,\n        // SignedExchange, Ping, CSPViolationReport, Preflight, FedCM, Other\n        // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType\n        pub fn string(self: ResourceType) []const u8 {\n            return switch (self) {\n                .document => \"Document\",\n                .xhr => \"XHR\",\n                .script => \"Script\",\n                .fetch => \"Fetch\",\n            };\n        }\n    };\n};\n\nconst AuthChallenge = Net.AuthChallenge;\n\npub const Transfer = struct {\n    arena: ArenaAllocator,\n    id: u32 = 0,\n    req: Request,\n    url: [:0]const u8,\n    ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers\n    client: *Client,\n    // total bytes received in the response, including the response status line,\n    // the headers, and the [encoded] body.\n    bytes_received: usize = 0,\n\n    aborted: bool = false,\n\n    max_response_size: ?usize = null,\n\n    // We'll store the response header here\n    response_header: ?ResponseHead = null,\n\n    // track if the header callbacks done have been called.\n    _header_done_called: bool = false,\n\n    _notified_fail: bool = false,\n\n    _conn: ?*Net.Connection = null,\n\n    _redirecting: bool = false,\n    _auth_challenge: ?AuthChallenge = null,\n\n    // number of times the transfer has been tried.\n    // incremented by reset func.\n    _tries: u8 = 0,\n    _performing: bool = false,\n\n    // for when a Transfer is queued in the client.queue\n    _node: std.DoublyLinkedList.Node = .{},\n    _intercept_state: InterceptState = .not_intercepted,\n\n    const InterceptState = union(enum) {\n        not_intercepted,\n        pending,\n        @\"continue\",\n        abort: anyerror,\n        fulfilled,\n    };\n\n    pub fn reset(self: *Transfer) void {\n        // There's an assertion in ScriptManager that's failing. Seemingly because\n        // the headerCallback is being called multiple times. This shouldn't be\n        // possible (hence the assertion). Previously, this `reset` would set\n        // _header_done_called = false. That could have been how headerCallback\n        // was called multuple times (because _header_done_called is the guard\n        // against that, so resetting it would allow a 2nd call to headerCallback).\n        // But it should also be impossible for this to be true. So, I've added\n        // this assertion to try to narrow down what's going on.\n        lp.assert(self._header_done_called == false, \"Transfer.reset header_done_called\", .{});\n\n        self._redirecting = false;\n        self._auth_challenge = null;\n        self._notified_fail = false;\n        self.response_header = null;\n        self.bytes_received = 0;\n\n        self._tries += 1;\n    }\n\n    fn deinit(self: *Transfer) void {\n        self.req.headers.deinit();\n        if (self._conn) |conn| {\n            self.client.removeConn(conn);\n        }\n        self.arena.deinit();\n        self.client.transfer_pool.destroy(self);\n    }\n\n    fn buildResponseHeader(self: *Transfer, conn: *const Net.Connection) !void {\n        if (comptime IS_DEBUG) {\n            std.debug.assert(self.response_header == null);\n        }\n\n        const url = try conn.getEffectiveUrl();\n\n        const status: u16 = if (self._auth_challenge != null)\n            407\n        else\n            try conn.getResponseCode();\n\n        self.response_header = .{\n            .url = url,\n            .status = status,\n            .redirect_count = try conn.getRedirectCount(),\n        };\n\n        if (conn.getResponseHeader(\"content-type\", 0)) |ct| {\n            var hdr = &self.response_header.?;\n            const value = ct.value;\n            const len = @min(value.len, ResponseHead.MAX_CONTENT_TYPE_LEN);\n            hdr._content_type_len = len;\n            @memcpy(hdr._content_type[0..len], value[0..len]);\n        }\n    }\n\n    pub fn format(self: *Transfer, writer: *std.Io.Writer) !void {\n        const req = self.req;\n        return writer.print(\"{s} {s}\", .{ @tagName(req.method), req.url });\n    }\n\n    pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {\n        // for cookies\n        self.url = url;\n\n        // for the request itself\n        self.req.url = url;\n    }\n\n    pub fn updateCredentials(self: *Transfer, userpwd: [:0]const u8) void {\n        self.req.credentials = userpwd;\n    }\n\n    pub fn replaceRequestHeaders(self: *Transfer, allocator: Allocator, headers: []const Net.Header) !void {\n        self.req.headers.deinit();\n\n        var buf: std.ArrayList(u8) = .empty;\n        var new_headers = try self.client.newHeaders();\n        for (headers) |hdr| {\n            // safe to re-use this buffer, because Headers.add because curl copies\n            // the value we pass into curl_slist_append.\n            defer buf.clearRetainingCapacity();\n            try std.fmt.format(buf.writer(allocator), \"{s}: {s}\", .{ hdr.name, hdr.value });\n            try buf.append(allocator, 0); // null terminated\n            try new_headers.add(buf.items[0 .. buf.items.len - 1 :0]);\n        }\n        self.req.headers = new_headers;\n    }\n\n    pub fn abort(self: *Transfer, err: anyerror) void {\n        requestFailed(self, err, true);\n\n        const client = self.client;\n        if (self._performing or client.performing) {\n            // We're currently in a curl_multi_perform. We cannot call endTransfer\n            // as that calls curl_multi_remove_handle, and you can't do that\n            // from a curl callback. Instead, we flag this transfer and all of\n            // our callbacks will check for this flag and abort the transfer for\n            // us\n            self.aborted = true;\n            return;\n        }\n\n        if (self._conn != null) {\n            client.endTransfer(self);\n        }\n        self.deinit();\n    }\n\n    pub fn terminate(self: *Transfer) void {\n        requestFailed(self, error.Shutdown, false);\n        if (self._conn != null) {\n            self.client.endTransfer(self);\n        }\n        self.deinit();\n    }\n\n    // internal, when the page is shutting down. Doesn't have the same ceremony\n    // as abort (doesn't send a notification, doesn't invoke an error callback)\n    fn kill(self: *Transfer) void {\n        if (self._conn != null) {\n            self.client.endTransfer(self);\n        }\n        if (self.req.shutdown_callback) |cb| {\n            cb(self.ctx);\n        }\n        self.deinit();\n    }\n\n    // abortAuthChallenge is called when an auth challenge interception is\n    // abort. We don't call self.client.endTransfer here b/c it has been done\n    // before interception process.\n    pub fn abortAuthChallenge(self: *Transfer) void {\n        if (comptime IS_DEBUG) {\n            std.debug.assert(self._intercept_state != .not_intercepted);\n            log.debug(.http, \"abort auth transfer\", .{ .intercepted = self.client.intercepted });\n        }\n        self.client.intercepted -= 1;\n        if (!self.req.blocking) {\n            self.abort(error.AbortAuthChallenge);\n            return;\n        }\n        self._intercept_state = .{ .abort = error.AbortAuthChallenge };\n    }\n\n    // redirectionCookies manages cookies during redirections handled by Curl.\n    // It sets the cookies from the current response to the cookie jar.\n    // It also immediately sets cookies for the following request.\n    fn redirectionCookies(transfer: *Transfer, conn: *const Net.Connection) !void {\n        const req = &transfer.req;\n        const arena = transfer.arena.allocator();\n\n        // retrieve cookies from the redirect's response.\n        if (req.cookie_jar) |jar| {\n            var i: usize = 0;\n            while (true) {\n                const ct = conn.getResponseHeader(\"set-cookie\", i);\n                if (ct == null) break;\n                try jar.populateFromResponse(transfer.url, ct.?.value);\n                i += 1;\n                if (i >= ct.?.amount) break;\n            }\n        }\n\n        // set cookies for the following redirection's request.\n        const location = conn.getResponseHeader(\"location\", 0) orelse {\n            return error.LocationNotFound;\n        };\n\n        const base_url = try conn.getEffectiveUrl();\n\n        const url = try URL.resolve(arena, std.mem.span(base_url), location.value, .{});\n        transfer.url = url;\n\n        if (req.cookie_jar) |jar| {\n            var cookies: std.ArrayList(u8) = .{};\n            try jar.forRequest(url, cookies.writer(arena), .{\n                .is_http = true,\n                .origin_url = url,\n                // used to enforce samesite cookie rules\n                .is_navigation = req.resource_type == .document,\n            });\n            try cookies.append(arena, 0); //null terminate\n            try conn.setCookies(@ptrCast(cookies.items.ptr));\n        }\n    }\n\n    // headerDoneCallback is called once the headers have been read.\n    // It can be called either on dataCallback or once the request for those\n    // w/o body.\n    fn headerDoneCallback(transfer: *Transfer, conn: *const Net.Connection) !bool {\n        lp.assert(transfer._header_done_called == false, \"Transfer.headerDoneCallback\", .{});\n        defer transfer._header_done_called = true;\n\n        try transfer.buildResponseHeader(conn);\n\n        if (conn.getResponseHeader(\"content-type\", 0)) |ct| {\n            var hdr = &transfer.response_header.?;\n            const value = ct.value;\n            const len = @min(value.len, ResponseHead.MAX_CONTENT_TYPE_LEN);\n            hdr._content_type_len = len;\n            @memcpy(hdr._content_type[0..len], value[0..len]);\n        }\n\n        if (transfer.req.cookie_jar) |jar| {\n            var i: usize = 0;\n            while (true) {\n                const ct = conn.getResponseHeader(\"set-cookie\", i);\n                if (ct == null) break;\n                jar.populateFromResponse(transfer.url, ct.?.value) catch |err| {\n                    log.err(.http, \"set cookie\", .{ .err = err, .req = transfer });\n                    return err;\n                };\n                i += 1;\n                if (i >= ct.?.amount) break;\n            }\n        }\n\n        if (transfer.max_response_size) |max_size| {\n            if (transfer.getContentLength()) |cl| {\n                if (cl > max_size) {\n                    return error.ResponseTooLarge;\n                }\n            }\n        }\n\n        const proceed = transfer.req.header_callback(transfer) catch |err| {\n            log.err(.http, \"header_callback\", .{ .err = err, .req = transfer });\n            return err;\n        };\n\n        transfer.req.notification.dispatch(.http_response_header_done, &.{\n            .transfer = transfer,\n        });\n\n        return proceed and transfer.aborted == false;\n    }\n\n    // headerCallback is called by curl on each request's header line read.\n    fn headerCallback(buffer: [*]const u8, header_count: usize, buf_len: usize, data: *anyopaque) usize {\n        // libcurl should only ever emit 1 header at a time\n        if (comptime IS_DEBUG) {\n            std.debug.assert(header_count == 1);\n        }\n\n        const conn: Net.Connection = .{ .easy = @ptrCast(@alignCast(data)) };\n        var transfer = fromConnection(&conn) catch |err| {\n            log.err(.http, \"get private info\", .{ .err = err, .source = \"header callback\" });\n            return 0;\n        };\n\n        if (comptime IS_DEBUG) {\n            // curl will allow header lines that end with either \\r\\n or just \\n\n            std.debug.assert(buffer[buf_len - 1] == '\\n');\n        }\n\n        if (buf_len < 3) {\n            // could be \\r\\n or \\n.\n            // We get the last header line.\n            if (transfer._redirecting) {\n                // parse and set cookies for the redirection.\n                redirectionCookies(transfer, &conn) catch |err| {\n                    if (comptime IS_DEBUG) {\n                        log.debug(.http, \"redirection cookies\", .{ .err = err });\n                    }\n                    return 0;\n                };\n            }\n            return buf_len;\n        }\n\n        var header_len = buf_len - 2;\n        if (buffer[buf_len - 2] != '\\r') {\n            // curl supports headers that just end with either \\r\\n or \\n\n            header_len = buf_len - 1;\n        }\n\n        const header = buffer[0..header_len];\n\n        // We need to parse the first line headers for each request b/c curl's\n        // CURLINFO_RESPONSE_CODE returns the status code of the final request.\n        // If a redirection or a proxy's CONNECT forbidden happens, we won't\n        // get this intermediary status code.\n        if (std.mem.startsWith(u8, header, \"HTTP/\")) {\n            // Is it the first header line.\n            if (buf_len < 13) {\n                if (comptime IS_DEBUG) {\n                    log.debug(.http, \"invalid response line\", .{ .line = header });\n                }\n                return 0;\n            }\n            const version_start: usize = if (header[5] == '2') 7 else 9;\n            const version_end = version_start + 3;\n\n            // a bit silly, but it makes sure that we don't change the length check\n            // above in a way that could break this.\n            if (comptime IS_DEBUG) {\n                std.debug.assert(version_end < 13);\n            }\n\n            const status = std.fmt.parseInt(u16, header[version_start..version_end], 10) catch {\n                if (comptime IS_DEBUG) {\n                    log.debug(.http, \"invalid status code\", .{ .line = header });\n                }\n                return 0;\n            };\n\n            if (status >= 300 and status <= 399) {\n                transfer._redirecting = true;\n                return buf_len;\n            }\n            transfer._redirecting = false;\n\n            if (status == 401 or status == 407) {\n                // The auth challenge must be parsed from a following\n                // WWW-Authenticate or Proxy-Authenticate header.\n                transfer._auth_challenge = .{\n                    .status = status,\n                    .source = null,\n                    .scheme = null,\n                    .realm = null,\n                };\n                return buf_len;\n            }\n            transfer._auth_challenge = null;\n\n            transfer.bytes_received = buf_len;\n            return buf_len;\n        }\n\n        if (transfer._redirecting == false and transfer._auth_challenge != null) {\n            transfer.bytes_received += buf_len;\n        }\n\n        if (transfer._auth_challenge != null) {\n            // try to parse auth challenge.\n            if (std.ascii.startsWithIgnoreCase(header, \"WWW-Authenticate\") or\n                std.ascii.startsWithIgnoreCase(header, \"Proxy-Authenticate\"))\n            {\n                const ac = AuthChallenge.parse(\n                    transfer._auth_challenge.?.status,\n                    header,\n                ) catch |err| {\n                    // We can't parse the auth challenge\n                    log.err(.http, \"parse auth challenge\", .{ .err = err, .header = header });\n                    // Should we cancel the request? I don't think so.\n                    return buf_len;\n                };\n                transfer._auth_challenge = ac;\n            }\n        }\n\n        return buf_len;\n    }\n\n    fn dataCallback(buffer: [*]const u8, chunk_count: usize, chunk_len: usize, data: *anyopaque) usize {\n        // libcurl should only ever emit 1 chunk at a time\n        if (comptime IS_DEBUG) {\n            std.debug.assert(chunk_count == 1);\n        }\n\n        const conn: Net.Connection = .{ .easy = @ptrCast(@alignCast(data)) };\n        var transfer = fromConnection(&conn) catch |err| {\n            log.err(.http, \"get private info\", .{ .err = err, .source = \"body callback\" });\n            return Net.writefunc_error;\n        };\n\n        if (transfer._redirecting or transfer._auth_challenge != null) {\n            return @intCast(chunk_len);\n        }\n\n        if (!transfer._header_done_called) {\n            const proceed = transfer.headerDoneCallback(&conn) catch |err| {\n                log.err(.http, \"header_done_callback\", .{ .err = err, .req = transfer });\n                return Net.writefunc_error;\n            };\n            if (!proceed) {\n                // signal abort to libcurl\n                return Net.writefunc_error;\n            }\n        }\n\n        transfer.bytes_received += chunk_len;\n        if (transfer.max_response_size) |max_size| {\n            if (transfer.bytes_received > max_size) {\n                requestFailed(transfer, error.ResponseTooLarge, true);\n                return Net.writefunc_error;\n            }\n        }\n\n        const chunk = buffer[0..chunk_len];\n        transfer.req.data_callback(transfer, chunk) catch |err| {\n            log.err(.http, \"data_callback\", .{ .err = err, .req = transfer });\n            return Net.writefunc_error;\n        };\n\n        transfer.req.notification.dispatch(.http_response_data, &.{\n            .data = chunk,\n            .transfer = transfer,\n        });\n\n        if (transfer.aborted) {\n            return Net.writefunc_error;\n        }\n\n        return @intCast(chunk_len);\n    }\n\n    pub fn responseHeaderIterator(self: *Transfer) HeaderIterator {\n        if (self._conn) |conn| {\n            // If we have a connection, than this is a real curl request and we\n            // iterate through the header that curl maintains.\n            return .{ .curl = .{ .conn = conn } };\n        }\n        // If there's no handle, it either means this is being called before\n        // the request is even being made (which would be a bug in the code)\n        // or when a response was injected via transfer.fulfill. The injected\n        // header should be iterated, since there is no handle/easy.\n        return .{ .list = .{ .list = self.response_header.?._injected_headers } };\n    }\n\n    pub fn fromConnection(conn: *const Net.Connection) !*Transfer {\n        const private = try conn.getPrivate();\n        return @ptrCast(@alignCast(private));\n    }\n\n    pub fn fulfill(transfer: *Transfer, status: u16, headers: []const Net.Header, body: ?[]const u8) !void {\n        if (transfer._conn != null) {\n            // should never happen, should have been intercepted/paused, and then\n            // either continued, aborted or fulfilled once.\n            @branchHint(.unlikely);\n            return error.RequestInProgress;\n        }\n\n        transfer._fulfill(status, headers, body) catch |err| {\n            transfer.req.error_callback(transfer.req.ctx, err);\n            return err;\n        };\n    }\n\n    fn _fulfill(transfer: *Transfer, status: u16, headers: []const Net.Header, body: ?[]const u8) !void {\n        const req = &transfer.req;\n        if (req.start_callback) |cb| {\n            try cb(transfer);\n        }\n\n        transfer.response_header = .{\n            .status = status,\n            .url = req.url,\n            .redirect_count = 0,\n            ._injected_headers = headers,\n        };\n        for (headers) |hdr| {\n            if (std.ascii.eqlIgnoreCase(hdr.name, \"content-type\")) {\n                const len = @min(hdr.value.len, ResponseHead.MAX_CONTENT_TYPE_LEN);\n                @memcpy(transfer.response_header.?._content_type[0..len], hdr.value[0..len]);\n                transfer.response_header.?._content_type_len = len;\n                break;\n            }\n        }\n\n        lp.assert(transfer._header_done_called == false, \"Transfer.fulfill header_done_called\", .{});\n        if (try req.header_callback(transfer) == false) {\n            transfer.abort(error.Abort);\n            return;\n        }\n\n        if (body) |b| {\n            try req.data_callback(transfer, b);\n        }\n\n        try req.done_callback(req.ctx);\n    }\n\n    // This function should be called during the dataCallback. Calling it after\n    // such as in the doneCallback is guaranteed to return null.\n    pub fn getContentLength(self: *const Transfer) ?u32 {\n        const cl = self.getContentLengthRawValue() orelse return null;\n        return std.fmt.parseInt(u32, cl, 10) catch null;\n    }\n\n    fn getContentLengthRawValue(self: *const Transfer) ?[]const u8 {\n        if (self._conn) |conn| {\n            // If we have a connection, than this is a normal request. We can get the\n            // header value from the connection.\n            const cl = conn.getResponseHeader(\"content-length\", 0) orelse return null;\n            return cl.value;\n        }\n\n        // If we have no handle, then maybe this is being called after the\n        // doneCallback. OR, maybe this is a \"fulfilled\" request. Let's check\n        // the injected headers (if we have any).\n\n        const rh = self.response_header orelse return null;\n        for (rh._injected_headers) |hdr| {\n            if (std.ascii.eqlIgnoreCase(hdr.name, \"content-length\")) {\n                return hdr.value;\n            }\n        }\n\n        return null;\n    }\n};\n"
  },
  {
    "path": "src/browser/Mime.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Mime = @This();\ncontent_type: ContentType,\nparams: []const u8 = \"\",\n// IANA defines max. charset value length as 40.\n// We keep 41 for null-termination since HTML parser expects in this format.\ncharset: [41]u8 = default_charset,\ncharset_len: usize = default_charset_len,\nis_default_charset: bool = true,\n\n/// String \"UTF-8\" continued by null characters.\nconst default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;\nconst default_charset_len = 5;\n\n/// Mime with unknown Content-Type, empty params and empty charset.\npub const unknown = Mime{ .content_type = .{ .unknown = {} } };\n\npub const ContentTypeEnum = enum {\n    text_xml,\n    text_html,\n    text_javascript,\n    text_plain,\n    text_css,\n    image_jpeg,\n    image_gif,\n    image_png,\n    image_webp,\n    application_json,\n    unknown,\n    other,\n};\n\npub const ContentType = union(ContentTypeEnum) {\n    text_xml: void,\n    text_html: void,\n    text_javascript: void,\n    text_plain: void,\n    text_css: void,\n    image_jpeg: void,\n    image_gif: void,\n    image_png: void,\n    image_webp: void,\n    application_json: void,\n    unknown: void,\n    other: struct { type: []const u8, sub_type: []const u8 },\n};\n\npub fn contentTypeString(mime: *const Mime) []const u8 {\n    return switch (mime.content_type) {\n        .text_xml => \"text/xml\",\n        .text_html => \"text/html\",\n        .text_javascript => \"application/javascript\",\n        .text_plain => \"text/plain\",\n        .text_css => \"text/css\",\n        .image_jpeg => \"image/jpeg\",\n        .image_png => \"image/png\",\n        .image_gif => \"image/gif\",\n        .image_webp => \"image/webp\",\n        .application_json => \"application/json\",\n        else => \"\",\n    };\n}\n\n/// Returns the null-terminated charset value.\npub fn charsetStringZ(mime: *const Mime) [:0]const u8 {\n    return mime.charset[0..mime.charset_len :0];\n}\n\npub fn charsetString(mime: *const Mime) []const u8 {\n    return mime.charset[0..mime.charset_len];\n}\n\n/// Removes quotes of value if quotes are given.\n///\n/// Currently we don't validate the charset.\n/// See section 2.3 Naming Requirements:\n/// https://datatracker.ietf.org/doc/rfc2978/\nfn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {\n    // Cannot be larger than 40.\n    // https://datatracker.ietf.org/doc/rfc2978/\n    if (value.len > 40) return error.CharsetTooBig;\n\n    // If the first char is a quote, look for a pair.\n    if (value[0] == '\"') {\n        if (value.len < 3 or value[value.len - 1] != '\"') {\n            return error.Invalid;\n        }\n\n        return value[1 .. value.len - 1];\n    }\n\n    // No quotes.\n    return value;\n}\n\npub fn parse(input: []u8) !Mime {\n    if (input.len > 255) {\n        return error.TooBig;\n    }\n\n    // Zig's trim API is broken. The return type is always `[]const u8`,\n    // even if the input type is `[]u8`. @constCast is safe here.\n    var normalized = @constCast(std.mem.trim(u8, input, &std.ascii.whitespace));\n    _ = std.ascii.lowerString(normalized, normalized);\n\n    const content_type, const type_len = try parseContentType(normalized);\n    if (type_len >= normalized.len) {\n        return .{ .content_type = content_type };\n    }\n\n    const params = trimLeft(normalized[type_len..]);\n\n    var charset: [41]u8 = default_charset;\n    var charset_len: usize = default_charset_len;\n    var has_explicit_charset = false;\n\n    var it = std.mem.splitScalar(u8, params, ';');\n    while (it.next()) |attr| {\n        const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse continue;\n        const name = trimLeft(attr[0..i]);\n\n        const value = trimRight(attr[i + 1 ..]);\n        if (value.len == 0) {\n            continue;\n        }\n\n        const attribute_name = std.meta.stringToEnum(enum {\n            charset,\n        }, name) orelse continue;\n\n        switch (attribute_name) {\n            .charset => {\n                if (value.len == 0) {\n                    break;\n                }\n\n                const attribute_value = parseCharset(value) catch continue;\n                @memcpy(charset[0..attribute_value.len], attribute_value);\n                // Null-terminate right after attribute value.\n                charset[attribute_value.len] = 0;\n                charset_len = attribute_value.len;\n                has_explicit_charset = true;\n            },\n        }\n    }\n\n    return .{\n        .params = params,\n        .charset = charset,\n        .charset_len = charset_len,\n        .content_type = content_type,\n        .is_default_charset = !has_explicit_charset,\n    };\n}\n\n/// Prescan the first 1024 bytes of an HTML document for a charset declaration.\n/// Looks for `<meta charset=\"X\">` and `<meta http-equiv=\"Content-Type\" content=\"...;charset=X\">`.\n/// Returns the charset value or null if none found.\n/// See: https://www.w3.org/International/questions/qa-html-encoding-declarations\npub fn prescanCharset(html: []const u8) ?[]const u8 {\n    const limit = @min(html.len, 1024);\n    const data = html[0..limit];\n\n    // Scan for <meta tags\n    var pos: usize = 0;\n    while (pos < data.len) {\n        // Find next '<'\n        pos = std.mem.indexOfScalarPos(u8, data, pos, '<') orelse return null;\n        pos += 1;\n        if (pos >= data.len) return null;\n\n        // Check for \"meta\" (case-insensitive)\n        if (pos + 4 >= data.len) return null;\n        var tag_buf: [4]u8 = undefined;\n        _ = std.ascii.lowerString(&tag_buf, data[pos..][0..4]);\n        if (!std.mem.eql(u8, &tag_buf, \"meta\")) {\n            continue;\n        }\n        pos += 4;\n\n        // Must be followed by whitespace or end of tag\n        if (pos >= data.len) return null;\n        if (data[pos] != ' ' and data[pos] != '\\t' and data[pos] != '\\n' and\n            data[pos] != '\\r' and data[pos] != '/')\n        {\n            continue;\n        }\n\n        // Scan attributes within this meta tag\n        const tag_end = std.mem.indexOfScalarPos(u8, data, pos, '>') orelse return null;\n        const attrs = data[pos..tag_end];\n\n        // Look for charset= attribute directly\n        if (findAttrValue(attrs, \"charset\")) |charset| {\n            if (charset.len > 0 and charset.len <= 40) return charset;\n        }\n\n        // Look for http-equiv=\"content-type\" with content=\"...;charset=X\"\n        if (findAttrValue(attrs, \"http-equiv\")) |he| {\n            if (std.ascii.eqlIgnoreCase(he, \"content-type\")) {\n                if (findAttrValue(attrs, \"content\")) |content| {\n                    if (extractCharsetFromContentType(content)) |charset| {\n                        return charset;\n                    }\n                }\n            }\n        }\n\n        pos = tag_end + 1;\n    }\n    return null;\n}\n\nfn findAttrValue(attrs: []const u8, name: []const u8) ?[]const u8 {\n    var pos: usize = 0;\n    while (pos < attrs.len) {\n        // Skip whitespace\n        while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\\t' or\n            attrs[pos] == '\\n' or attrs[pos] == '\\r'))\n        {\n            pos += 1;\n        }\n        if (pos >= attrs.len) return null;\n\n        // Read attribute name\n        const attr_start = pos;\n        while (pos < attrs.len and attrs[pos] != '=' and attrs[pos] != ' ' and\n            attrs[pos] != '\\t' and attrs[pos] != '>' and attrs[pos] != '/')\n        {\n            pos += 1;\n        }\n        const attr_name = attrs[attr_start..pos];\n\n        // Skip whitespace around =\n        while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\\t')) pos += 1;\n        if (pos >= attrs.len or attrs[pos] != '=') {\n            // No '=' found - skip this token. Advance at least one byte to avoid infinite loop.\n            if (pos == attr_start) pos += 1;\n            continue;\n        }\n        pos += 1; // skip '='\n        while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\\t')) pos += 1;\n        if (pos >= attrs.len) return null;\n\n        // Read attribute value\n        const value = blk: {\n            if (attrs[pos] == '\"' or attrs[pos] == '\\'') {\n                const quote = attrs[pos];\n                pos += 1;\n                const val_start = pos;\n                while (pos < attrs.len and attrs[pos] != quote) pos += 1;\n                const val = attrs[val_start..pos];\n                if (pos < attrs.len) pos += 1; // skip closing quote\n                break :blk val;\n            } else {\n                const val_start = pos;\n                while (pos < attrs.len and attrs[pos] != ' ' and attrs[pos] != '\\t' and\n                    attrs[pos] != '>' and attrs[pos] != '/')\n                {\n                    pos += 1;\n                }\n                break :blk attrs[val_start..pos];\n            }\n        };\n\n        if (std.ascii.eqlIgnoreCase(attr_name, name)) return value;\n    }\n    return null;\n}\n\nfn extractCharsetFromContentType(content: []const u8) ?[]const u8 {\n    var it = std.mem.splitScalar(u8, content, ';');\n    while (it.next()) |part| {\n        const trimmed = std.mem.trimLeft(u8, part, &.{ ' ', '\\t' });\n        if (trimmed.len > 8 and std.ascii.eqlIgnoreCase(trimmed[0..8], \"charset=\")) {\n            const val = std.mem.trim(u8, trimmed[8..], &.{ ' ', '\\t', '\"', '\\'' });\n            if (val.len > 0 and val.len <= 40) return val;\n        }\n    }\n    return null;\n}\n\npub fn sniff(body: []const u8) ?Mime {\n    // 0x0C is form feed\n    const content = std.mem.trimLeft(u8, body, &.{ ' ', '\\t', '\\n', '\\r', 0x0C });\n    if (content.len == 0) {\n        return null;\n    }\n\n    if (content[0] != '<') {\n        if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {\n            // UTF-8 BOM\n            return .{\n                .content_type = .{ .text_plain = {} },\n                .charset = default_charset,\n                .charset_len = default_charset_len,\n                .is_default_charset = false,\n            };\n        }\n        if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {\n            // UTF-16 big-endian BOM\n            return .{\n                .content_type = .{ .text_plain = {} },\n                .charset = .{ 'U', 'T', 'F', '-', '1', '6', 'B', 'E' } ++ .{0} ** 33,\n                .charset_len = 8,\n                .is_default_charset = false,\n            };\n        }\n        if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {\n            // UTF-16 little-endian BOM\n            return .{\n                .content_type = .{ .text_plain = {} },\n                .charset = .{ 'U', 'T', 'F', '-', '1', '6', 'L', 'E' } ++ .{0} ** 33,\n                .charset_len = 8,\n                .is_default_charset = false,\n            };\n        }\n        return null;\n    }\n\n    // The longest prefix we have is \"<!DOCTYPE HTML \", 15 bytes. If we're\n    // here, we already know content[0] == '<', so we can skip that. So 14\n    // bytes.\n\n    // +1 because we don't need the leading '<'\n    var buf: [14]u8 = undefined;\n\n    const stripped = content[1..];\n    const prefix_len = @min(stripped.len, buf.len);\n    const prefix = std.ascii.lowerString(&buf, stripped[0..prefix_len]);\n\n    // we already know it starts with a <\n    const known_prefixes = [_]struct { []const u8, ContentType }{\n        .{ \"!doctype html\", .{ .text_html = {} } },\n        .{ \"html\", .{ .text_html = {} } },\n        .{ \"script\", .{ .text_html = {} } },\n        .{ \"iframe\", .{ .text_html = {} } },\n        .{ \"h1\", .{ .text_html = {} } },\n        .{ \"div\", .{ .text_html = {} } },\n        .{ \"font\", .{ .text_html = {} } },\n        .{ \"table\", .{ .text_html = {} } },\n        .{ \"a\", .{ .text_html = {} } },\n        .{ \"style\", .{ .text_html = {} } },\n        .{ \"title\", .{ .text_html = {} } },\n        .{ \"b\", .{ .text_html = {} } },\n        .{ \"body\", .{ .text_html = {} } },\n        .{ \"br\", .{ .text_html = {} } },\n        .{ \"p\", .{ .text_html = {} } },\n        .{ \"!--\", .{ .text_html = {} } },\n        .{ \"xml\", .{ .text_xml = {} } },\n    };\n    inline for (known_prefixes) |kp| {\n        const known_prefix = kp.@\"0\";\n        if (std.mem.startsWith(u8, prefix, known_prefix) and prefix.len > known_prefix.len) {\n            const next = prefix[known_prefix.len];\n            // a \"tag-terminating-byte\"\n            if (next == ' ' or next == '>') {\n                return .{ .content_type = kp.@\"1\" };\n            }\n        }\n    }\n\n    return null;\n}\n\npub fn isHTML(self: *const Mime) bool {\n    return self.content_type == .text_html;\n}\n\n// we expect value to be lowercase\nfn parseContentType(value: []const u8) !struct { ContentType, usize } {\n    const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;\n    const type_name = trimRight(value[0..end]);\n    const attribute_start = end + 1;\n\n    if (std.meta.stringToEnum(enum {\n        @\"text/xml\",\n        @\"text/html\",\n        @\"text/css\",\n        @\"text/plain\",\n\n        @\"text/javascript\",\n        @\"application/javascript\",\n        @\"application/x-javascript\",\n\n        @\"image/jpeg\",\n        @\"image/png\",\n        @\"image/gif\",\n        @\"image/webp\",\n\n        @\"application/json\",\n    }, type_name)) |known_type| {\n        const ct: ContentType = switch (known_type) {\n            .@\"text/xml\" => .{ .text_xml = {} },\n            .@\"text/html\" => .{ .text_html = {} },\n            .@\"text/javascript\", .@\"application/javascript\", .@\"application/x-javascript\" => .{ .text_javascript = {} },\n            .@\"text/plain\" => .{ .text_plain = {} },\n            .@\"text/css\" => .{ .text_css = {} },\n            .@\"image/jpeg\" => .{ .image_jpeg = {} },\n            .@\"image/png\" => .{ .image_png = {} },\n            .@\"image/gif\" => .{ .image_gif = {} },\n            .@\"image/webp\" => .{ .image_webp = {} },\n            .@\"application/json\" => .{ .application_json = {} },\n        };\n        return .{ ct, attribute_start };\n    }\n\n    const separator = std.mem.indexOfScalarPos(u8, type_name, 0, '/') orelse return error.Invalid;\n\n    const main_type = value[0..separator];\n    const sub_type = trimRight(value[separator + 1 .. end]);\n\n    if (main_type.len == 0 or validType(main_type) == false) {\n        return error.Invalid;\n    }\n    if (sub_type.len == 0 or validType(sub_type) == false) {\n        return error.Invalid;\n    }\n\n    return .{ .{ .other = .{\n        .type = main_type,\n        .sub_type = sub_type,\n    } }, attribute_start };\n}\n\nconst VALID_CODEPOINTS = blk: {\n    var v: [256]bool = undefined;\n    for (0..256) |i| {\n        v[i] = std.ascii.isAlphanumeric(i);\n    }\n    for (\"!#$%&\\\\*+-.^'_`|~\") |b| {\n        v[b] = true;\n    }\n    break :blk v;\n};\n\nfn validType(value: []const u8) bool {\n    for (value) |b| {\n        if (VALID_CODEPOINTS[b] == false) {\n            return false;\n        }\n    }\n    return true;\n}\n\nfn trimLeft(s: []const u8) []const u8 {\n    return std.mem.trimLeft(u8, s, &std.ascii.whitespace);\n}\n\nfn trimRight(s: []const u8) []const u8 {\n    return std.mem.trimRight(u8, s, &std.ascii.whitespace);\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"Mime: invalid\" {\n    defer testing.reset();\n\n    const invalids = [_][]const u8{\n        \"\",\n        \"text\",\n        \"text /html\",\n        \"text/ html\",\n        \"text / html\",\n        \"text/html other\",\n    };\n\n    for (invalids) |invalid| {\n        const mutable_input = try testing.arena_allocator.dupe(u8, invalid);\n        try testing.expectError(error.Invalid, Mime.parse(mutable_input));\n    }\n}\n\ntest \"Mime: malformed parameters are ignored\" {\n    defer testing.reset();\n\n    // These should all parse successfully as text/html with malformed params ignored\n    const valid_with_malformed_params = [_][]const u8{\n        \"text/html; x\",\n        \"text/html; x=\",\n        \"text/html; x=  \",\n        \"text/html; = \",\n        \"text/html;=\",\n        \"text/html; charset=\\\"\\\"\",\n        \"text/html; charset=\\\"\",\n        \"text/html; charset=\\\"\\\\\",\n        \"text/html;\\\"\",\n    };\n\n    for (valid_with_malformed_params) |input| {\n        const mutable_input = try testing.arena_allocator.dupe(u8, input);\n        const mime = try Mime.parse(mutable_input);\n        try testing.expectEqual(.text_html, std.meta.activeTag(mime.content_type));\n    }\n}\n\ntest \"Mime: parse common\" {\n    defer testing.reset();\n\n    try expect(.{ .content_type = .{ .text_xml = {} } }, \"text/xml\");\n    try expect(.{ .content_type = .{ .text_html = {} } }, \"text/html\");\n    try expect(.{ .content_type = .{ .text_plain = {} } }, \"text/plain\");\n\n    try expect(.{ .content_type = .{ .text_xml = {} } }, \"text/xml;\");\n    try expect(.{ .content_type = .{ .text_html = {} } }, \"text/html;\");\n    try expect(.{ .content_type = .{ .text_plain = {} } }, \"text/plain;\");\n\n    try expect(.{ .content_type = .{ .text_xml = {} } }, \"  \\ttext/xml\");\n    try expect(.{ .content_type = .{ .text_html = {} } }, \"text/html   \");\n    try expect(.{ .content_type = .{ .text_plain = {} } }, \"text/plain \\t\\t\");\n\n    try expect(.{ .content_type = .{ .text_xml = {} } }, \"TEXT/xml\");\n    try expect(.{ .content_type = .{ .text_html = {} } }, \"text/Html\");\n    try expect(.{ .content_type = .{ .text_plain = {} } }, \"TEXT/PLAIN\");\n\n    try expect(.{ .content_type = .{ .text_xml = {} } }, \" TeXT/xml\");\n    try expect(.{ .content_type = .{ .text_html = {} } }, \"teXt/HtML  ;\");\n    try expect(.{ .content_type = .{ .text_plain = {} } }, \"tExT/PlAiN;\");\n\n    try expect(.{ .content_type = .{ .text_javascript = {} } }, \"text/javascript\");\n    try expect(.{ .content_type = .{ .text_javascript = {} } }, \"Application/JavaScript\");\n    try expect(.{ .content_type = .{ .text_javascript = {} } }, \"application/x-javascript\");\n\n    try expect(.{ .content_type = .{ .application_json = {} } }, \"application/json\");\n    try expect(.{ .content_type = .{ .text_css = {} } }, \"text/css\");\n\n    try expect(.{ .content_type = .{ .image_jpeg = {} } }, \"image/jpeg\");\n    try expect(.{ .content_type = .{ .image_png = {} } }, \"image/png\");\n    try expect(.{ .content_type = .{ .image_gif = {} } }, \"image/gif\");\n    try expect(.{ .content_type = .{ .image_webp = {} } }, \"image/webp\");\n}\n\ntest \"Mime: parse uncommon\" {\n    defer testing.reset();\n\n    const text_csv = Expectation{\n        .content_type = .{ .other = .{ .type = \"text\", .sub_type = \"csv\" } },\n    };\n    try expect(text_csv, \"text/csv\");\n    try expect(text_csv, \"text/csv;\");\n    try expect(text_csv, \"  text/csv\\t  \");\n    try expect(text_csv, \"  text/csv\\t  ;\");\n\n    try expect(\n        .{ .content_type = .{ .other = .{ .type = \"text\", .sub_type = \"csv\" } } },\n        \"Text/CSV\",\n    );\n}\n\ntest \"Mime: parse charset\" {\n    defer testing.reset();\n\n    try expect(.{\n        .content_type = .{ .text_xml = {} },\n        .charset = \"utf-8\",\n        .params = \"charset=utf-8\",\n    }, \"text/xml; charset=utf-8\");\n\n    try expect(.{\n        .content_type = .{ .text_xml = {} },\n        .charset = \"utf-8\",\n        .params = \"charset=\\\"utf-8\\\"\",\n    }, \"text/xml;charset=\\\"UTF-8\\\"\");\n\n    try expect(.{\n        .content_type = .{ .text_html = {} },\n        .charset = \"iso-8859-1\",\n        .params = \"charset=\\\"iso-8859-1\\\"\",\n    }, \"text/html; charset=\\\"iso-8859-1\\\"\");\n\n    try expect(.{\n        .content_type = .{ .text_html = {} },\n        .charset = \"iso-8859-1\",\n        .params = \"charset=\\\"iso-8859-1\\\"\",\n    }, \"text/html; charset=\\\"ISO-8859-1\\\"\");\n\n    try expect(.{\n        .content_type = .{ .text_xml = {} },\n        .charset = \"custom-non-standard-charset-value\",\n        .params = \"charset=\\\"custom-non-standard-charset-value\\\"\",\n    }, \"text/xml;charset=\\\"custom-non-standard-charset-value\\\"\");\n\n    try expect(.{\n        .content_type = .{ .text_html = {} },\n        .charset = \"UTF-8\",\n        .params = \"x=\\\"\",\n    }, \"text/html;x=\\\"\");\n}\n\ntest \"Mime: isHTML\" {\n    defer testing.reset();\n\n    const assert = struct {\n        fn assert(expected: bool, input: []const u8) !void {\n            const mutable_input = try testing.arena_allocator.dupe(u8, input);\n            var mime = try Mime.parse(mutable_input);\n            try testing.expectEqual(expected, mime.isHTML());\n        }\n    }.assert;\n    try assert(true, \"text/html\");\n    try assert(true, \"text/html;\");\n    try assert(true, \"text/html; charset=utf-8\");\n    try assert(false, \"text/htm\"); // htm not html\n    try assert(false, \"text/plain\");\n    try assert(false, \"over/9000\");\n}\n\ntest \"Mime: sniff\" {\n    try testing.expectEqual(null, Mime.sniff(\"\"));\n    try testing.expectEqual(null, Mime.sniff(\"<htm\"));\n    try testing.expectEqual(null, Mime.sniff(\"<html!\"));\n    try testing.expectEqual(null, Mime.sniff(\"<a_\"));\n    try testing.expectEqual(null, Mime.sniff(\"<!doctype html\"));\n    try testing.expectEqual(null, Mime.sniff(\"<!doctype  html>\"));\n    try testing.expectEqual(null, Mime.sniff(\"\\n  <!doctype  html>\"));\n    try testing.expectEqual(null, Mime.sniff(\"\\n \\t <font/>\"));\n\n    const expectHTML = struct {\n        fn expect(input: []const u8) !void {\n            try testing.expectEqual(.text_html, std.meta.activeTag(Mime.sniff(input).?.content_type));\n        }\n    }.expect;\n\n    try expectHTML(\"<!doctype html \");\n    try expectHTML(\"\\n  \\t    <!DOCTYPE HTML \");\n\n    try expectHTML(\"<html \");\n    try expectHTML(\"\\n  \\t    <HtmL> even more stufff\");\n\n    try expectHTML(\"<script>\");\n    try expectHTML(\"\\n  \\t    <SCRIpt >alert(document.cookies)</script>\");\n\n    try expectHTML(\"<iframe>\");\n    try expectHTML(\" \\t    <ifRAME >\");\n\n    try expectHTML(\"<h1>\");\n    try expectHTML(\"  <H1>\");\n\n    try expectHTML(\"<div>\");\n    try expectHTML(\"\\n\\r\\r  <DiV>\");\n\n    try expectHTML(\"<font>\");\n    try expectHTML(\"  <fonT>\");\n\n    try expectHTML(\"<table>\");\n    try expectHTML(\"\\t\\t<TAblE>\");\n\n    try expectHTML(\"<a>\");\n    try expectHTML(\"\\n\\n<A>\");\n\n    try expectHTML(\"<style>\");\n    try expectHTML(\"    \\n\\t <STyLE>\");\n\n    try expectHTML(\"<title>\");\n    try expectHTML(\"    \\n\\t <TITLE>\");\n\n    try expectHTML(\"<b>\");\n    try expectHTML(\"    \\n\\t <B>\");\n\n    try expectHTML(\"<body>\");\n    try expectHTML(\"    \\n\\t <BODY>\");\n\n    try expectHTML(\"<br>\");\n    try expectHTML(\"    \\n\\t <BR>\");\n\n    try expectHTML(\"<p>\");\n    try expectHTML(\"    \\n\\t <P>\");\n\n    try expectHTML(\"<!-->\");\n    try expectHTML(\"    \\n\\t <!-->\");\n\n    {\n        const mime = Mime.sniff(&.{ 0xEF, 0xBB, 0xBF }).?;\n        try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));\n        try testing.expectEqual(\"UTF-8\", mime.charsetString());\n    }\n\n    {\n        const mime = Mime.sniff(&.{ 0xFE, 0xFF }).?;\n        try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));\n        try testing.expectEqual(\"UTF-16BE\", mime.charsetString());\n    }\n\n    {\n        const mime = Mime.sniff(&.{ 0xFF, 0xFE }).?;\n        try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));\n        try testing.expectEqual(\"UTF-16LE\", mime.charsetString());\n    }\n}\n\nconst Expectation = struct {\n    content_type: Mime.ContentType,\n    params: []const u8 = \"\",\n    charset: ?[]const u8 = null,\n};\n\nfn expect(expected: Expectation, input: []const u8) !void {\n    const mutable_input = try testing.arena_allocator.dupe(u8, input);\n\n    const actual = try Mime.parse(mutable_input);\n    try testing.expectEqual(\n        std.meta.activeTag(expected.content_type),\n        std.meta.activeTag(actual.content_type),\n    );\n\n    switch (expected.content_type) {\n        .other => |e| {\n            const a = actual.content_type.other;\n            try testing.expectEqual(e.type, a.type);\n            try testing.expectEqual(e.sub_type, a.sub_type);\n        },\n        else => {}, // already asserted above\n    }\n\n    try testing.expectEqual(expected.params, actual.params);\n\n    if (expected.charset) |ec| {\n        // We remove the null characters for testing purposes here.\n        try testing.expectEqual(ec, actual.charsetString());\n    } else {\n        const m: Mime = .unknown;\n        try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());\n    }\n}\n\ntest \"Mime: prescanCharset\" {\n    // <meta charset=\"X\">\n    try testing.expectEqual(\"utf-8\", Mime.prescanCharset(\"<html><head><meta charset=\\\"utf-8\\\">\").?);\n    try testing.expectEqual(\"iso-8859-1\", Mime.prescanCharset(\"<html><head><meta charset=\\\"iso-8859-1\\\">\").?);\n    try testing.expectEqual(\"shift_jis\", Mime.prescanCharset(\"<meta charset='shift_jis'>\").?);\n\n    // Case-insensitive tag matching\n    try testing.expectEqual(\"utf-8\", Mime.prescanCharset(\"<META charset=\\\"utf-8\\\">\").?);\n    try testing.expectEqual(\"utf-8\", Mime.prescanCharset(\"<Meta charset=\\\"utf-8\\\">\").?);\n\n    // <meta http-equiv=\"Content-Type\" content=\"text/html; charset=X\">\n    try testing.expectEqual(\n        \"iso-8859-1\",\n        Mime.prescanCharset(\"<meta http-equiv=\\\"Content-Type\\\" content=\\\"text/html; charset=iso-8859-1\\\">\").?,\n    );\n\n    // No charset found\n    try testing.expectEqual(null, Mime.prescanCharset(\"<html><head><title>Test</title>\"));\n    try testing.expectEqual(null, Mime.prescanCharset(\"\"));\n    try testing.expectEqual(null, Mime.prescanCharset(\"no html here\"));\n\n    // Self-closing meta without charset must not loop forever\n    try testing.expectEqual(null, Mime.prescanCharset(\"<meta foo=\\\"bar\\\"/>\"));\n\n    // Charset after 1024 bytes should not be found\n    var long_html: [1100]u8 = undefined;\n    @memset(&long_html, ' ');\n    const suffix = \"<meta charset=\\\"windows-1252\\\">\";\n    @memcpy(long_html[1050 .. 1050 + suffix.len], suffix);\n    try testing.expectEqual(null, Mime.prescanCharset(&long_html));\n}\n"
  },
  {
    "path": "src/browser/Page.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst JS = @import(\"js/js.zig\");\nconst lp = @import(\"lightpanda\");\nconst builtin = @import(\"builtin\");\n\nconst Allocator = std.mem.Allocator;\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\nconst log = @import(\"../log.zig\");\n\nconst App = @import(\"../App.zig\");\nconst String = @import(\"../string.zig\").String;\n\nconst Mime = @import(\"Mime.zig\");\nconst Factory = @import(\"Factory.zig\");\nconst Session = @import(\"Session.zig\");\nconst EventManager = @import(\"EventManager.zig\");\nconst ScriptManager = @import(\"ScriptManager.zig\");\n\nconst Parser = @import(\"parser/Parser.zig\");\n\nconst URL = @import(\"URL.zig\");\nconst Blob = @import(\"webapi/Blob.zig\");\nconst Node = @import(\"webapi/Node.zig\");\nconst Event = @import(\"webapi/Event.zig\");\nconst EventTarget = @import(\"webapi/EventTarget.zig\");\nconst CData = @import(\"webapi/CData.zig\");\nconst Element = @import(\"webapi/Element.zig\");\nconst HtmlElement = @import(\"webapi/element/Html.zig\");\nconst Window = @import(\"webapi/Window.zig\");\nconst Location = @import(\"webapi/Location.zig\");\nconst Document = @import(\"webapi/Document.zig\");\nconst ShadowRoot = @import(\"webapi/ShadowRoot.zig\");\nconst Performance = @import(\"webapi/Performance.zig\");\nconst Screen = @import(\"webapi/Screen.zig\");\nconst VisualViewport = @import(\"webapi/VisualViewport.zig\");\nconst PerformanceObserver = @import(\"webapi/PerformanceObserver.zig\");\nconst AbstractRange = @import(\"webapi/AbstractRange.zig\");\nconst MutationObserver = @import(\"webapi/MutationObserver.zig\");\nconst IntersectionObserver = @import(\"webapi/IntersectionObserver.zig\");\nconst CustomElementDefinition = @import(\"webapi/CustomElementDefinition.zig\");\nconst storage = @import(\"webapi/storage/storage.zig\");\nconst PageTransitionEvent = @import(\"webapi/event/PageTransitionEvent.zig\");\nconst NavigationKind = @import(\"webapi/navigation/root.zig\").NavigationKind;\nconst KeyboardEvent = @import(\"webapi/event/KeyboardEvent.zig\");\nconst MouseEvent = @import(\"webapi/event/MouseEvent.zig\");\n\nconst HttpClient = @import(\"HttpClient.zig\");\nconst ArenaPool = App.ArenaPool;\n\nconst timestamp = @import(\"../datetime.zig\").timestamp;\nconst milliTimestamp = @import(\"../datetime.zig\").milliTimestamp;\n\nconst IFrame = Element.Html.IFrame;\nconst WebApiURL = @import(\"webapi/URL.zig\");\nconst GlobalEventHandlersLookup = @import(\"webapi/global_event_handlers.zig\").Lookup;\n\nvar default_url = WebApiURL{ ._raw = \"about:blank\" };\npub var default_location: Location = Location{ ._url = &default_url };\n\npub const BUF_SIZE = 1024;\n\nconst Page = @This();\n\nid: u32,\n\n// This is the \"id\" of the frame. It can be re-used from page-to-page, e.g.\n// when navigating.\n_frame_id: u32,\n\n_session: *Session,\n\n_event_manager: EventManager,\n\n_parse_mode: enum { document, fragment, document_write } = .document,\n\n// See Attribute.List for what this is. TL;DR: proper DOM Attribute Nodes are\n// fat yet rarely needed. We only create them on-demand, but still need proper\n// identity (a given attribute should return the same *Attribute), so we do\n// a look here. We don't store this in the Element or Attribute.List.Entry\n// because that would require additional space per element / Attribute.List.Entry\n// even thoug we'll create very few (if any) actual *Attributes.\n_attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute) = .empty,\n\n// Same as _atlribute_lookup, but instead of individual attributes, this is for\n// the return of elements.attributes.\n_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap) = .empty,\n\n// Lazily-created style, classList, and dataset objects. Only stored for elements\n// that actually access these features via JavaScript, saving 24 bytes per element.\n_element_styles: Element.StyleLookup = .empty,\n_element_datasets: Element.DatasetLookup = .empty,\n_element_class_lists: Element.ClassListLookup = .empty,\n_element_rel_lists: Element.RelListLookup = .empty,\n_element_shadow_roots: Element.ShadowRootLookup = .empty,\n_node_owner_documents: Node.OwnerDocumentLookup = .empty,\n_element_assigned_slots: Element.AssignedSlotLookup = .empty,\n_element_scroll_positions: Element.ScrollPositionLookup = .empty,\n_element_namespace_uris: Element.NamespaceUriLookup = .empty,\n\n/// Lazily-created inline event listeners (or listeners provided as attributes).\n/// Avoids bloating all elements with extra function fields for rare usage.\n///\n/// Use this when a listener provided like this:\n///\n/// ```js\n/// img.onload = () => { ... };\n/// ```\n///\n/// Its also used as cache for such cases after lazy evaluation:\n///\n/// ```html\n/// <img onload=\"(() => { ... })()\" />\n/// ```\n///\n/// ```js\n/// img.setAttribute(\"onload\", \"(() => { ... })()\");\n/// ```\n_event_target_attr_listeners: GlobalEventHandlersLookup = .empty,\n\n// Blob URL registry for URL.createObjectURL/revokeObjectURL\n_blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},\n\n/// `load` events that'll be fired before window's `load` event.\n/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.\n_to_load: std.ArrayList(*Element.Html) = .{},\n\n_script_manager: ScriptManager,\n\n// List of active live ranges (for mutation updates per DOM spec)\n_live_ranges: std.DoublyLinkedList = .{},\n\n// List of active MutationObservers\n_mutation_observers: std.DoublyLinkedList = .{},\n_mutation_delivery_scheduled: bool = false,\n_mutation_delivery_depth: u32 = 0,\n\n// List of active IntersectionObservers\n_intersection_observers: std.ArrayList(*IntersectionObserver) = .{},\n_intersection_check_scheduled: bool = false,\n_intersection_delivery_scheduled: bool = false,\n\n// Slots that need slotchange events to be fired\n_slots_pending_slotchange: std.AutoHashMapUnmanaged(*Element.Html.Slot, void) = .{},\n_slotchange_delivery_scheduled: bool = false,\n\n/// List of active PerformanceObservers.\n/// Contrary to MutationObserver and IntersectionObserver, these are regular tasks.\n_performance_observers: std.ArrayList(*PerformanceObserver) = .{},\n_performance_delivery_scheduled: bool = false,\n\n// Lookup for customized built-in elements. Maps element pointer to definition.\n_customized_builtin_definitions: std.AutoHashMapUnmanaged(*Element, *CustomElementDefinition) = .{},\n_customized_builtin_connected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{},\n_customized_builtin_disconnected_callback_invoked: std.AutoHashMapUnmanaged(*Element, void) = .{},\n\n// This is set when an element is being upgraded (constructor is called).\n// The constructor can access this to get the element being upgraded.\n_upgrading_element: ?*Node = null,\n\n// List of custom elements that were created before their definition was registered\n_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},\n\n// for heap allocations and managing WebAPI objects\n_factory: *Factory,\n\n_load_state: LoadState = .waiting,\n\n_parse_state: ParseState = .pre,\n\n_notified_network_idle: IdleNotification = .init,\n_notified_network_almost_idle: IdleNotification = .init,\n\n// A navigation event that happens from a script gets scheduled to run on the\n// next tick.\n_queued_navigation: ?*QueuedNavigation = null,\n\n// The URL of the current page\nurl: [:0]const u8 = \"about:blank\",\n\norigin: ?[]const u8 = null,\n\n// The base url specifies the base URL used to resolve the relative urls.\n// It is set by a <base> tag.\n// If null the url must be used.\nbase_url: ?[:0]const u8 = null,\n\n// referer header cache.\nreferer_header: ?[:0]const u8 = null,\n\n// Arbitrary buffer. Need to temporarily lowercase a value? Use this. No lifetime\n// guarantee - it's valid until someone else uses it.\nbuf: [BUF_SIZE]u8 = undefined,\n\n// access to the JavaScript engine\njs: *JS.Context,\n\n// An arena for the lifetime of the page.\narena: Allocator,\n\n// An arena with a lifetime guaranteed to be for 1 invoking of a Zig function\n// from JS. Best arena to use, when possible.\ncall_arena: Allocator,\n\nparent: ?*Page,\nwindow: *Window,\ndocument: *Document,\niframe: ?*IFrame = null,\nframes: std.ArrayList(*Page) = .{},\nframes_sorted: bool = true,\n\n// DOM version used to invalidate cached state of \"live\" collections\nversion: usize = 0,\n\n// This is maybe not great. It's a counter on the number of events that we're\n// waiting on before triggering the \"load\" event. Essentially, we need all\n// synchronous scripts and all iframes to be loaded. Scripts are handled by the\n// ScriptManager, so all scripts just count as 1 pending load.\n_pending_loads: u32,\n\n_parent_notified: bool = false,\n\n_type: enum { root, frame }, // only used for logs right now\n_req_id: u32 = 0,\n_navigated_options: ?NavigatedOpts = null,\n\npub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void {\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"page.init\", .{});\n    }\n\n    const call_arena = try session.getArena(.{ .debug = \"call_arena\" });\n    errdefer session.releaseArena(call_arena);\n\n    const factory = &session.factory;\n    const document = (try factory.document(Node.Document.HTMLDocument{\n        ._proto = undefined,\n    })).asDocument();\n\n    self.* = .{\n        .id = session.nextPageId(),\n        .js = undefined,\n        .parent = parent,\n        .arena = session.page_arena,\n        .document = document,\n        .window = undefined,\n        .call_arena = call_arena,\n        ._frame_id = frame_id,\n        ._session = session,\n        ._factory = factory,\n        ._pending_loads = 1, // always 1 for the ScriptManager\n        ._type = if (parent == null) .root else .frame,\n        ._script_manager = undefined,\n        ._event_manager = EventManager.init(session.page_arena, self),\n    };\n\n    var screen: *Screen = undefined;\n    var visual_viewport: *VisualViewport = undefined;\n    if (parent) |p| {\n        screen = p.window._screen;\n        visual_viewport = p.window._visual_viewport;\n    } else {\n        screen = try factory.eventTarget(Screen{\n            ._proto = undefined,\n            ._orientation = null,\n        });\n        visual_viewport = try factory.eventTarget(VisualViewport{\n            ._proto = undefined,\n        });\n    }\n\n    self.window = try factory.eventTarget(Window{\n        ._page = self,\n        ._proto = undefined,\n        ._document = self.document,\n        ._location = &default_location,\n        ._performance = Performance.init(),\n        ._screen = screen,\n        ._visual_viewport = visual_viewport,\n    });\n\n    const browser = session.browser;\n    self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);\n    errdefer self._script_manager.deinit();\n\n    self.js = try browser.env.createContext(self);\n    errdefer self.js.deinit();\n\n    document._page = self;\n\n    if (comptime builtin.is_test == false) {\n        if (parent == null) {\n            // HTML test runner manually calls these as necessary\n            try self.js.scheduler.add(session.browser, struct {\n                fn runIdleTasks(ctx: *anyopaque) !?u32 {\n                    const b: *@import(\"Browser.zig\") = @ptrCast(@alignCast(ctx));\n                    b.runIdleTasks();\n                    return 200;\n                }\n            }.runIdleTasks, 200, .{ .name = \"page.runIdleTasks\", .low_priority = true });\n        }\n    }\n}\n\npub fn deinit(self: *Page, abort_http: bool) void {\n    for (self.frames.items) |frame| {\n        frame.deinit(abort_http);\n    }\n\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"page.deinit\", .{ .url = self.url, .type = self._type });\n\n        // Uncomment if you want slab statistics to print.\n        // const stats = self._factory._slab.getStats(self.arena) catch unreachable;\n        // var buffer: [256]u8 = undefined;\n        // var stream = std.fs.File.stderr().writer(&buffer).interface;\n        // stats.print(&stream) catch unreachable;\n    }\n\n    const session = self._session;\n\n    if (self._queued_navigation) |qn| {\n        session.releaseArena(qn.arena);\n    }\n\n    session.browser.env.destroyContext(self.js);\n\n    self._script_manager.shutdown = true;\n\n    if (self.parent == null) {\n        session.browser.http_client.abort();\n    } else if (abort_http) {\n        // a small optimization, it's faster to abort _everything_ on the root\n        // page, so we prefer that. But if it's just the frame that's going\n        // away (a frame navigation) then we'll abort the frame-related requests\n        session.browser.http_client.abortFrame(self._frame_id);\n    }\n\n    self._script_manager.deinit();\n\n    session.releaseArena(self.call_arena);\n}\n\npub fn base(self: *const Page) [:0]const u8 {\n    return self.base_url orelse self.url;\n}\n\npub fn getTitle(self: *Page) !?[]const u8 {\n    if (self.window._document.is(Document.HTMLDocument)) |html_doc| {\n        return try html_doc.getTitle(self);\n    }\n    return null;\n}\n\n// Add comon headers for a request:\n// * cookies\n// * referer\npub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *HttpClient.Headers) !void {\n    try self.requestCookie(.{}).headersForRequest(temp, url, headers);\n\n    // Build the referer\n    const referer = blk: {\n        if (self.referer_header == null) {\n            // build the cache\n            if (std.mem.startsWith(u8, self.url, \"http\")) {\n                self.referer_header = try std.mem.concatWithSentinel(self.arena, u8, &.{ \"Referer: \", self.url }, 0);\n            } else {\n                self.referer_header = \"\";\n            }\n        }\n\n        break :blk self.referer_header.?;\n    };\n\n    // If the referer is empty, ignore the header.\n    if (referer.len > 0) {\n        try headers.add(referer);\n    }\n}\n\npub fn getArena(self: *Page, comptime opts: Session.GetArenaOpts) !Allocator {\n    return self._session.getArena(opts);\n}\n\npub fn releaseArena(self: *Page, allocator: Allocator) void {\n    return self._session.releaseArena(allocator);\n}\n\npub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {\n    const current_origin = self.origin orelse return false;\n    return std.mem.startsWith(u8, url, current_origin);\n}\n\n/// Look up a blob URL in this page's registry.\npub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob {\n    return self._blob_urls.get(url);\n}\n\npub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {\n    lp.assert(self._load_state == .waiting, \"page.renavigate\", .{});\n    const session = self._session;\n    self._load_state = .parsing;\n\n    const req_id = self._session.browser.http_client.nextReqId();\n    log.info(.page, \"navigate\", .{\n        .url = request_url,\n        .method = opts.method,\n        .reason = opts.reason,\n        .body = opts.body != null,\n        .req_id = req_id,\n        .type = self._type,\n    });\n\n    // Handle synthetic navigations: about:blank and blob: URLs\n    const is_about_blank = std.mem.eql(u8, \"about:blank\", request_url);\n    const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, \"blob:\");\n\n    if (is_about_blank or is_blob) {\n        self.url = if (is_about_blank) \"about:blank\" else try self.arena.dupeZ(u8, request_url);\n\n        if (is_blob) {\n            // strip out blob:\n            self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);\n        } else if (self.parent) |parent| {\n            self.origin = parent.origin;\n        } else {\n            self.origin = null;\n        }\n        try self.js.setOrigin(self.origin);\n\n        // Assume we parsed the document.\n        // It's important to force a reset during the following navigation.\n        self._parse_state = .complete;\n\n        // Content injection\n        if (is_blob) {\n            // For navigation, walk up the parent chain to find blob URLs\n            // (e.g., parent creates blob URL and sets iframe.src to it)\n            const blob = blk: {\n                var current: ?*Page = self.parent;\n                while (current) |page| {\n                    if (page._blob_urls.get(request_url)) |b| break :blk b;\n                    current = page.parent;\n                }\n                log.warn(.js, \"invalid blob\", .{ .url = request_url });\n                return error.BlobNotFound;\n            };\n            const parse_arena = try self.getArena(.{ .debug = \"Page.parseBlob\" });\n            defer self.releaseArena(parse_arena);\n            var parser = Parser.init(parse_arena, self.document.asNode(), self);\n            parser.parse(blob._slice);\n        } else {\n            self.document.injectBlank(self) catch |err| {\n                log.err(.browser, \"inject blank\", .{ .err = err });\n                return error.InjectBlankFailed;\n            };\n        }\n        self.documentIsComplete();\n\n        session.notification.dispatch(.page_navigate, &.{\n            .frame_id = self._frame_id,\n            .req_id = req_id,\n            .opts = opts,\n            .url = request_url,\n            .timestamp = timestamp(.monotonic),\n        });\n\n        // Record telemetry for navigation\n        session.browser.app.telemetry.record(.{\n            .navigate = .{\n                .tls = false, // about:blank and blob: are not TLS\n                .proxy = session.browser.app.config.httpProxy() != null,\n            },\n        });\n\n        session.notification.dispatch(.page_navigated, &.{\n            .frame_id = self._frame_id,\n            .req_id = req_id,\n            .opts = .{\n                .cdp_id = opts.cdp_id,\n                .reason = opts.reason,\n                .method = opts.method,\n            },\n            .url = request_url,\n            .timestamp = timestamp(.monotonic),\n        });\n\n        // force next request id manually b/c we won't create a real req.\n        _ = session.browser.http_client.incrReqId();\n        return;\n    }\n\n    var http_client = session.browser.http_client;\n\n    self.url = try self.arena.dupeZ(u8, request_url);\n    self.origin = try URL.getOrigin(self.arena, self.url);\n\n    self._req_id = req_id;\n    self._navigated_options = .{\n        .cdp_id = opts.cdp_id,\n        .reason = opts.reason,\n        .method = opts.method,\n    };\n\n    var headers = try http_client.newHeaders();\n    if (opts.header) |hdr| {\n        try headers.add(hdr);\n    }\n    try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, self.url, &headers);\n\n    // We dispatch page_navigate event before sending the request.\n    // It ensures the event page_navigated is not dispatched before this one.\n    session.notification.dispatch(.page_navigate, &.{\n        .frame_id = self._frame_id,\n        .req_id = req_id,\n        .opts = opts,\n        .url = self.url,\n        .timestamp = timestamp(.monotonic),\n    });\n\n    // Record telemetry for navigation\n    session.browser.app.telemetry.record(.{ .navigate = .{\n        .tls = std.ascii.startsWithIgnoreCase(self.url, \"https://\"),\n        .proxy = session.browser.app.config.httpProxy() != null,\n    } });\n\n    session.navigation._current_navigation_kind = opts.kind;\n\n    http_client.request(.{\n        .ctx = self,\n        .url = self.url,\n        .frame_id = self._frame_id,\n        .method = opts.method,\n        .headers = headers,\n        .body = opts.body,\n        .cookie_jar = &session.cookie_jar,\n        .resource_type = .document,\n        .notification = self._session.notification,\n        .header_callback = pageHeaderDoneCallback,\n        .data_callback = pageDataCallback,\n        .done_callback = pageDoneCallback,\n        .error_callback = pageErrorCallback,\n    }) catch |err| {\n        log.err(.page, \"navigate request\", .{ .url = self.url, .err = err, .type = self._type });\n        return err;\n    };\n}\n\n// Navigation can happen in many places, such as executing a <script> tag or\n// a JavaScript callback, a CDP command, etc...It's rarely safe to do immediately\n// as the caller almost certainly does'nt expect the page to go away during the\n// call. So, we schedule the navigation for the next tick.\npub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {\n    if (self.canScheduleNavigation(std.meta.activeTag(nt)) == false) {\n        return;\n    }\n    const arena = try self._session.getArena(.{ .debug = \"scheduleNavigation\" });\n    errdefer self._session.releaseArena(arena);\n    return self.scheduleNavigationWithArena(arena, request_url, opts, nt);\n}\n\n// Don't name the first parameter \"self\", because the target of this navigation\n// might change inside the function. So the code should be explicit about the\n// page that it's acting on.\nfn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {\n    const resolved_url, const is_about_blank = blk: {\n        if (std.mem.eql(u8, request_url, \"about:blank\")) {\n            // navigate will handle this special case\n            break :blk .{ \"about:blank\", true };\n        }\n        const u = try URL.resolve(\n            arena,\n            originator.base(),\n            request_url,\n            .{ .always_dupe = true, .encode = true },\n        );\n        break :blk .{ u, false };\n    };\n\n    const target = switch (nt) {\n        .form, .anchor => |p| p,\n        .script => |p| p orelse originator,\n        .iframe => |iframe| iframe._window.?._page, // only an frame with existing content (i.e. a window) can be navigated\n    };\n\n    const session = target._session;\n    if (!opts.force and URL.eqlDocument(target.url, resolved_url)) {\n        target.url = try target.arena.dupeZ(u8, resolved_url);\n        target.window._location = try Location.init(target.url, target);\n        target.document._location = target.window._location;\n        if (target.parent == null) {\n            try session.navigation.updateEntries(target.url, opts.kind, target, true);\n        }\n        // don't defer this, the caller is responsible for freeing it on error\n        session.releaseArena(arena);\n        return;\n    }\n\n    log.info(.browser, \"schedule navigation\", .{\n        .url = resolved_url,\n        .reason = opts.reason,\n        .type = target._type,\n    });\n\n    // This is a micro-optimization. Terminate any inflight request as early\n    // as we can. This will be more propery shutdown when we process the\n    // scheduled navigation.\n    if (target.parent == null) {\n        session.browser.http_client.abort();\n    } else {\n        // This doesn't terminate any inflight requests for nested frames, but\n        // again, this is just an optimization. We'll correctly shut down all\n        // nested inflight requests when we process the navigation.\n        session.browser.http_client.abortFrame(target._frame_id);\n    }\n\n    const qn = try arena.create(QueuedNavigation);\n    qn.* = .{\n        .opts = opts,\n        .arena = arena,\n        .url = resolved_url,\n        .is_about_blank = is_about_blank,\n        .navigation_type = std.meta.activeTag(nt),\n    };\n\n    if (target._queued_navigation) |existing| {\n        session.releaseArena(existing.arena);\n    }\n\n    target._queued_navigation = qn;\n    return session.scheduleNavigation(target);\n}\n\n// A script can have multiple competing navigation events, say it starts off\n// by doing top.location = 'x' and then does a form submission.\n// You might think that we just stop at the first one, but that doesn't seem\n// to be what browsers do, and it isn't particularly well supported by v8 (i.e.\n// halting execution mid-script).\n// From what I can tell, there are 4 \"levels\" of priority, in order:\n// 1 - form submission\n// 2 - JavaScript apis (e.g. top.location)\n// 3 - anchor clicks\n// 4 - iframe.src =\n// Within, each category, it's last-one-wins.\nfn canScheduleNavigation(self: *Page, new_target_type: NavigationType) bool {\n    if (self.parent) |parent| {\n        if (parent.isGoingAway()) {\n            return false;\n        }\n    }\n\n    const existing_target_type = (self._queued_navigation orelse return true).navigation_type;\n\n    if (existing_target_type == new_target_type) {\n        // same reason, than this latest one wins\n        return true;\n    }\n\n    return switch (existing_target_type) {\n        .iframe => true, // everything is higher priority than iframe.src = \"x\"\n        .anchor => new_target_type != .iframe, // an anchor is only higher priority than an iframe\n        .form => false, // nothing is higher priority than a form\n        .script => new_target_type == .form, // a form is higher priority than a script\n    };\n}\n\npub fn documentIsLoaded(self: *Page) void {\n    if (self._load_state != .parsing) {\n        // Ideally, documentIsLoaded would only be called once, but if a\n        // script is dynamically added from an async script after\n        // documentIsLoaded is already called, then ScriptManager will call\n        // it again.\n        return;\n    }\n\n    self._load_state = .load;\n    self.document._ready_state = .interactive;\n    self._documentIsLoaded() catch |err| {\n        log.err(.page, \"document is loaded\", .{ .err = err, .type = self._type, .url = self.url });\n    };\n}\n\npub fn _documentIsLoaded(self: *Page) !void {\n    const event = try Event.initTrusted(.wrap(\"DOMContentLoaded\"), .{ .bubbles = true }, self);\n    try self._event_manager.dispatch(\n        self.document.asEventTarget(),\n        event,\n    );\n}\n\npub fn scriptsCompletedLoading(self: *Page) void {\n    self.pendingLoadCompleted();\n}\n\npub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {\n    var ls: JS.Local.Scope = undefined;\n    self.js.localScope(&ls);\n    defer ls.deinit();\n\n    const entered = self.js.enter(&ls.handle_scope);\n    defer entered.exit();\n\n    blk: {\n        const event = Event.initTrusted(comptime .wrap(\"load\"), .{}, self) catch |err| {\n            log.err(.page, \"iframe event init\", .{ .err = err, .url = iframe._src });\n            break :blk;\n        };\n        self._event_manager.dispatch(iframe.asNode().asEventTarget(), event) catch |err| {\n            log.warn(.js, \"iframe onload\", .{ .err = err, .url = iframe._src });\n        };\n    }\n\n    self.pendingLoadCompleted();\n}\n\nfn pendingLoadCompleted(self: *Page) void {\n    const pending_loads = self._pending_loads;\n    if (pending_loads == 1) {\n        self._pending_loads = 0;\n        self.documentIsComplete();\n    } else {\n        self._pending_loads = pending_loads - 1;\n    }\n}\n\npub fn documentIsComplete(self: *Page) void {\n    if (self._load_state == .complete) {\n        // Ideally, documentIsComplete would only be called once, but with\n        // dynamic scripts, it can be hard to keep track of that. An async\n        // script could be evaluated AFTER Loaded and Complete and load its\n        // own non non-async script - which, upon completion, needs to check\n        // whether Laoded/Complete have already been called, which is what\n        // this guard is.\n        return;\n    }\n\n    // documentIsComplete could be called directly, without first calling\n    // documentIsLoaded, if there were _only_ async scripts\n    if (self._load_state == .parsing) {\n        self.documentIsLoaded();\n    }\n\n    self._load_state = .complete;\n    self._documentIsComplete() catch |err| {\n        log.err(.page, \"document is complete\", .{ .err = err, .type = self._type, .url = self.url });\n    };\n\n    if (self._navigated_options) |no| {\n        // _navigated_options will be null in special short-circuit cases, like\n        // \"navigating\" to about:blank, in which case this notification has\n        // already been sent\n        self._session.notification.dispatch(.page_navigated, &.{\n            .frame_id = self._frame_id,\n            .req_id = self._req_id,\n            .opts = no,\n            .url = self.url,\n            .timestamp = timestamp(.monotonic),\n        });\n    }\n}\n\nfn _documentIsComplete(self: *Page) !void {\n    self.document._ready_state = .complete;\n\n    // Run load events before window.load.\n    try self.dispatchLoad();\n\n    // Dispatch window.load event.\n    const window_target = self.window.asEventTarget();\n    if (self._event_manager.hasDirectListeners(window_target, \"load\", self.window._on_load)) {\n        const event = try Event.initTrusted(comptime .wrap(\"load\"), .{}, self);\n        // This event is weird, it's dispatched directly on the window, but\n        // with the document as the target.\n        event._target = self.document.asEventTarget();\n        try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = \"page load\" });\n    }\n\n    if (self._event_manager.hasDirectListeners(window_target, \"pageshow\", self.window._on_pageshow)) {\n        const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap(\"pageshow\"), .{}, self)).asEvent();\n        try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = \"page show\" });\n    }\n\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"load\", .{ .url = self.url, .type = self._type });\n    }\n\n    self.notifyParentLoadComplete();\n}\n\nfn notifyParentLoadComplete(self: *Page) void {\n    const parent = self.parent orelse return;\n\n    if (self._parent_notified == true) {\n        if (comptime IS_DEBUG) {\n            std.debug.assert(false);\n        }\n        // shouldn't happen, don't want to crash a release build over it\n        return;\n    }\n\n    self._parent_notified = true;\n    parent.iframeCompletedLoading(self.iframe.?);\n}\n\nfn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {\n    var self: *Page = @ptrCast(@alignCast(transfer.ctx));\n\n    const header = &transfer.response_header.?;\n\n    const response_url = std.mem.span(header.url);\n    if (std.mem.eql(u8, response_url, self.url) == false) {\n        // would be different than self.url in the case of a redirect\n        self.url = try self.arena.dupeZ(u8, response_url);\n        self.origin = try URL.getOrigin(self.arena, self.url);\n    }\n    try self.js.setOrigin(self.origin);\n\n    self.window._location = try Location.init(self.url, self);\n    self.document._location = self.window._location;\n\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"navigate header\", .{\n            .url = self.url,\n            .status = header.status,\n            .content_type = header.contentType(),\n            .type = self._type,\n        });\n    }\n\n    return true;\n}\n\nfn pageDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {\n    var self: *Page = @ptrCast(@alignCast(transfer.ctx));\n\n    if (self._parse_state == .pre) {\n        // we lazily do this, because we might need the first chunk of data\n        // to sniff the content type\n        var mime: Mime = blk: {\n            if (transfer.response_header.?.contentType()) |ct| {\n                break :blk try Mime.parse(ct);\n            }\n            break :blk Mime.sniff(data);\n        } orelse .unknown;\n\n        // If the HTTP Content-Type header didn't specify a charset and this is HTML,\n        // prescan the first 1024 bytes for a <meta charset> declaration.\n        if (mime.content_type == .text_html and mime.is_default_charset) {\n            if (Mime.prescanCharset(data)) |charset| {\n                if (charset.len <= 40) {\n                    @memcpy(mime.charset[0..charset.len], charset);\n                    mime.charset[charset.len] = 0;\n                    mime.charset_len = charset.len;\n                }\n            }\n        }\n\n        if (comptime IS_DEBUG) {\n            log.debug(.page, \"navigate first chunk\", .{\n                .content_type = mime.content_type,\n                .len = data.len,\n                .type = self._type,\n                .url = self.url,\n            });\n        }\n\n        switch (mime.content_type) {\n            .text_html => self._parse_state = .{ .html = .{} },\n            .application_json, .text_javascript, .text_css, .text_plain => {\n                var arr: std.ArrayList(u8) = .empty;\n                try arr.appendSlice(self.arena, \"<html><head><meta charset=\\\"utf-8\\\"></head><body><pre>\");\n                self._parse_state = .{ .text = arr };\n            },\n            .image_jpeg, .image_gif, .image_png, .image_webp => {\n                self._parse_state = .{ .image = .empty };\n            },\n            else => self._parse_state = .{ .raw = .empty },\n        }\n    }\n\n    switch (self._parse_state) {\n        .html => |*buf| try buf.appendSlice(self.arena, data),\n        .text => |*buf| {\n            // we have to escape the data...\n            var v = data;\n            while (v.len > 0) {\n                const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '<', '>' }) orelse {\n                    return buf.appendSlice(self.arena, v);\n                };\n                try buf.appendSlice(self.arena, v[0..index]);\n                switch (v[index]) {\n                    '<' => try buf.appendSlice(self.arena, \"&lt;\"),\n                    '>' => try buf.appendSlice(self.arena, \"&gt;\"),\n                    else => unreachable,\n                }\n                v = v[index + 1 ..];\n            }\n        },\n        .raw, .image => |*buf| try buf.appendSlice(self.arena, data),\n        .pre => unreachable,\n        .complete => unreachable,\n        .err => unreachable,\n        .raw_done => unreachable,\n    }\n}\n\nfn pageDoneCallback(ctx: *anyopaque) !void {\n    var self: *Page = @ptrCast(@alignCast(ctx));\n\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"navigate done\", .{ .type = self._type, .url = self.url });\n    }\n\n    //We need to handle different navigation types differently.\n    try self._session.navigation.commitNavigation(self);\n\n    defer if (comptime IS_DEBUG) {\n        log.debug(.page, \"page load complete\", .{\n            .url = self.url,\n            .type = self._type,\n            .state = std.meta.activeTag(self._parse_state),\n        });\n    };\n\n    const parse_arena = try self.getArena(.{ .debug = \"Page.parse\" });\n    defer self.releaseArena(parse_arena);\n\n    var parser = Parser.init(parse_arena, self.document.asNode(), self);\n\n    switch (self._parse_state) {\n        .html => |buf| {\n            parser.parse(buf.items);\n            self._script_manager.staticScriptsDone();\n            self._parse_state = .complete;\n        },\n        .text => |*buf| {\n            try buf.appendSlice(self.arena, \"</pre></body></html>\");\n            parser.parse(buf.items);\n            self.documentIsComplete();\n        },\n        .image => |buf| {\n            self._parse_state = .{ .raw_done = buf.items };\n\n            // Use empty an HTML containing the image.\n            const html = try std.mem.concat(parse_arena, u8, &.{\n                \"<html><head><meta charset=\\\"utf-8\\\"></head><body><img src=\\\"\",\n                self.url,\n                \"\\\"></body></htm>\",\n            });\n            parser.parse(html);\n            self.documentIsComplete();\n        },\n        .raw => |buf| {\n            self._parse_state = .{ .raw_done = buf.items };\n\n            // Use empty an empty HTML document.\n            parser.parse(\"<html><head><meta charset=\\\"utf-8\\\"></head><body></body></htm>\");\n            self.documentIsComplete();\n        },\n        .pre => {\n            // Received a response without a body like: https://httpbin.io/status/200\n            // We assume we have received an OK status (checked in Client.headerCallback)\n            // so we load a blank document to navigate away from any prior page.\n            self._parse_state = .{ .complete = {} };\n\n            // Use empty an empty HTML document.\n            parser.parse(\"<html><head><meta charset=\\\"utf-8\\\"></head><body></body></htm>\");\n            self.documentIsComplete();\n        },\n        .err => |err| {\n            // Generate a pseudo HTML page indicating the failure.\n            const html = try std.mem.concat(parse_arena, u8, &.{\n                \"<html><head><meta charset=\\\"utf-8\\\"></head><body><h1>Navigation failed</h1><p>Reason: \",\n                @errorName(err),\n                \"</p></body></htm>\",\n            });\n\n            parser.parse(html);\n            self.documentIsComplete();\n        },\n        else => unreachable,\n    }\n}\n\nfn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {\n    var self: *Page = @ptrCast(@alignCast(ctx));\n\n    log.err(.page, \"navigate failed\", .{ .err = err, .type = self._type, .url = self.url });\n    self._parse_state = .{ .err = err };\n\n    // In case of error, we want to complete the page with a custom HTML\n    // containing the error.\n    pageDoneCallback(ctx) catch |e| {\n        log.err(.browser, \"pageErrorCallback\", .{ .err = e, .type = self._type, .url = self.url });\n        return;\n    };\n}\n\npub fn isGoingAway(self: *const Page) bool {\n    if (self._queued_navigation != null) {\n        return true;\n    }\n    const parent = self.parent orelse return false;\n    return parent.isGoingAway();\n}\n\npub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Element.Html.Script) !void {\n    if (self.isGoingAway()) {\n        // if we're planning on navigating to another page, don't run this script\n        return;\n    }\n\n    if (comptime from_parser) {\n        // parser-inserted scripts have force-async set to false, but only if\n        // they have src or non-empty content\n        if (script._src.len > 0 or script.asNode().firstChild() != null) {\n            script._force_async = false;\n        }\n    }\n\n    self._script_manager.addFromElement(from_parser, script, \"parsing\") catch |err| {\n        log.err(.page, \"page.scriptAddedCallback\", .{\n            .err = err,\n            .url = self.url,\n            .src = script.asElement().getAttributeSafe(comptime .wrap(\"src\")),\n            .type = self._type,\n        });\n    };\n}\n\npub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void {\n    if (self.isGoingAway()) {\n        // if we're planning on navigating to another page, don't load this iframe\n        return;\n    }\n    if (iframe._executed) {\n        return;\n    }\n\n    var src = iframe.asElement().getAttributeSafe(comptime .wrap(\"src\")) orelse \"\";\n    if (src.len == 0) {\n        src = \"about:blank\";\n    }\n\n    if (iframe._window != null) {\n        // This frame is being re-navigated. We need to do this through a\n        // scheduleNavigation phase. We can't navigate immediately here, for\n        // the same reason that a \"root\" page can't immediately navigate:\n        // we could be in the middle of a JS callback or something else that\n        // doesn't exit the page to just suddenly go away.\n        return self.scheduleNavigation(src, .{\n            .reason = .script,\n            .kind = .{ .push = null },\n        }, .{ .iframe = iframe });\n    }\n\n    iframe._executed = true;\n    const session = self._session;\n\n    const page_frame = try self.arena.create(Page);\n    const frame_id = session.nextFrameId();\n\n    try Page.init(page_frame, frame_id, session, self);\n    errdefer page_frame.deinit(true);\n\n    self._pending_loads += 1;\n    page_frame.iframe = iframe;\n    iframe._window = page_frame.window;\n    errdefer iframe._window = null;\n\n    // on first load, dispatch frame_created evnet\n    self._session.notification.dispatch(.page_frame_created, &.{\n        .frame_id = frame_id,\n        .parent_id = self._frame_id,\n        .timestamp = timestamp(.monotonic),\n    });\n\n    const url = blk: {\n        if (std.mem.eql(u8, src, \"about:blank\")) {\n            break :blk \"about:blank\"; // navigate will handle this special case\n        }\n        break :blk try URL.resolve(\n            self.call_arena, // ok to use, page.navigate dupes this\n            self.base(),\n            src,\n            .{ .encode = true },\n        );\n    };\n\n    page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| {\n        log.warn(.page, \"iframe navigate failure\", .{ .url = url, .err = err });\n        self._pending_loads -= 1;\n        iframe._window = null;\n        return error.IFrameLoadError;\n    };\n\n    // window[N] is based on document order. For now we'll just append the frame\n    // at the end of our list and set frames_sorted == false. window.getFrame\n    // will check this flag to decide if it needs to sort the frames or not.\n    // But, we can optimize this a bit. Since we expect frames to often be\n    // added in document order, we can do a quick check to see whether the list\n    // is sorted or not.\n    try self.frames.append(self.arena, page_frame);\n\n    const frames_len = self.frames.items.len;\n    if (frames_len == 1) {\n        // this is the only frame, it must be sorted.\n        return;\n    }\n\n    if (self.frames_sorted == false) {\n        // the list already wasn't sorted, it still isn't\n        return;\n    }\n\n    // So we added a frame into a sorted list. If this frame is sorted relative\n    // to the last frame, it's still sorted\n    const iframe_a = self.frames.items[frames_len - 2].iframe.?;\n    const iframe_b = self.frames.items[frames_len - 1].iframe.?;\n\n    if (iframe_a.asNode().compareDocumentPosition(iframe_b.asNode()) & 0x04 == 0) {\n        // if b followed a, then & 0x04 = 0x04\n        // but since we got 0, it means b does not follow a, and thus our list\n        // is no longer sorted.\n        self.frames_sorted = false;\n    }\n}\n\npub fn domChanged(self: *Page) void {\n    self.version += 1;\n\n    if (self._intersection_check_scheduled) {\n        return;\n    }\n\n    self._intersection_check_scheduled = true;\n    self.js.queueIntersectionChecks() catch |err| {\n        log.err(.page, \"page.schedIntersectChecks\", .{ .err = err, .type = self._type, .url = self.url });\n    };\n}\n\nconst ElementIdMaps = struct { lookup: *std.StringHashMapUnmanaged(*Element), removed_ids: *std.StringHashMapUnmanaged(void) };\n\nfn getElementIdMap(page: *Page, node: *Node) ElementIdMaps {\n    // Walk up the tree checking for ShadowRoot and tracking the root\n    var current = node;\n    while (true) {\n        if (current.is(ShadowRoot)) |shadow_root| {\n            return .{\n                .lookup = &shadow_root._elements_by_id,\n                .removed_ids = &shadow_root._removed_ids,\n            };\n        }\n\n        const parent = current._parent orelse {\n            if (current._type == .document) {\n                return .{\n                    .lookup = &current._type.document._elements_by_id,\n                    .removed_ids = &current._type.document._removed_ids,\n                };\n            }\n            // Detached nodes should not have IDs registered\n            if (IS_DEBUG) {\n                std.debug.assert(false);\n            }\n            return .{\n                .lookup = &page.document._elements_by_id,\n                .removed_ids = &page.document._removed_ids,\n            };\n        };\n\n        current = parent;\n    }\n}\n\npub fn addElementId(self: *Page, parent: *Node, element: *Element, id: []const u8) !void {\n    var id_maps = self.getElementIdMap(parent);\n    const gop = try id_maps.lookup.getOrPut(self.arena, id);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = element;\n        return;\n    }\n\n    const existing = gop.value_ptr.*.asNode();\n    switch (element.asNode().compareDocumentPosition(existing)) {\n        0x04 => gop.value_ptr.* = element,\n        else => {},\n    }\n}\n\npub fn removeElementId(self: *Page, element: *Element, id: []const u8) void {\n    const node = element.asNode();\n    self.removeElementIdWithMaps(self.getElementIdMap(node), id);\n}\n\npub fn removeElementIdWithMaps(self: *Page, id_maps: ElementIdMaps, id: []const u8) void {\n    if (id_maps.lookup.remove(id)) {\n        id_maps.removed_ids.put(self.arena, self.dupeString(id) catch return, {}) catch {};\n    }\n}\n\npub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Element {\n    if (node.isConnected() or node.isInShadowTree()) {\n        const lookup = self.getElementIdMap(node).lookup;\n        return lookup.get(id);\n    }\n    var tw = @import(\"webapi/TreeWalker.zig\").Full.Elements.init(node, .{});\n    while (tw.next()) |el| {\n        const element_id = el.getAttributeSafe(comptime .wrap(\"id\")) orelse continue;\n        if (std.mem.eql(u8, element_id, id)) {\n            return el;\n        }\n    }\n    return null;\n}\n\npub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {\n    return self._performance_observers.append(self.arena, observer);\n}\n\npub fn unregisterPerformanceObserver(self: *Page, observer: *PerformanceObserver) void {\n    for (self._performance_observers.items, 0..) |perf_observer, i| {\n        if (perf_observer == observer) {\n            _ = self._performance_observers.swapRemove(i);\n            return;\n        }\n    }\n}\n\n/// Updates performance observers with the new entry.\n/// This doesn't emit callbacks but rather fills the queues of observers.\npub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void {\n    for (self._performance_observers.items) |observer| {\n        if (observer.interested(entry)) {\n            observer._entries.append(self.arena, entry) catch |err| {\n                log.err(.page, \"notifyPerformanceObservers\", .{ .err = err, .type = self._type, .url = self.url });\n            };\n        }\n    }\n\n    try self.schedulePerformanceObserverDelivery();\n}\n\n/// Schedules async delivery of performance observer records.\npub fn schedulePerformanceObserverDelivery(self: *Page) !void {\n    // Already scheduled.\n    if (self._performance_delivery_scheduled) {\n        return;\n    }\n    self._performance_delivery_scheduled = true;\n\n    return self.js.scheduler.add(\n        self,\n        struct {\n            fn run(_page: *anyopaque) anyerror!?u32 {\n                const page: *Page = @ptrCast(@alignCast(_page));\n                page._performance_delivery_scheduled = false;\n\n                // Dispatch performance observer events.\n                for (page._performance_observers.items) |observer| {\n                    if (observer.hasRecords()) {\n                        try observer.dispatch(page);\n                    }\n                }\n\n                return null;\n            }\n        }.run,\n        0,\n        .{ .low_priority = true },\n    );\n}\n\npub fn registerMutationObserver(self: *Page, observer: *MutationObserver) !void {\n    self._mutation_observers.append(&observer.node);\n}\n\npub fn unregisterMutationObserver(self: *Page, observer: *MutationObserver) void {\n    self._mutation_observers.remove(&observer.node);\n}\n\npub fn registerIntersectionObserver(self: *Page, observer: *IntersectionObserver) !void {\n    try self._intersection_observers.append(self.arena, observer);\n}\n\npub fn unregisterIntersectionObserver(self: *Page, observer: *IntersectionObserver) void {\n    for (self._intersection_observers.items, 0..) |obs, i| {\n        if (obs == observer) {\n            _ = self._intersection_observers.swapRemove(i);\n            return;\n        }\n    }\n}\n\npub fn checkIntersections(self: *Page) !void {\n    for (self._intersection_observers.items) |observer| {\n        try observer.checkIntersections(self);\n    }\n}\n\npub fn dispatchLoad(self: *Page) !void {\n    const has_dom_load_listener = self._event_manager.has_dom_load_listener;\n    for (self._to_load.items) |html_element| {\n        if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) {\n            const event = try Event.initTrusted(comptime .wrap(\"load\"), .{}, self);\n            try self._event_manager.dispatch(html_element.asEventTarget(), event);\n        }\n    }\n    // We drained everything.\n    self._to_load.clearRetainingCapacity();\n}\n\npub fn scheduleMutationDelivery(self: *Page) !void {\n    if (self._mutation_delivery_scheduled) {\n        return;\n    }\n    self._mutation_delivery_scheduled = true;\n    try self.js.queueMutationDelivery();\n}\n\npub fn scheduleIntersectionDelivery(self: *Page) !void {\n    if (self._intersection_delivery_scheduled) {\n        return;\n    }\n    self._intersection_delivery_scheduled = true;\n    try self.js.queueIntersectionDelivery();\n}\n\npub fn scheduleSlotchangeDelivery(self: *Page) !void {\n    if (self._slotchange_delivery_scheduled) {\n        return;\n    }\n    self._slotchange_delivery_scheduled = true;\n    try self.js.queueSlotchangeDelivery();\n}\n\npub fn performScheduledIntersectionChecks(self: *Page) void {\n    if (!self._intersection_check_scheduled) {\n        return;\n    }\n    self._intersection_check_scheduled = false;\n    self.checkIntersections() catch |err| {\n        log.err(.page, \"page.schedIntersectChecks\", .{ .err = err, .type = self._type, .url = self.url });\n    };\n}\n\npub fn deliverIntersections(self: *Page) void {\n    if (!self._intersection_delivery_scheduled) {\n        return;\n    }\n    self._intersection_delivery_scheduled = false;\n\n    // Iterate backwards to handle observers that disconnect during their callback\n    var i = self._intersection_observers.items.len;\n    while (i > 0) {\n        i -= 1;\n        const observer = self._intersection_observers.items[i];\n        observer.deliverEntries(self) catch |err| {\n            log.err(.page, \"page.deliverIntersections\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n}\n\npub fn deliverMutations(self: *Page) void {\n    if (!self._mutation_delivery_scheduled) {\n        return;\n    }\n    self._mutation_delivery_scheduled = false;\n\n    self._mutation_delivery_depth += 1;\n    defer if (!self._mutation_delivery_scheduled) {\n        // reset the depth once nothing is left to be scheduled\n        self._mutation_delivery_depth = 0;\n    };\n\n    if (self._mutation_delivery_depth > 100) {\n        log.err(.page, \"page.MutationLimit\", .{ .type = self._type, .url = self.url });\n        self._mutation_delivery_depth = 0;\n        return;\n    }\n\n    var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;\n    while (it) |node| : (it = node.next) {\n        const observer: *MutationObserver = @fieldParentPtr(\"node\", node);\n        observer.deliverRecords(self) catch |err| {\n            log.err(.page, \"page.deliverMutations\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n}\n\npub fn deliverSlotchangeEvents(self: *Page) void {\n    if (!self._slotchange_delivery_scheduled) {\n        return;\n    }\n    self._slotchange_delivery_scheduled = false;\n\n    // we need to collect the pending slots, and then clear it and THEN exeute\n    // the slot change. We do this in case the slotchange event itself schedules\n    // more slot changes (which should only be executed on the next microtask)\n    const pending = self._slots_pending_slotchange.count();\n\n    var i: usize = 0;\n    var slots = self.call_arena.alloc(*Element.Html.Slot, pending) catch |err| {\n        log.err(.page, \"deliverSlotchange.append\", .{ .err = err, .type = self._type, .url = self.url });\n        return;\n    };\n\n    var it = self._slots_pending_slotchange.keyIterator();\n    while (it.next()) |slot| {\n        slots[i] = slot.*;\n        i += 1;\n    }\n    self._slots_pending_slotchange.clearRetainingCapacity();\n\n    for (slots) |slot| {\n        const event = Event.initTrusted(comptime .wrap(\"slotchange\"), .{ .bubbles = true }, self) catch |err| {\n            log.err(.page, \"deliverSlotchange.init\", .{ .err = err, .type = self._type, .url = self.url });\n            continue;\n        };\n        const target = slot.asNode().asEventTarget();\n        _ = target.dispatchEvent(event, self) catch |err| {\n            log.err(.page, \"deliverSlotchange.dispatch\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n}\n\npub fn notifyNetworkIdle(self: *Page) void {\n    lp.assert(self._notified_network_idle == .done, \"Page.notifyNetworkIdle\", .{});\n    self._session.notification.dispatch(.page_network_idle, &.{\n        .req_id = self._req_id,\n        .frame_id = self._frame_id,\n        .timestamp = timestamp(.monotonic),\n    });\n}\n\npub fn notifyNetworkAlmostIdle(self: *Page) void {\n    lp.assert(self._notified_network_almost_idle == .done, \"Page.notifyNetworkAlmostIdle\", .{});\n    self._session.notification.dispatch(.page_network_almost_idle, &.{\n        .req_id = self._req_id,\n        .frame_id = self._frame_id,\n        .timestamp = timestamp(.monotonic),\n    });\n}\n\n// called from the parser\npub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void {\n    const node = switch (child) {\n        .node => |n| n,\n        .text => |txt| blk: {\n            // If we're appending this adjacently to a text node, we should merge\n            if (parent.lastChild()) |sibling| {\n                if (sibling.is(CData.Text)) |tn| {\n                    const cdata = tn._proto;\n                    const existing = cdata.getData().str();\n                    cdata._data = try String.concat(self.arena, &.{ existing, txt });\n                    return;\n                }\n            }\n            break :blk try self.createTextNode(txt);\n        },\n    };\n\n    lp.assert(node._parent == null, \"Page.appendNew\", .{});\n    try self._insertNodeRelative(true, parent, node, .append, .{\n        // this opts has no meaning since we're passing `true` as the first\n        // parameter, which indicates this comes from the parser, and has its\n        // own special processing. Still, set it to be clear.\n        .child_already_connected = false,\n    });\n}\n\n// called from the parser when the node and all its children have been added\npub fn nodeComplete(self: *Page, node: *Node) !void {\n    Node.Build.call(node, \"complete\", .{ node, self }) catch |err| {\n        log.err(.bug, \"build.complete\", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type, .url = self.url });\n        return err;\n    };\n    return self.nodeIsReady(true, node);\n}\n\n// Sets the owner document for a node. Only stores entries for nodes whose owner\n// is NOT page.document to minimize memory overhead.\npub fn setNodeOwnerDocument(self: *Page, node: *Node, owner: *Document) !void {\n    if (owner == self.document) {\n        // No need to store if it's the main document - remove if present\n        _ = self._node_owner_documents.remove(node);\n    } else {\n        try self._node_owner_documents.put(self.arena, node, owner);\n    }\n}\n\n// Recursively sets the owner document for a node and all its descendants\npub fn adoptNodeTree(self: *Page, node: *Node, new_owner: *Document) !void {\n    try self.setNodeOwnerDocument(node, new_owner);\n    var it = node.childrenIterator();\n    while (it.next()) |child| {\n        try self.adoptNodeTree(child, new_owner);\n    }\n}\n\npub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node {\n    const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator;\n\n    switch (namespace) {\n        .html => {\n            switch (name.len) {\n                1 => switch (name[0]) {\n                    'p' => return self.createHtmlElementT(\n                        Element.Html.Paragraph,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    'a' => return self.createHtmlElementT(\n                        Element.Html.Anchor,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    'b' => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"b\", .{}) catch unreachable, ._tag = .b },\n                    ),\n                    'i' => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"i\", .{}) catch unreachable, ._tag = .i },\n                    ),\n                    'q' => return self.createHtmlElementT(\n                        Element.Html.Quote,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"q\", .{}) catch unreachable, ._tag = .quote },\n                    ),\n                    's' => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"s\", .{}) catch unreachable, ._tag = .s },\n                    ),\n                    else => {},\n                },\n                2 => switch (@as(u16, @bitCast(name[0..2].*))) {\n                    asUint(\"br\") => return self.createHtmlElementT(\n                        Element.Html.BR,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"ol\") => return self.createHtmlElementT(\n                        Element.Html.OL,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"ul\") => return self.createHtmlElementT(\n                        Element.Html.UL,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"li\") => return self.createHtmlElementT(\n                        Element.Html.LI,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"h1\") => return self.createHtmlElementT(\n                        Element.Html.Heading,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"h1\", .{}) catch unreachable, ._tag = .h1 },\n                    ),\n                    asUint(\"h2\") => return self.createHtmlElementT(\n                        Element.Html.Heading,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"h2\", .{}) catch unreachable, ._tag = .h2 },\n                    ),\n                    asUint(\"h3\") => return self.createHtmlElementT(\n                        Element.Html.Heading,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"h3\", .{}) catch unreachable, ._tag = .h3 },\n                    ),\n                    asUint(\"h4\") => return self.createHtmlElementT(\n                        Element.Html.Heading,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"h4\", .{}) catch unreachable, ._tag = .h4 },\n                    ),\n                    asUint(\"h5\") => return self.createHtmlElementT(\n                        Element.Html.Heading,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"h5\", .{}) catch unreachable, ._tag = .h5 },\n                    ),\n                    asUint(\"h6\") => return self.createHtmlElementT(\n                        Element.Html.Heading,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"h6\", .{}) catch unreachable, ._tag = .h6 },\n                    ),\n                    asUint(\"hr\") => return self.createHtmlElementT(\n                        Element.Html.HR,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"em\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"em\", .{}) catch unreachable, ._tag = .em },\n                    ),\n                    asUint(\"dd\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"dd\", .{}) catch unreachable, ._tag = .dd },\n                    ),\n                    asUint(\"dl\") => return self.createHtmlElementT(\n                        Element.Html.DList,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"dt\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"dt\", .{}) catch unreachable, ._tag = .dt },\n                    ),\n                    asUint(\"td\") => return self.createHtmlElementT(\n                        Element.Html.TableCell,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"td\", .{}) catch unreachable, ._tag = .td },\n                    ),\n                    asUint(\"th\") => return self.createHtmlElementT(\n                        Element.Html.TableCell,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"th\", .{}) catch unreachable, ._tag = .th },\n                    ),\n                    asUint(\"tr\") => return self.createHtmlElementT(\n                        Element.Html.TableRow,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    else => {},\n                },\n                3 => switch (@as(u24, @bitCast(name[0..3].*))) {\n                    asUint(\"div\") => return self.createHtmlElementT(\n                        Element.Html.Div,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"img\") => return self.createHtmlElementT(\n                        Element.Html.Image,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"nav\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"nav\", .{}) catch unreachable, ._tag = .nav },\n                    ),\n                    asUint(\"del\") => return self.createHtmlElementT(\n                        Element.Html.Mod,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"del\", .{}) catch unreachable, ._tag = .del },\n                    ),\n                    asUint(\"ins\") => return self.createHtmlElementT(\n                        Element.Html.Mod,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"ins\", .{}) catch unreachable, ._tag = .ins },\n                    ),\n                    asUint(\"col\") => return self.createHtmlElementT(\n                        Element.Html.TableCol,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"col\", .{}) catch unreachable, ._tag = .col },\n                    ),\n                    asUint(\"dir\") => return self.createHtmlElementT(\n                        Element.Html.Directory,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"map\") => return self.createHtmlElementT(\n                        Element.Html.Map,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"pre\") => return self.createHtmlElementT(\n                        Element.Html.Pre,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"sub\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"sub\", .{}) catch unreachable, ._tag = .sub },\n                    ),\n                    asUint(\"sup\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"sup\", .{}) catch unreachable, ._tag = .sup },\n                    ),\n                    asUint(\"dfn\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"dfn\", .{}) catch unreachable, ._tag = .dfn },\n                    ),\n                    else => {},\n                },\n                4 => switch (@as(u32, @bitCast(name[0..4].*))) {\n                    asUint(\"span\") => return self.createHtmlElementT(\n                        Element.Html.Span,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"meta\") => return self.createHtmlElementT(\n                        Element.Html.Meta,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"link\") => return self.createHtmlElementT(\n                        Element.Html.Link,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"slot\") => return self.createHtmlElementT(\n                        Element.Html.Slot,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"html\") => return self.createHtmlElementT(\n                        Element.Html.Html,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"head\") => return self.createHtmlElementT(\n                        Element.Html.Head,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"body\") => return self.createHtmlElementT(\n                        Element.Html.Body,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"form\") => return self.createHtmlElementT(\n                        Element.Html.Form,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"main\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"main\", .{}) catch unreachable, ._tag = .main },\n                    ),\n                    asUint(\"data\") => return self.createHtmlElementT(\n                        Element.Html.Data,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"base\") => {\n                        const n = try self.createHtmlElementT(\n                            Element.Html.Base,\n                            namespace,\n                            attribute_iterator,\n                            .{ ._proto = undefined },\n                        );\n\n                        // If page's base url is not already set, fill it with the base\n                        // tag.\n                        if (self.base_url == null) {\n                            if (n.as(Element).getAttributeSafe(comptime .wrap(\"href\"))) |href| {\n                                self.base_url = try URL.resolve(self.arena, self.url, href, .{});\n                            }\n                        }\n\n                        return n;\n                    },\n                    asUint(\"menu\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"menu\", .{}) catch unreachable, ._tag = .menu },\n                    ),\n                    asUint(\"area\") => return self.createHtmlElementT(\n                        Element.Html.Area,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"font\") => return self.createHtmlElementT(\n                        Element.Html.Font,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"code\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"code\", .{}) catch unreachable, ._tag = .code },\n                    ),\n                    asUint(\"time\") => return self.createHtmlElementT(\n                        Element.Html.Time,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    else => {},\n                },\n                5 => switch (@as(u40, @bitCast(name[0..5].*))) {\n                    asUint(\"input\") => return self.createHtmlElementT(\n                        Element.Html.Input,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"style\") => return self.createHtmlElementT(\n                        Element.Html.Style,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"title\") => return self.createHtmlElementT(\n                        Element.Html.Title,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"embed\") => return self.createHtmlElementT(\n                        Element.Html.Embed,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"audio\") => return self.createHtmlMediaElementT(\n                        Element.Html.Media.Audio,\n                        namespace,\n                        attribute_iterator,\n                    ),\n                    asUint(\"video\") => return self.createHtmlMediaElementT(\n                        Element.Html.Media.Video,\n                        namespace,\n                        attribute_iterator,\n                    ),\n                    asUint(\"aside\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"aside\", .{}) catch unreachable, ._tag = .aside },\n                    ),\n                    asUint(\"label\") => return self.createHtmlElementT(\n                        Element.Html.Label,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"meter\") => return self.createHtmlElementT(\n                        Element.Html.Meter,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"param\") => return self.createHtmlElementT(\n                        Element.Html.Param,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"table\") => return self.createHtmlElementT(\n                        Element.Html.Table,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"thead\") => return self.createHtmlElementT(\n                        Element.Html.TableSection,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"thead\", .{}) catch unreachable, ._tag = .thead },\n                    ),\n                    asUint(\"tbody\") => return self.createHtmlElementT(\n                        Element.Html.TableSection,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"tbody\", .{}) catch unreachable, ._tag = .tbody },\n                    ),\n                    asUint(\"tfoot\") => return self.createHtmlElementT(\n                        Element.Html.TableSection,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"tfoot\", .{}) catch unreachable, ._tag = .tfoot },\n                    ),\n                    asUint(\"track\") => return self.createHtmlElementT(\n                        Element.Html.Track,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._kind = comptime .wrap(\"subtitles\"), ._ready_state = .none },\n                    ),\n                    else => {},\n                },\n                6 => switch (@as(u48, @bitCast(name[0..6].*))) {\n                    asUint(\"script\") => return self.createHtmlElementT(\n                        Element.Html.Script,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"button\") => return self.createHtmlElementT(\n                        Element.Html.Button,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"canvas\") => return self.createHtmlElementT(\n                        Element.Html.Canvas,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"dialog\") => return self.createHtmlElementT(\n                        Element.Html.Dialog,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"legend\") => return self.createHtmlElementT(\n                        Element.Html.Legend,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"object\") => return self.createHtmlElementT(\n                        Element.Html.Object,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"output\") => return self.createHtmlElementT(\n                        Element.Html.Output,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"source\") => return self.createHtmlElementT(\n                        Element.Html.Source,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"strong\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"strong\", .{}) catch unreachable, ._tag = .strong },\n                    ),\n                    asUint(\"header\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"header\", .{}) catch unreachable, ._tag = .header },\n                    ),\n                    asUint(\"footer\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"footer\", .{}) catch unreachable, ._tag = .footer },\n                    ),\n                    asUint(\"select\") => return self.createHtmlElementT(\n                        Element.Html.Select,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"option\") => return self.createHtmlElementT(\n                        Element.Html.Option,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"iframe\") => return self.createHtmlElementT(\n                        IFrame,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"figure\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"figure\", .{}) catch unreachable, ._tag = .figure },\n                    ),\n                    asUint(\"hgroup\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"hgroup\", .{}) catch unreachable, ._tag = .hgroup },\n                    ),\n                    else => {},\n                },\n                7 => switch (@as(u56, @bitCast(name[0..7].*))) {\n                    asUint(\"section\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"section\", .{}) catch unreachable, ._tag = .section },\n                    ),\n                    asUint(\"article\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"article\", .{}) catch unreachable, ._tag = .article },\n                    ),\n                    asUint(\"details\") => return self.createHtmlElementT(\n                        Element.Html.Details,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"summary\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"summary\", .{}) catch unreachable, ._tag = .summary },\n                    ),\n                    asUint(\"caption\") => return self.createHtmlElementT(\n                        Element.Html.TableCaption,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"marquee\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"marquee\", .{}) catch unreachable, ._tag = .marquee },\n                    ),\n                    asUint(\"address\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"address\", .{}) catch unreachable, ._tag = .address },\n                    ),\n                    asUint(\"picture\") => return self.createHtmlElementT(\n                        Element.Html.Picture,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    else => {},\n                },\n                8 => switch (@as(u64, @bitCast(name[0..8].*))) {\n                    asUint(\"textarea\") => return self.createHtmlElementT(\n                        Element.Html.TextArea,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"template\") => return self.createHtmlElementT(\n                        Element.Html.Template,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._content = undefined },\n                    ),\n                    asUint(\"colgroup\") => return self.createHtmlElementT(\n                        Element.Html.TableCol,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"colgroup\", .{}) catch unreachable, ._tag = .colgroup },\n                    ),\n                    asUint(\"fieldset\") => return self.createHtmlElementT(\n                        Element.Html.FieldSet,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"optgroup\") => return self.createHtmlElementT(\n                        Element.Html.OptGroup,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"progress\") => return self.createHtmlElementT(\n                        Element.Html.Progress,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"datalist\") => return self.createHtmlElementT(\n                        Element.Html.DataList,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined },\n                    ),\n                    asUint(\"noscript\") => return self.createHtmlElementT(\n                        Element.Html.Generic,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"noscript\", .{}) catch unreachable, ._tag = .noscript },\n                    ),\n                    else => {},\n                },\n                10 => switch (@as(u80, @bitCast(name[0..10].*))) {\n                    asUint(\"blockquote\") => return self.createHtmlElementT(\n                        Element.Html.Quote,\n                        namespace,\n                        attribute_iterator,\n                        .{ ._proto = undefined, ._tag_name = String.init(undefined, \"blockquote\", .{}) catch unreachable, ._tag = .blockquote },\n                    ),\n                    else => {},\n                },\n                else => {},\n            }\n            const tag_name = try String.init(self.arena, name, .{});\n\n            // Check if this is a custom element (must have hyphen for HTML namespace)\n            const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null;\n            if (has_hyphen and namespace == .html) {\n                const definition = self.window._custom_elements._definitions.get(name);\n                const node = try self.createHtmlElementT(Element.Html.Custom, namespace, attribute_iterator, .{\n                    ._proto = undefined,\n                    ._tag_name = tag_name,\n                    ._definition = definition,\n                });\n\n                const def = definition orelse {\n                    const element = node.as(Element);\n                    const custom = element.is(Element.Html.Custom).?;\n                    try self._undefined_custom_elements.append(self.arena, custom);\n                    return node;\n                };\n\n                // Save and restore upgrading element to allow nested createElement calls\n                const prev_upgrading = self._upgrading_element;\n                self._upgrading_element = node;\n                defer self._upgrading_element = prev_upgrading;\n\n                var ls: JS.Local.Scope = undefined;\n                self.js.localScope(&ls);\n                defer ls.deinit();\n\n                if (from_parser) {\n                    // There are some things custom elements aren't allowed to do\n                    // when we're parsing.\n                    self.document._throw_on_dynamic_markup_insertion_counter += 1;\n                }\n                defer if (from_parser) {\n                    self.document._throw_on_dynamic_markup_insertion_counter -= 1;\n                };\n\n                var caught: JS.TryCatch.Caught = undefined;\n                _ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| {\n                    log.warn(.js, \"custom element constructor\", .{ .name = name, .err = err, .caught = caught, .type = self._type, .url = self.url });\n                    return node;\n                };\n\n                // After constructor runs, invoke attributeChangedCallback for initial attributes\n                const element = node.as(Element);\n                if (element._attributes) |attributes| {\n                    var it = attributes.iterator();\n                    while (it.next()) |attr| {\n                        Element.Html.Custom.invokeAttributeChangedCallbackOnElement(\n                            element,\n                            attr._name,\n                            null, // old_value is null for initial attributes\n                            attr._value,\n                            self,\n                        );\n                    }\n                }\n\n                return node;\n            }\n\n            return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name });\n        },\n        .svg => {\n            const tag_name = try String.init(self.arena, name, .{});\n            if (std.ascii.eqlIgnoreCase(name, \"svg\")) {\n                return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{\n                    ._proto = undefined,\n                    ._type = .svg,\n                    ._tag_name = tag_name,\n                });\n            }\n\n            // Other SVG elements (rect, circle, text, g, etc.)\n            const lower = std.ascii.lowerString(&self.buf, name);\n            const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown;\n            return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag });\n        },\n        else => {\n            const tag_name = try String.init(self.arena, name, .{});\n            return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name });\n        },\n    }\n}\n\nfn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype, html_element: E) !*Node {\n    const html_element_ptr = try self._factory.htmlElement(html_element);\n    const element = html_element_ptr.asElement();\n    element._namespace = namespace;\n    try self.populateElementAttributes(element, attribute_iterator);\n\n    // Check for customized built-in element via \"is\" attribute\n    try Element.Html.Custom.checkAndAttachBuiltIn(element, self);\n\n    const node = element.asNode();\n    if (@hasDecl(E, \"Build\") and @hasDecl(E.Build, \"created\")) {\n        @call(.auto, @field(E.Build, \"created\"), .{ node, self }) catch |err| {\n            log.err(.page, \"build.created\", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type, .url = self.url });\n            return err;\n        };\n    }\n    return node;\n}\n\nfn createHtmlMediaElementT(self: *Page, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node {\n    const media_element = try self._factory.htmlMediaElement(E{ ._proto = undefined });\n    const element = media_element.asElement();\n    element._namespace = namespace;\n    try self.populateElementAttributes(element, attribute_iterator);\n    return element.asNode();\n}\n\nfn createSvgElementT(self: *Page, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node {\n    const svg_element_ptr = try self._factory.svgElement(tag_name, svg_element);\n    var element = svg_element_ptr.asElement();\n    element._namespace = .svg;\n    try self.populateElementAttributes(element, attribute_iterator);\n    return element.asNode();\n}\n\nfn populateElementAttributes(self: *Page, element: *Element, list: anytype) !void {\n    if (@TypeOf(list) == ?*Element.Attribute.List) {\n        // from cloneNode\n\n        var existing = list orelse return;\n\n        var attributes = try self.arena.create(Element.Attribute.List);\n        attributes.* = .{\n            .normalize = existing.normalize,\n        };\n\n        var it = existing.iterator();\n        while (it.next()) |attr| {\n            try attributes.putNew(attr._name.str(), attr._value.str(), self);\n        }\n        element._attributes = attributes;\n        return;\n    }\n\n    // from the parser\n    if (@TypeOf(list) == @TypeOf(null) or list.count() == 0) {\n        return;\n    }\n    var attributes = try element.createAttributeList(self);\n    while (list.next()) |attr| {\n        try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self);\n    }\n}\n\npub fn createTextNode(self: *Page, text: []const u8) !*Node {\n    const cd = try self._factory.node(CData{\n        ._proto = undefined,\n        ._type = .{ .text = .{\n            ._proto = undefined,\n        } },\n        ._data = try self.dupeSSO(text),\n    });\n    cd._type.text._proto = cd;\n    return cd.asNode();\n}\n\npub fn createComment(self: *Page, text: []const u8) !*Node {\n    const cd = try self._factory.node(CData{\n        ._proto = undefined,\n        ._type = .{ .comment = .{\n            ._proto = undefined,\n        } },\n        ._data = try self.dupeSSO(text),\n    });\n    cd._type.comment._proto = cd;\n    return cd.asNode();\n}\n\npub fn createCDATASection(self: *Page, data: []const u8) !*Node {\n    // Validate that the data doesn't contain \"]]>\"\n    if (std.mem.indexOf(u8, data, \"]]>\") != null) {\n        return error.InvalidCharacterError;\n    }\n\n    // First allocate the Text node separately\n    const text_node = try self._factory.create(CData.Text{\n        ._proto = undefined,\n    });\n\n    // Then create the CData with cdata_section variant\n    const cd = try self._factory.node(CData{\n        ._proto = undefined,\n        ._type = .{ .cdata_section = .{\n            ._proto = text_node,\n        } },\n        ._data = try self.dupeSSO(data),\n    });\n\n    // Set up the back pointer from Text to CData\n    text_node._proto = cd;\n\n    return cd.asNode();\n}\n\npub fn createProcessingInstruction(self: *Page, target: []const u8, data: []const u8) !*Node {\n    // Validate neither target nor data contain \"?>\"\n    if (std.mem.indexOf(u8, target, \"?>\") != null) {\n        return error.InvalidCharacterError;\n    }\n    if (std.mem.indexOf(u8, data, \"?>\") != null) {\n        return error.InvalidCharacterError;\n    }\n\n    // Validate target follows XML Name production\n    try validateXmlName(target);\n\n    const owned_target = try self.dupeString(target);\n\n    const pi = try self._factory.create(CData.ProcessingInstruction{\n        ._proto = undefined,\n        ._target = owned_target,\n    });\n\n    const cd = try self._factory.node(CData{\n        ._proto = undefined,\n        ._type = .{ .processing_instruction = pi },\n        ._data = try self.dupeSSO(data),\n    });\n\n    // Set up the back pointer from ProcessingInstruction to CData\n    pi._proto = cd;\n\n    return cd.asNode();\n}\n\n/// Validate a string against the XML Name production.\n/// https://www.w3.org/TR/xml/#NT-Name\nfn validateXmlName(name: []const u8) !void {\n    if (name.len == 0) return error.InvalidCharacterError;\n\n    var i: usize = 0;\n\n    // First character must be a NameStartChar.\n    const first_len = std.unicode.utf8ByteSequenceLength(name[0]) catch\n        return error.InvalidCharacterError;\n    if (first_len > name.len) return error.InvalidCharacterError;\n    const first_cp = std.unicode.utf8Decode(name[0..][0..first_len]) catch\n        return error.InvalidCharacterError;\n    if (!isXmlNameStartChar(first_cp)) return error.InvalidCharacterError;\n    i = first_len;\n\n    // Subsequent characters must be NameChars.\n    while (i < name.len) {\n        const cp_len = std.unicode.utf8ByteSequenceLength(name[i]) catch\n            return error.InvalidCharacterError;\n        if (i + cp_len > name.len) return error.InvalidCharacterError;\n        const cp = std.unicode.utf8Decode(name[i..][0..cp_len]) catch\n            return error.InvalidCharacterError;\n        if (!isXmlNameChar(cp)) return error.InvalidCharacterError;\n        i += cp_len;\n    }\n}\n\nfn isXmlNameStartChar(c: u21) bool {\n    return c == ':' or\n        (c >= 'A' and c <= 'Z') or\n        c == '_' or\n        (c >= 'a' and c <= 'z') or\n        (c >= 0xC0 and c <= 0xD6) or\n        (c >= 0xD8 and c <= 0xF6) or\n        (c >= 0xF8 and c <= 0x2FF) or\n        (c >= 0x370 and c <= 0x37D) or\n        (c >= 0x37F and c <= 0x1FFF) or\n        (c >= 0x200C and c <= 0x200D) or\n        (c >= 0x2070 and c <= 0x218F) or\n        (c >= 0x2C00 and c <= 0x2FEF) or\n        (c >= 0x3001 and c <= 0xD7FF) or\n        (c >= 0xF900 and c <= 0xFDCF) or\n        (c >= 0xFDF0 and c <= 0xFFFD) or\n        (c >= 0x10000 and c <= 0xEFFFF);\n}\n\nfn isXmlNameChar(c: u21) bool {\n    return isXmlNameStartChar(c) or\n        c == '-' or\n        c == '.' or\n        (c >= '0' and c <= '9') or\n        c == 0xB7 or\n        (c >= 0x300 and c <= 0x36F) or\n        (c >= 0x203F and c <= 0x2040);\n}\n\npub fn dupeString(self: *Page, value: []const u8) ![]const u8 {\n    if (String.intern(value)) |v| {\n        return v;\n    }\n    return self.arena.dupe(u8, value);\n}\n\npub fn dupeSSO(self: *Page, value: []const u8) !String {\n    return String.init(self.arena, value, .{ .dupe = true });\n}\n\nconst RemoveNodeOpts = struct {\n    will_be_reconnected: bool,\n};\npub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts) void {\n    // Capture siblings before removing\n    const previous_sibling = child.previousSibling();\n    const next_sibling = child.nextSibling();\n\n    // Capture child's index before removal for live range updates (DOM spec remove steps 4-7)\n    const child_index_for_ranges: ?u32 = if (self._live_ranges.first != null)\n        parent.getChildIndex(child)\n    else\n        null;\n\n    const children = parent._children.?;\n    switch (children.*) {\n        .one => |n| {\n            lp.assert(n == child, \"Page.removeNode.one\", .{});\n            parent._children = null;\n            self._factory.destroy(children);\n        },\n        .list => |list| {\n            list.remove(&child._child_link);\n\n            // Should not be possible to get a child list with a single node.\n            // While it doesn't cause any problems, it indicates an bug in the\n            // code as these should always be represented as .{.one = node}\n            const first = list.first.?;\n            if (first.next == null) {\n                children.* = .{ .one = Node.linkToNode(first) };\n                self._factory.destroy(list);\n            }\n        },\n    }\n    // grab this before we null the parent\n    const was_connected = child.isConnected();\n    // Capture the ID map before disconnecting, so we can remove IDs from the correct document\n    const id_maps = if (was_connected) self.getElementIdMap(child) else null;\n\n    child._parent = null;\n    child._child_link = .{};\n\n    // Update live ranges for removal (DOM spec remove steps 4-7)\n    if (child_index_for_ranges) |idx| {\n        self.updateRangesForNodeRemoval(parent, child, idx);\n    }\n\n    // Handle slot assignment removal before mutation observers\n    if (child.is(Element)) |el| {\n        // Check if the parent was a shadow host\n        if (parent.is(Element)) |parent_el| {\n            if (self._element_shadow_roots.get(parent_el)) |shadow_root| {\n                // Signal slot changes for any affected slots\n                const slot_name = el.getAttributeSafe(comptime .wrap(\"slot\")) orelse \"\";\n                var tw = @import(\"webapi/TreeWalker.zig\").Full.Elements.init(shadow_root.asNode(), .{});\n                while (tw.next()) |slot_el| {\n                    if (slot_el.is(Element.Html.Slot)) |slot| {\n                        if (std.mem.eql(u8, slot.getName(), slot_name)) {\n                            self.signalSlotChange(slot);\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n        // Remove from assigned slot lookup\n        _ = self._element_assigned_slots.remove(el);\n    }\n\n    if (self.hasMutationObservers()) {\n        const removed = [_]*Node{child};\n        self.childListChange(parent, &.{}, &removed, previous_sibling, next_sibling);\n    }\n\n    if (opts.will_be_reconnected) {\n        // We might be removing the node only to re-insert it. If the node will\n        // remain connected, we can skip the expensive process of fully\n        // disconnecting it.\n        return;\n    }\n\n    if (was_connected == false) {\n        // If the child wasn't connected, then there should be nothing left for\n        // us to do\n        return;\n    }\n\n    // The child was connected and now it no longer is. We need to \"disconnect\"\n    // it and all of its descendants. For now \"disconnect\" just means updating\n    // the ID map and invoking disconnectedCallback for custom elements\n    var tw = @import(\"webapi/TreeWalker.zig\").Full.Elements.init(child, .{});\n    while (tw.next()) |el| {\n        if (el.getAttributeSafe(comptime .wrap(\"id\"))) |id| {\n            self.removeElementIdWithMaps(id_maps.?, id);\n        }\n\n        Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);\n    }\n}\n\npub fn appendNode(self: *Page, parent: *Node, child: *Node, opts: InsertNodeOpts) !void {\n    return self._insertNodeRelative(false, parent, child, .append, opts);\n}\n\npub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void {\n    self.domChanged();\n    const dest_connected = target.isConnected();\n\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        // Check if child was connected BEFORE removing it from parent\n        const child_was_connected = child.isConnected();\n        self.removeNode(parent, child, .{ .will_be_reconnected = dest_connected });\n        try self.appendNode(target, child, .{ .child_already_connected = child_was_connected });\n    }\n}\n\npub fn insertAllChildrenBefore(self: *Page, fragment: *Node, parent: *Node, ref_node: *Node) !void {\n    self.domChanged();\n    const dest_connected = parent.isConnected();\n\n    var it = fragment.childrenIterator();\n    while (it.next()) |child| {\n        // Check if child was connected BEFORE removing it from fragment\n        const child_was_connected = child.isConnected();\n        self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });\n        try self.insertNodeRelative(\n            parent,\n            child,\n            .{ .before = ref_node },\n            .{ .child_already_connected = child_was_connected },\n        );\n    }\n}\n\nconst InsertNodeRelative = union(enum) {\n    append,\n    after: *Node,\n    before: *Node,\n};\nconst InsertNodeOpts = struct {\n    child_already_connected: bool = false,\n    adopting_to_new_document: bool = false,\n};\npub fn insertNodeRelative(self: *Page, parent: *Node, child: *Node, relative: InsertNodeRelative, opts: InsertNodeOpts) !void {\n    return self._insertNodeRelative(false, parent, child, relative, opts);\n}\npub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, relative: InsertNodeRelative, opts: InsertNodeOpts) !void {\n    // caller should have made sure this was the case\n\n    lp.assert(child._parent == null, \"Page.insertNodeRelative parent\", .{});\n\n    const children = blk: {\n        // expand parent._children so that it can take another child\n        if (parent._children) |c| {\n            switch (c.*) {\n                .list => {},\n                .one => |node| {\n                    const list = try self._factory.create(std.DoublyLinkedList{});\n                    list.append(&node._child_link);\n                    c.* = .{ .list = list };\n                },\n            }\n            break :blk c;\n        } else {\n            const Children = @import(\"webapi/children.zig\").Children;\n            const c = try self._factory.create(Children{ .one = child });\n            parent._children = c;\n            break :blk c;\n        }\n    };\n\n    switch (relative) {\n        .append => switch (children.*) {\n            .one => {}, // already set in the expansion above\n            .list => |list| list.append(&child._child_link),\n        },\n        .after => |ref_node| {\n            // caller should have made sure this was the case\n            lp.assert(ref_node._parent.? == parent, \"Page.insertNodeRelative after\", .{ .url = self.url });\n            // if ref_node is in parent, and expanded _children above to\n            // accommodate another child, then `children` must be a list\n            children.list.insertAfter(&ref_node._child_link, &child._child_link);\n        },\n        .before => |ref_node| {\n            // caller should have made sure this was the case\n            lp.assert(ref_node._parent.? == parent, \"Page.insertNodeRelative before\", .{ .url = self.url });\n            // if ref_node is in parent, and expanded _children above to\n            // accommodate another child, then `children` must be a list\n            children.list.insertBefore(&ref_node._child_link, &child._child_link);\n        },\n    }\n    child._parent = parent;\n\n    // Update live ranges for insertion (DOM spec insert step 6).\n    // For .before/.after the child was inserted at a specific position;\n    // ranges on parent with offsets past that position must be incremented.\n    // For .append no range update is needed (spec: \"if child is non-null\").\n    if (self._live_ranges.first != null) {\n        switch (relative) {\n            .append => {},\n            .before, .after => {\n                if (parent.getChildIndex(child)) |idx| {\n                    self.updateRangesForNodeInsertion(parent, idx);\n                }\n            },\n        }\n    }\n\n    const parent_is_connected = parent.isConnected();\n\n    // Tri-state behavior for mutations:\n    // 1. from_parser=true, parse_mode=document -> no mutations (initial document parse)\n    // 2. from_parser=true, parse_mode=fragment -> mutations (innerHTML additions)\n    // 3. from_parser=false, parse_mode=document -> mutation (js manipulation)\n    // split like this because from_parser can be comptime known.\n    const should_notify = if (comptime from_parser)\n        self._parse_mode == .fragment\n    else\n        true;\n\n    if (should_notify) {\n        if (comptime from_parser == false) {\n            // When the parser adds the node, nodeIsReady is only called when the\n            // nodeComplete() callback is executed.\n            try self.nodeIsReady(false, child);\n\n            // Check if text was added to a script that hasn't started yet.\n            if (child._type == .cdata and parent_is_connected) {\n                if (parent.is(Element.Html.Script)) |script| {\n                    if (!script._executed) {\n                        try self.nodeIsReady(false, parent);\n                    }\n                }\n            }\n        }\n\n        // Notify mutation observers about childList change\n        if (self.hasMutationObservers()) {\n            const previous_sibling = child.previousSibling();\n            const next_sibling = child.nextSibling();\n            const added = [_]*Node{child};\n            self.childListChange(parent, &added, &.{}, previous_sibling, next_sibling);\n        }\n    }\n\n    if (comptime from_parser) {\n        if (child.is(Element)) |el| {\n            // Invoke connectedCallback for custom elements during parsing\n            // For main document parsing, we know nodes are connected (fast path)\n            // For fragment parsing (innerHTML), we need to check connectivity\n            if (child.isConnected() or child.isInShadowTree()) {\n                if (el.getAttributeSafe(comptime .wrap(\"id\"))) |id| {\n                    try self.addElementId(parent, el, id);\n                }\n                try Element.Html.Custom.invokeConnectedCallbackOnElement(true, el, self);\n            }\n        }\n        return;\n    }\n\n    // Update slot assignments for the inserted child if parent is a shadow host\n    // This needs to happen even if the element isn't connected to the document\n    if (child.is(Element)) |el| {\n        self.updateElementAssignedSlot(el);\n    }\n\n    if (opts.child_already_connected and !opts.adopting_to_new_document) {\n        // The child is already connected in the same document, we don't have to reconnect it\n        return;\n    }\n\n    const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree();\n\n    if (!parent_in_shadow and !parent_is_connected) {\n        return;\n    }\n\n    // If we're here, it means either:\n    // 1. A disconnected child became connected (parent.isConnected() == true)\n    // 2. Child is being added to a shadow tree (parent_in_shadow == true)\n    // In both cases, we need to update ID maps and invoke callbacks\n\n    // Only invoke connectedCallback if the root child is transitioning from\n    // disconnected to connected. When that happens, all descendants should also\n    // get connectedCallback invoked (they're becoming connected as a group).\n    const should_invoke_connected = parent_is_connected and !opts.child_already_connected;\n\n    var tw = @import(\"webapi/TreeWalker.zig\").Full.Elements.init(child, .{});\n    while (tw.next()) |el| {\n        if (el.getAttributeSafe(comptime .wrap(\"id\"))) |id| {\n            try self.addElementId(el.asNode()._parent.?, el, id);\n        }\n\n        if (should_invoke_connected) {\n            try Element.Html.Custom.invokeConnectedCallbackOnElement(false, el, self);\n        }\n    }\n}\n\npub fn attributeChange(self: *Page, element: *Element, name: String, value: String, old_value: ?String) void {\n    _ = Element.Build.call(element, \"attributeChange\", .{ element, name, value, self }) catch |err| {\n        log.err(.bug, \"build.attributeChange\", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type, .url = self.url });\n    };\n\n    Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self);\n\n    var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;\n    while (it) |node| : (it = node.next) {\n        const observer: *MutationObserver = @fieldParentPtr(\"node\", node);\n        observer.notifyAttributeChange(element, name, old_value, self) catch |err| {\n            log.err(.page, \"attributeChange.notifyObserver\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n\n    // Handle slot assignment changes\n    if (name.eql(comptime .wrap(\"slot\"))) {\n        self.updateSlotAssignments(element);\n    } else if (name.eql(comptime .wrap(\"name\"))) {\n        // Check if this is a slot element\n        if (element.is(Element.Html.Slot)) |slot| {\n            self.signalSlotChange(slot);\n        }\n    }\n}\n\npub fn attributeRemove(self: *Page, element: *Element, name: String, old_value: String) void {\n    _ = Element.Build.call(element, \"attributeRemove\", .{ element, name, self }) catch |err| {\n        log.err(.bug, \"build.attributeRemove\", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type, .url = self.url });\n    };\n\n    Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self);\n\n    var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;\n    while (it) |node| : (it = node.next) {\n        const observer: *MutationObserver = @fieldParentPtr(\"node\", node);\n        observer.notifyAttributeChange(element, name, old_value, self) catch |err| {\n            log.err(.page, \"attributeRemove.notifyObserver\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n\n    // Handle slot assignment changes\n    if (name.eql(comptime .wrap(\"slot\"))) {\n        self.updateSlotAssignments(element);\n    } else if (name.eql(comptime .wrap(\"name\"))) {\n        // Check if this is a slot element\n        if (element.is(Element.Html.Slot)) |slot| {\n            self.signalSlotChange(slot);\n        }\n    }\n}\n\nfn signalSlotChange(self: *Page, slot: *Element.Html.Slot) void {\n    self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| {\n        log.err(.page, \"signalSlotChange.put\", .{ .err = err, .type = self._type, .url = self.url });\n        return;\n    };\n    self.scheduleSlotchangeDelivery() catch |err| {\n        log.err(.page, \"signalSlotChange.schedule\", .{ .err = err, .type = self._type, .url = self.url });\n    };\n}\n\nfn updateSlotAssignments(self: *Page, element: *Element) void {\n    // Find all slots in the shadow root that might be affected\n    const parent = element.asNode()._parent orelse return;\n\n    // Check if parent is a shadow host\n    const parent_el = parent.is(Element) orelse return;\n    _ = self._element_shadow_roots.get(parent_el) orelse return;\n\n    // Signal change for the old slot (if any)\n    if (self._element_assigned_slots.get(element)) |old_slot| {\n        self.signalSlotChange(old_slot);\n    }\n\n    // Update the assignedSlot lookup to the new slot\n    self.updateElementAssignedSlot(element);\n\n    // Signal change for the new slot (if any)\n    if (self._element_assigned_slots.get(element)) |new_slot| {\n        self.signalSlotChange(new_slot);\n    }\n}\n\nfn updateElementAssignedSlot(self: *Page, element: *Element) void {\n    // Remove old assignment\n    _ = self._element_assigned_slots.remove(element);\n\n    // Find the new assigned slot\n    const parent = element.asNode()._parent orelse return;\n    const parent_el = parent.is(Element) orelse return;\n    const shadow_root = self._element_shadow_roots.get(parent_el) orelse return;\n\n    const slot_name = element.getAttributeSafe(comptime .wrap(\"slot\")) orelse \"\";\n\n    // Recursively search through the shadow root for a matching slot\n    if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {\n        self._element_assigned_slots.put(self.arena, element, slot) catch |err| {\n            log.err(.page, \"updateElementAssignedSlot.put\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n}\n\nfn findMatchingSlot(node: *Node, slot_name: []const u8) ?*Element.Html.Slot {\n    // Check if this node is a matching slot\n    if (node.is(Element)) |el| {\n        if (el.is(Element.Html.Slot)) |slot| {\n            if (std.mem.eql(u8, slot.getName(), slot_name)) {\n                return slot;\n            }\n        }\n    }\n\n    // Search children\n    var it = node.childrenIterator();\n    while (it.next()) |child| {\n        if (findMatchingSlot(child, slot_name)) |slot| {\n            return slot;\n        }\n    }\n\n    return null;\n}\n\npub fn hasMutationObservers(self: *const Page) bool {\n    return self._mutation_observers.first != null;\n}\n\npub fn getCustomizedBuiltInDefinition(self: *Page, element: *Element) ?*CustomElementDefinition {\n    return self._customized_builtin_definitions.get(element);\n}\n\npub fn setCustomizedBuiltInDefinition(self: *Page, element: *Element, definition: *CustomElementDefinition) !void {\n    try self._customized_builtin_definitions.put(self.arena, element, definition);\n}\n\npub fn characterDataChange(\n    self: *Page,\n    target: *Node,\n    old_value: String,\n) void {\n    var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;\n    while (it) |node| : (it = node.next) {\n        const observer: *MutationObserver = @fieldParentPtr(\"node\", node);\n        observer.notifyCharacterDataChange(target, old_value, self) catch |err| {\n            log.err(.page, \"cdataChange.notifyObserver\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n}\n\npub fn childListChange(\n    self: *Page,\n    target: *Node,\n    added_nodes: []const *Node,\n    removed_nodes: []const *Node,\n    previous_sibling: ?*Node,\n    next_sibling: ?*Node,\n) void {\n    // Filter out HTML wrapper element during fragment parsing (html5ever quirk)\n    if (self._parse_mode == .fragment and added_nodes.len == 1) {\n        if (added_nodes[0].is(Element.Html.Html) != null) {\n            // This is the temporary HTML wrapper, added by html5ever\n            // that will be unwrapped, see:\n            // https://github.com/servo/html5ever/issues/583\n            return;\n        }\n    }\n\n    var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;\n    while (it) |node| : (it = node.next) {\n        const observer: *MutationObserver = @fieldParentPtr(\"node\", node);\n        observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| {\n            log.err(.page, \"childListChange.notifyObserver\", .{ .err = err, .type = self._type, .url = self.url });\n        };\n    }\n}\n\n// --- Live range update methods (DOM spec §4.2.3, §4.2.4, §4.7, §4.8) ---\n\n/// Update all live ranges after a replaceData mutation on a CharacterData node.\n/// Per DOM spec: insertData = replaceData(offset, 0, data),\n///               deleteData = replaceData(offset, count, \"\").\n/// All parameters are in UTF-16 code unit offsets.\npub fn updateRangesForCharacterDataReplace(self: *Page, target: *Node, offset: u32, count: u32, data_len: u32) void {\n    var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first;\n    while (it) |link| : (it = link.next) {\n        const ar: *AbstractRange = @fieldParentPtr(\"_range_link\", link);\n        ar.updateForCharacterDataReplace(target, offset, count, data_len);\n    }\n}\n\n/// Update all live ranges after a splitText operation.\n/// Steps 7b-7e of the DOM spec splitText algorithm.\n/// Steps 7d-7e complement (not overlap) updateRangesForNodeInsertion:\n/// the insert update handles offsets > child_index, while 7d/7e handle\n/// offsets == node_index+1 (these are equal values but with > vs == checks).\npub fn updateRangesForSplitText(self: *Page, target: *Node, new_node: *Node, offset: u32, parent: *Node, node_index: u32) void {\n    var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first;\n    while (it) |link| : (it = link.next) {\n        const ar: *AbstractRange = @fieldParentPtr(\"_range_link\", link);\n        ar.updateForSplitText(target, new_node, offset, parent, node_index);\n    }\n}\n\n/// Update all live ranges after a node insertion.\n/// Per DOM spec insert algorithm step 6: only applies when inserting before a\n/// non-null reference node.\npub fn updateRangesForNodeInsertion(self: *Page, parent: *Node, child_index: u32) void {\n    var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first;\n    while (it) |link| : (it = link.next) {\n        const ar: *AbstractRange = @fieldParentPtr(\"_range_link\", link);\n        ar.updateForNodeInsertion(parent, child_index);\n    }\n}\n\n/// Update all live ranges after a node removal.\n/// Per DOM spec remove algorithm steps 4-7.\npub fn updateRangesForNodeRemoval(self: *Page, parent: *Node, child: *Node, child_index: u32) void {\n    var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first;\n    while (it) |link| : (it = link.next) {\n        const ar: *AbstractRange = @fieldParentPtr(\"_range_link\", link);\n        ar.updateForNodeRemoval(parent, child, child_index);\n    }\n}\n\n// TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '')\npub fn parseHtmlAsChildren(self: *Page, node: *Node, html: []const u8) !void {\n    const previous_parse_mode = self._parse_mode;\n    self._parse_mode = .fragment;\n    defer self._parse_mode = previous_parse_mode;\n\n    var parser = Parser.init(self.call_arena, node, self);\n    parser.parseFragment(html);\n\n    // https://github.com/servo/html5ever/issues/583\n    const children = node._children orelse return;\n    const first = children.one;\n    lp.assert(first.is(Element.Html.Html) != null, \"Page.parseHtmlAsChildren root\", .{ .type = first._type });\n    node._children = first._children;\n\n    if (self.hasMutationObservers()) {\n        var it = node.childrenIterator();\n        while (it.next()) |child| {\n            child._parent = node;\n            // Notify mutation observers for each unwrapped child\n            const previous_sibling = child.previousSibling();\n            const next_sibling = child.nextSibling();\n            const added = [_]*Node{child};\n            self.childListChange(node, &added, &.{}, previous_sibling, next_sibling);\n        }\n    } else {\n        var it = node.childrenIterator();\n        while (it.next()) |child| {\n            child._parent = node;\n        }\n    }\n}\n\nfn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {\n    if ((comptime from_parser) and self._parse_mode == .fragment) {\n        // we don't execute scripts added via innerHTML = '<script...';\n        return;\n    }\n    if (node.is(Element.Html.Script)) |script| {\n        if ((comptime from_parser == false) and script._src.len == 0) {\n            // Script was added via JavaScript without a src attribute.\n            // Only skip if it has no inline content either — scripts with\n            // textContent/text should still execute per spec.\n            if (node.firstChild() == null) {\n                return;\n            }\n        }\n\n        self.scriptAddedCallback(from_parser, script) catch |err| {\n            log.err(.page, \"page.nodeIsReady\", .{ .err = err, .element = \"script\", .type = self._type, .url = self.url });\n            return err;\n        };\n    } else if (node.is(IFrame)) |iframe| {\n        self.iframeAddedCallback(iframe) catch |err| {\n            log.err(.page, \"page.nodeIsReady\", .{ .err = err, .element = \"iframe\", .type = self._type, .url = self.url });\n            return err;\n        };\n    } else if (node.is(Element.Html.Link)) |link| {\n        link.linkAddedCallback(self) catch |err| {\n            log.err(.page, \"page.nodeIsReady\", .{ .err = err, .element = \"link\", .type = self._type });\n            return error.LinkLoadError;\n        };\n    } else if (node.is(Element.Html.Style)) |style| {\n        style.styleAddedCallback(self) catch |err| {\n            log.err(.page, \"page.nodeIsReady\", .{ .err = err, .element = \"style\", .type = self._type });\n            return error.StyleLoadError;\n        };\n    }\n}\n\nconst ParseState = union(enum) {\n    pre,\n    complete,\n    err: anyerror,\n    html: std.ArrayList(u8),\n    text: std.ArrayList(u8),\n    image: std.ArrayList(u8),\n    raw: std.ArrayList(u8),\n    raw_done: []const u8,\n};\n\nconst LoadState = enum {\n    // waiting for the main HTML\n    waiting,\n\n    // the main HTML is being parsed (or downloaded)\n    parsing,\n\n    // the main HTML has been parsed and the JavaScript (including deferred\n    // scripts) have been loaded. Corresponds to the DOMContentLoaded event\n    load,\n\n    // the page has been loaded and all async scripts (if any) are done\n    // Corresponds to the load event\n    complete,\n};\n\nconst IdleNotification = union(enum) {\n    // hasn't started yet.\n    init,\n\n    // timestamp where the state was first triggered. If the state stays\n    // true (e.g. 0 nework activity for NetworkIdle, or <= 2 for NetworkAlmostIdle)\n    // for 500ms, it'll send the notification and transition to .done. If\n    // the state doesn't stay true, it'll revert to .init.\n    triggered: u64,\n\n    // notification sent - should never be reset\n    done,\n\n    // Returns `true` if we should send a notification. Only returns true if it\n    // was previously triggered 500+ milliseconds ago.\n    // active == true when the condition for the notification is true\n    // active == false when the condition for the notification is false\n    pub fn check(self: *IdleNotification, active: bool) bool {\n        if (active) {\n            switch (self.*) {\n                .done => {\n                    // Notification was already sent.\n                },\n                .init => {\n                    // This is the first time the condition was triggered (or\n                    // the first time after being un-triggered). Record the time\n                    // so that if the condition holds for long enough, we can\n                    // send a notification.\n                    self.* = .{ .triggered = milliTimestamp(.monotonic) };\n                },\n                .triggered => |ms| {\n                    // The condition was already triggered and was triggered\n                    // again. When this condition holds for 500+ms, we'll send\n                    // a notification.\n                    if (milliTimestamp(.monotonic) - ms >= 500) {\n                        // This is the only place in this function where we can\n                        // return true. The only place where we can tell our caller\n                        // \"send the notification!\".\n                        self.* = .done;\n                        return true;\n                    }\n                    // the state hasn't held for 500ms.\n                },\n            }\n        } else {\n            switch (self.*) {\n                .done => {\n                    // The condition became false, but we already sent the notification\n                    // There's nothing we can do, it stays .done. We never re-send\n                    // a notification or \"undo\" a sent notification (not that we can).\n                },\n                .init => {\n                    // The condition remains false\n                },\n                .triggered => {\n                    // The condition _had_ been true, and we were waiting (500ms)\n                    // for it to hold, but it hasn't. So we go back to waiting.\n                    self.* = .init;\n                },\n            }\n        }\n\n        // See above for the only case where we ever return true. All other\n        // paths go here. This means \"don't send the notification\". Maybe\n        // because it's already been sent, maybe because active is false, or\n        // maybe because the condition hasn't held long enough.\n        return false;\n    }\n};\n\npub const NavigateReason = enum {\n    anchor,\n    address_bar,\n    form,\n    script,\n    history,\n    navigation,\n    initialFrameNavigation,\n};\n\npub const NavigateOpts = struct {\n    cdp_id: ?i64 = null,\n    reason: NavigateReason = .address_bar,\n    method: HttpClient.Method = .GET,\n    body: ?[]const u8 = null,\n    header: ?[:0]const u8 = null,\n    force: bool = false,\n    kind: NavigationKind = .{ .push = null },\n};\n\npub const NavigatedOpts = struct {\n    cdp_id: ?i64 = null,\n    reason: NavigateReason = .address_bar,\n    method: HttpClient.Method = .GET,\n};\n\nconst NavigationType = enum {\n    form,\n    script,\n    anchor,\n    iframe,\n};\n\nconst Navigation = union(NavigationType) {\n    form: *Page,\n    script: ?*Page,\n    anchor: *Page,\n    iframe: *IFrame,\n};\n\npub const QueuedNavigation = struct {\n    arena: Allocator,\n    url: [:0]const u8,\n    opts: NavigateOpts,\n    is_about_blank: bool,\n    navigation_type: NavigationType,\n};\n\n/// Resolves a target attribute value (e.g., \"_self\", \"_parent\", \"_top\", or frame name)\n/// to the appropriate Page to navigate.\n/// Returns null if the target is \"_blank\" (which would open a new window/tab).\n/// Note: Callers should handle empty target separately (for owner document resolution).\npub fn resolveTargetPage(self: *Page, target_name: []const u8) ?*Page {\n    if (std.ascii.eqlIgnoreCase(target_name, \"_self\")) {\n        return self;\n    }\n\n    if (std.ascii.eqlIgnoreCase(target_name, \"_blank\")) {\n        return null;\n    }\n\n    if (std.ascii.eqlIgnoreCase(target_name, \"_parent\")) {\n        return self.parent orelse self;\n    }\n\n    if (std.ascii.eqlIgnoreCase(target_name, \"_top\")) {\n        var page = self;\n        while (page.parent) |p| {\n            page = p;\n        }\n        return page;\n    }\n\n    // Named frame lookup: search current page's descendants first, then from root\n    // This follows the HTML spec's \"implementation-defined\" search order.\n    if (findFrameByName(self, target_name)) |frame_page| {\n        return frame_page;\n    }\n\n    // If not found in descendants, search from root (catches siblings and ancestors' descendants)\n    var root = self;\n    while (root.parent) |p| {\n        root = p;\n    }\n    if (root != self) {\n        if (findFrameByName(root, target_name)) |frame_page| {\n            return frame_page;\n        }\n    }\n\n    // If no frame found with that name, navigate in current page\n    // (this matches browser behavior - unknown targets act like _self)\n    return self;\n}\n\nfn findFrameByName(page: *Page, name: []const u8) ?*Page {\n    for (page.frames.items) |frame| {\n        if (frame.iframe) |iframe| {\n            const frame_name = iframe.asElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n            if (std.mem.eql(u8, frame_name, name)) {\n                return frame;\n            }\n        }\n        // Recursively search child frames\n        if (findFrameByName(frame, name)) |found| {\n            return found;\n        }\n    }\n    return null;\n}\n\npub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {\n    const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"page mouse click\", .{\n            .url = self.url,\n            .node = target,\n            .x = x,\n            .y = y,\n            .type = self._type,\n        });\n    }\n    const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap(\"click\"), .{\n        .bubbles = true,\n        .cancelable = true,\n        .composed = true,\n        .clientX = x,\n        .clientY = y,\n    }, self);\n    try self._event_manager.dispatch(target.asEventTarget(), mouse_event.asEvent());\n}\n\n// callback when the \"click\" event reaches the pages.\npub fn handleClick(self: *Page, target: *Node) !void {\n    // TODO: Also support <area> elements when implement\n    const element = target.is(Element) orelse return;\n    const html_element = element.is(Element.Html) orelse return;\n\n    switch (html_element._type) {\n        .anchor => |anchor| {\n            const href = element.getAttributeSafe(comptime .wrap(\"href\")) orelse return;\n            if (href.len == 0) {\n                return;\n            }\n\n            if (std.mem.startsWith(u8, href, \"javascript:\")) {\n                return;\n            }\n\n            if (try element.hasAttribute(comptime .wrap(\"download\"), self)) {\n                log.warn(.browser, \"a.download\", .{ .type = self._type, .url = self.url });\n                return;\n            }\n\n            const target_page = blk: {\n                const target_name = anchor.getTarget();\n                if (target_name.len == 0) {\n                    break :blk target.ownerPage(self);\n                }\n                break :blk self.resolveTargetPage(target_name) orelse {\n                    log.warn(.not_implemented, \"target\", .{ .type = self._type, .url = self.url, .target = target_name });\n                    return;\n                };\n            };\n\n            try element.focus(self);\n            try self.scheduleNavigation(href, .{\n                .reason = .script,\n                .kind = .{ .push = null },\n            }, .{ .anchor = target_page });\n        },\n        .input => |input| {\n            try element.focus(self);\n            if (input._input_type == .submit) {\n                return self.submitForm(element, input.getForm(self), .{});\n            }\n        },\n        .button => |button| {\n            try element.focus(self);\n            if (std.mem.eql(u8, button.getType(), \"submit\")) {\n                return self.submitForm(element, button.getForm(self), .{});\n            }\n        },\n        .select, .textarea => try element.focus(self),\n        else => {},\n    }\n}\n\npub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {\n    const event = keyboard_event.asEvent();\n    const element = self.window._document._active_element orelse {\n        keyboard_event.deinit(false, self._session);\n        return;\n    };\n\n    if (comptime IS_DEBUG) {\n        log.debug(.page, \"page keydown\", .{\n            .url = self.url,\n            .node = element,\n            .key = keyboard_event._key,\n            .type = self._type,\n        });\n    }\n    try self._event_manager.dispatch(element.asEventTarget(), event);\n}\n\npub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {\n    const keyboard_event = event.is(KeyboardEvent) orelse return;\n    const key = keyboard_event.getKey();\n\n    if (key == .Dead) {\n        return;\n    }\n\n    if (target.is(Element.Html.Input)) |input| {\n        if (key == .Enter) {\n            return self.submitForm(input.asElement(), input.getForm(self), .{});\n        }\n\n        // Don't handle text input for radio/checkbox\n        const input_type = input._input_type;\n        if (input_type == .radio or input_type == .checkbox) {\n            return;\n        }\n\n        // Handle printable characters\n        if (key.isPrintable()) {\n            try input.innerInsert(key.asString(), self);\n        }\n        return;\n    }\n\n    if (target.is(Element.Html.TextArea)) |textarea| {\n        // zig fmt: off\n        const append =\n            if (key == .Enter) \"\\n\"\n            else if (key.isPrintable()) key.asString()\n            else return\n        ;\n        // zig fmt: on\n        return textarea.innerInsert(append, self);\n    }\n}\n\nconst SubmitFormOpts = struct {\n    fire_event: bool = true,\n};\npub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form, submit_opts: SubmitFormOpts) !void {\n    const form = form_ orelse return;\n\n    if (submitter_) |submitter| {\n        if (submitter.getAttributeSafe(comptime .wrap(\"disabled\")) != null) {\n            return;\n        }\n    }\n\n    if (self.canScheduleNavigation(.form) == false) {\n        return;\n    }\n\n    const form_element = form.asElement();\n\n    const target_name_: ?[]const u8 = blk: {\n        if (submitter_) |submitter| {\n            if (submitter.getAttributeSafe(comptime .wrap(\"formtarget\"))) |ft| {\n                break :blk ft;\n            }\n        }\n        break :blk form_element.getAttributeSafe(comptime .wrap(\"target\"));\n    };\n\n    const target_page = blk: {\n        const target_name = target_name_ orelse {\n            break :blk form_element.asNode().ownerPage(self);\n        };\n        break :blk self.resolveTargetPage(target_name) orelse {\n            log.warn(.not_implemented, \"target\", .{ .type = self._type, .url = self.url, .target = target_name });\n            return;\n        };\n    };\n\n    if (submit_opts.fire_event) {\n        const submit_event = try Event.initTrusted(comptime .wrap(\"submit\"), .{ .bubbles = true, .cancelable = true }, self);\n\n        // so submit_event is still valid when we check _prevent_default\n        submit_event.acquireRef();\n        defer submit_event.deinit(false, self._session);\n\n        try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);\n        // If the submit event was prevented, don't submit the form\n        if (submit_event._prevent_default) {\n            return;\n        }\n    }\n\n    const FormData = @import(\"webapi/net/FormData.zig\");\n    // The submitter can be an input box (if enter was entered on the box)\n    // I don't think this is technically correct, but FormData handles it ok\n    const form_data = try FormData.init(form, submitter_, self);\n\n    const arena = try self._session.getArena(.{ .debug = \"submitForm\" });\n    errdefer self._session.releaseArena(arena);\n\n    const encoding = form_element.getAttributeSafe(comptime .wrap(\"enctype\"));\n\n    var buf = std.Io.Writer.Allocating.init(arena);\n    try form_data.write(encoding, &buf.writer);\n\n    const method = form_element.getAttributeSafe(comptime .wrap(\"method\")) orelse \"\";\n    var action = form_element.getAttributeSafe(comptime .wrap(\"action\")) orelse self.url;\n\n    var opts = NavigateOpts{\n        .reason = .form,\n        .kind = .{ .push = null },\n    };\n    if (std.ascii.eqlIgnoreCase(method, \"post\")) {\n        opts.method = .POST;\n        opts.body = buf.written();\n        // form_data.write currently only supports this encoding, so we know this has to be the content type\n        opts.header = \"Content-Type: application/x-www-form-urlencoded\";\n    } else {\n        action = try URL.concatQueryString(arena, action, buf.written());\n    }\n\n    return self.scheduleNavigationWithArena(arena, action, opts, .{ .form = target_page });\n}\n\n// insertText is a shortcut to insert text into the active element.\npub fn insertText(self: *Page, v: []const u8) !void {\n    const html_element = self.document._active_element orelse return;\n\n    if (html_element.is(Element.Html.Input)) |input| {\n        const input_type = input._input_type;\n        if (input_type == .radio or input_type == .checkbox) {\n            return;\n        }\n\n        return input.innerInsert(v, self);\n    }\n\n    if (html_element.is(Element.Html.TextArea)) |textarea| {\n        return textarea.innerInsert(v, self);\n    }\n}\n\nconst RequestCookieOpts = struct {\n    is_http: bool = true,\n    is_navigation: bool = false,\n};\npub fn requestCookie(self: *const Page, opts: RequestCookieOpts) HttpClient.RequestCookie {\n    return .{\n        .jar = &self._session.cookie_jar,\n        .origin = self.url,\n        .is_http = opts.is_http,\n        .is_navigation = opts.is_navigation,\n    };\n}\n\nfn asUint(comptime string: anytype) std.meta.Int(\n    .unsigned,\n    @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0\n) {\n    const byteLength = @sizeOf(@TypeOf(string.*)) - 1;\n    const expectedType = *const [byteLength:0]u8;\n    if (@TypeOf(string) != expectedType) {\n        @compileError(\"expected : \" ++ @typeName(expectedType) ++ \", got: \" ++ @typeName(@TypeOf(string)));\n    }\n\n    return @bitCast(@as(*const [byteLength]u8, string).*);\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"WebApi: Page\" {\n    const filter: testing.LogFilter = .init(&.{ .http, .js });\n    defer filter.deinit();\n\n    try testing.htmlRunner(\"page\", .{});\n}\n\ntest \"WebApi: Frames\" {\n    const filter: testing.LogFilter = .init(&.{.js});\n    defer filter.deinit();\n\n    try testing.htmlRunner(\"frames\", .{});\n}\n\ntest \"WebApi: Integration\" {\n    try testing.htmlRunner(\"integration\", .{});\n}\n"
  },
  {
    "path": "src/browser/ScriptManager.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst builtin = @import(\"builtin\");\n\nconst log = @import(\"../log.zig\");\nconst HttpClient = @import(\"HttpClient.zig\");\nconst net_http = @import(\"../network/http.zig\");\nconst String = @import(\"../string.zig\").String;\n\nconst js = @import(\"js/js.zig\");\nconst URL = @import(\"URL.zig\");\nconst Page = @import(\"Page.zig\");\nconst Browser = @import(\"Browser.zig\");\n\nconst Element = @import(\"webapi/Element.zig\");\n\nconst Allocator = std.mem.Allocator;\nconst ArrayList = std.ArrayList;\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\nconst ScriptManager = @This();\n\npage: *Page,\n\n// used to prevent recursive evaluation\nis_evaluating: bool,\n\n// Only once this is true can deferred scripts be run\nstatic_scripts_done: bool,\n\n// List of async scripts. We don't care about the execution order of these, but\n// on shutdown/abort, we need to cleanup any pending ones.\nasync_scripts: std.DoublyLinkedList,\n\n// List of deferred scripts. These must be executed in order, but only once\n// dom_loaded == true,\ndefer_scripts: std.DoublyLinkedList,\n\n// When an async script is ready, it's queued here. We played with executing\n// them as they complete, but it can cause timing issues with v8 module loading.\nready_scripts: std.DoublyLinkedList,\n\nshutdown: bool = false,\n\nclient: *HttpClient,\nallocator: Allocator,\n\n// We can download multiple sync modules in parallel, but we want to process\n// them in order. We can't use an std.DoublyLinkedList, like the other script types,\n// because the order we load them might not be the order we want to process\n// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't\n// make any guarantees about the list of sub-module dependencies it gives us\n// So this is more like a cache. When an imported module is completed, its\n// source is placed here (keyed by the full url) for some point in the future\n// when v8 asks for it.\n// The type is confusing (too confusing? move to a union). Starts of as `null`\n// then transitions to either an error (from errorCalback) or the completed\n// buffer from doneCallback\nimported_modules: std.StringHashMapUnmanaged(ImportedModule),\n\n// Mapping between module specifier and resolution.\n// see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap\n// importmap contains resolved urls.\nimportmap: std.StringHashMapUnmanaged([:0]const u8),\n\n// have we notified the page that all scripts are loaded (used to fire the \"load\"\n// event).\npage_notified_of_completion: bool,\n\npub fn init(allocator: Allocator, http_client: *HttpClient, page: *Page) ScriptManager {\n    return .{\n        .page = page,\n        .async_scripts = .{},\n        .defer_scripts = .{},\n        .ready_scripts = .{},\n        .importmap = .empty,\n        .is_evaluating = false,\n        .allocator = allocator,\n        .imported_modules = .empty,\n        .client = http_client,\n        .static_scripts_done = false,\n        .page_notified_of_completion = false,\n    };\n}\n\npub fn deinit(self: *ScriptManager) void {\n    // necessary to free any arenas scripts may be referencing\n    self.reset();\n\n    self.imported_modules.deinit(self.allocator);\n    // we don't deinit self.importmap b/c we use the page's arena for its\n    // allocations.\n}\n\npub fn reset(self: *ScriptManager) void {\n    var it = self.imported_modules.valueIterator();\n    while (it.next()) |value_ptr| {\n        switch (value_ptr.state) {\n            .done => |script| script.deinit(),\n            else => {},\n        }\n    }\n    self.imported_modules.clearRetainingCapacity();\n\n    // Our allocator is the page arena, it's been reset. We cannot use\n    // clearAndRetainCapacity, since that space is no longer ours\n    self.importmap = .empty;\n\n    clearList(&self.defer_scripts);\n    clearList(&self.async_scripts);\n    clearList(&self.ready_scripts);\n    self.static_scripts_done = false;\n}\n\nfn clearList(list: *std.DoublyLinkedList) void {\n    while (list.popFirst()) |n| {\n        const script: *Script = @fieldParentPtr(\"node\", n);\n        script.deinit();\n    }\n}\n\nfn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers {\n    var headers = try self.client.newHeaders();\n    try self.page.headersForRequest(arena, url, &headers);\n    return headers;\n}\n\npub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {\n    if (script_element._executed) {\n        // If a script tag gets dynamically created and added to the dom:\n        //    document.getElementsByTagName('head')[0].appendChild(script)\n        // that script tag will immediately get executed by our scriptAddedCallback.\n        // However, if the location where the script tag is inserted happens to be\n        // below where processHTMLDoc currently is, then we'll re-run that same script\n        // again in processHTMLDoc. This flag is used to let us know if a specific\n        // <script> has already been processed.\n        return;\n    }\n\n    const element = script_element.asElement();\n    if (element.getAttributeSafe(comptime .wrap(\"nomodule\")) != null) {\n        // these scripts should only be loaded if we don't support modules\n        // but since we do support modules, we can just skip them.\n        return;\n    }\n\n    const kind: Script.Kind = blk: {\n        const script_type = element.getAttributeSafe(comptime .wrap(\"type\")) orelse break :blk .javascript;\n        if (script_type.len == 0) {\n            break :blk .javascript;\n        }\n        if (std.ascii.eqlIgnoreCase(script_type, \"application/javascript\")) {\n            break :blk .javascript;\n        }\n        if (std.ascii.eqlIgnoreCase(script_type, \"text/javascript\")) {\n            break :blk .javascript;\n        }\n        if (std.ascii.eqlIgnoreCase(script_type, \"module\")) {\n            break :blk .module;\n        }\n        if (std.ascii.eqlIgnoreCase(script_type, \"importmap\")) {\n            break :blk .importmap;\n        }\n\n        // \"type\" could be anything, but only the above are ones we need to process.\n        // Common other ones are application/json, application/ld+json, text/template\n\n        return;\n    };\n\n    var handover = false;\n    const page = self.page;\n\n    const arena = try page.getArena(.{ .debug = \"addFromElement\" });\n    errdefer if (!handover) {\n        page.releaseArena(arena);\n    };\n\n    var source: Script.Source = undefined;\n    var remote_url: ?[:0]const u8 = null;\n    const base_url = page.base();\n    if (element.getAttributeSafe(comptime .wrap(\"src\"))) |src| {\n        if (try parseDataURI(arena, src)) |data_uri| {\n            source = .{ .@\"inline\" = data_uri };\n        } else {\n            remote_url = try URL.resolve(arena, base_url, src, .{});\n            source = .{ .remote = .{} };\n        }\n    } else {\n        var buf = std.Io.Writer.Allocating.init(arena);\n        try element.asNode().getChildTextContent(&buf.writer);\n        try buf.writer.writeByte(0);\n        const data = buf.written();\n        const inline_source: [:0]const u8 = data[0 .. data.len - 1 :0];\n        if (inline_source.len == 0) {\n            // we haven't set script_element._executed = true yet, which is good.\n            // If content is appended to the script, we will execute it then.\n            page.releaseArena(arena);\n            return;\n        }\n        source = .{ .@\"inline\" = inline_source };\n    }\n\n    // Only set _executed (already-started) when we actually have content to execute\n    script_element._executed = true;\n    const is_inline = source == .@\"inline\";\n\n    const script = try arena.create(Script);\n    script.* = .{\n        .kind = kind,\n        .node = .{},\n        .arena = arena,\n        .manager = self,\n        .source = source,\n        .script_element = script_element,\n        .complete = is_inline,\n        .status = if (is_inline) 200 else 0,\n        .url = remote_url orelse base_url,\n        .mode = blk: {\n            if (source == .@\"inline\") {\n                break :blk if (kind == .module) .@\"defer\" else .normal;\n            }\n\n            if (element.getAttributeSafe(comptime .wrap(\"async\")) != null) {\n                break :blk .async;\n            }\n\n            // Check for defer or module (before checking dynamic script default)\n            if (kind == .module or element.getAttributeSafe(comptime .wrap(\"defer\")) != null) {\n                break :blk .@\"defer\";\n            }\n\n            // For dynamically-inserted scripts (not from parser), default to async\n            // unless async was explicitly set to false (which removes the attribute)\n            // and defer was set to true (checked above)\n            if (comptime !from_parser) {\n                // Script has src and no explicit async/defer attributes\n                // Per HTML spec, dynamically created scripts default to async\n                break :blk .async;\n            }\n\n            break :blk .normal;\n        },\n    };\n\n    const is_blocking = script.mode == .normal;\n    if (is_blocking == false) {\n        self.scriptList(script).append(&script.node);\n    }\n\n    if (remote_url) |url| {\n        errdefer {\n            if (is_blocking == false) {\n                self.scriptList(script).remove(&script.node);\n            }\n            // Let the outer errdefer handle releasing the arena if client.request fails\n        }\n\n        try self.client.request(.{\n            .url = url,\n            .ctx = script,\n            .method = .GET,\n            .frame_id = page._frame_id,\n            .headers = try self.getHeaders(arena, url),\n            .blocking = is_blocking,\n            .cookie_jar = &page._session.cookie_jar,\n            .resource_type = .script,\n            .notification = page._session.notification,\n            .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,\n            .header_callback = Script.headerCallback,\n            .data_callback = Script.dataCallback,\n            .done_callback = Script.doneCallback,\n            .error_callback = Script.errorCallback,\n        });\n        handover = true;\n\n        if (comptime IS_DEBUG) {\n            var ls: js.Local.Scope = undefined;\n            page.js.localScope(&ls);\n            defer ls.deinit();\n\n            log.debug(.http, \"script queue\", .{\n                .ctx = ctx,\n                .url = remote_url.?,\n                .element = element,\n                .stack = ls.local.stackTrace() catch \"???\",\n            });\n        }\n    }\n\n    if (is_blocking == false) {\n        return;\n    }\n\n    // this is <script src=\"...\"></script>, it needs to block the caller\n    // until it's evaluated\n    var client = self.client;\n    while (true) {\n        if (!script.complete) {\n            _ = try client.tick(200);\n            continue;\n        }\n        if (script.status == 0) {\n            // an error (that we already logged)\n            script.deinit();\n            return;\n        }\n\n        // could have already been evaluating if this is dynamically added\n        const was_evaluating = self.is_evaluating;\n        self.is_evaluating = true;\n        defer {\n            self.is_evaluating = was_evaluating;\n            script.deinit();\n        }\n        return script.eval(page);\n    }\n}\n\nfn scriptList(self: *ScriptManager, script: *const Script) *std.DoublyLinkedList {\n    return switch (script.mode) {\n        .normal => unreachable, // not added to a list, executed immediately\n        .@\"defer\" => &self.defer_scripts,\n        .async, .import_async, .import => &self.async_scripts,\n    };\n}\n\n// Resolve a module specifier to an valid URL.\npub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, base: [:0]const u8, specifier: [:0]const u8) ![:0]const u8 {\n    // If the specifier is mapped in the importmap, return the pre-resolved value.\n    if (self.importmap.get(specifier)) |s| {\n        return s;\n    }\n\n    return URL.resolve(arena, base, specifier, .{ .always_dupe = true });\n}\n\npub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {\n    const gop = try self.imported_modules.getOrPut(self.allocator, url);\n    if (gop.found_existing) {\n        gop.value_ptr.waiters += 1;\n        return;\n    }\n    errdefer _ = self.imported_modules.remove(url);\n\n    const page = self.page;\n    const arena = try page.getArena(.{ .debug = \"preloadImport\" });\n    errdefer page.releaseArena(arena);\n\n    const script = try arena.create(Script);\n    script.* = .{\n        .kind = .module,\n        .arena = arena,\n        .url = url,\n        .node = .{},\n        .manager = self,\n        .complete = false,\n        .script_element = null,\n        .source = .{ .remote = .{} },\n        .mode = .import,\n    };\n\n    gop.value_ptr.* = ImportedModule{};\n\n    if (comptime IS_DEBUG) {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        log.debug(.http, \"script queue\", .{\n            .url = url,\n            .ctx = \"module\",\n            .referrer = referrer,\n            .stack = ls.local.stackTrace() catch \"???\",\n        });\n    }\n\n    // This seems wrong since we're not dealing with an async import (unlike\n    // getAsyncModule below), but all we're trying to do here is pre-load the\n    // script for execution at some point in the future (when waitForImport is\n    // called).\n    self.async_scripts.append(&script.node);\n\n    self.client.request(.{\n        .url = url,\n        .ctx = script,\n        .method = .GET,\n        .frame_id = page._frame_id,\n        .headers = try self.getHeaders(arena, url),\n        .cookie_jar = &page._session.cookie_jar,\n        .resource_type = .script,\n        .notification = page._session.notification,\n        .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,\n        .header_callback = Script.headerCallback,\n        .data_callback = Script.dataCallback,\n        .done_callback = Script.doneCallback,\n        .error_callback = Script.errorCallback,\n    }) catch |err| {\n        self.async_scripts.remove(&script.node);\n        return err;\n    };\n}\n\npub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {\n    const entry = self.imported_modules.getEntry(url) orelse {\n        // It shouldn't be possible for v8 to ask for a module that we didn't\n        // `preloadImport` above.\n        return error.UnknownModule;\n    };\n\n    const was_evaluating = self.is_evaluating;\n    self.is_evaluating = true;\n    defer self.is_evaluating = was_evaluating;\n\n    var client = self.client;\n    while (true) {\n        switch (entry.value_ptr.state) {\n            .loading => {\n                _ = try client.tick(200);\n                continue;\n            },\n            .done => |script| {\n                var shared = false;\n                const buffer = entry.value_ptr.buffer;\n                const waiters = entry.value_ptr.waiters;\n\n                if (waiters == 1) {\n                    self.imported_modules.removeByPtr(entry.key_ptr);\n                } else {\n                    shared = true;\n                    entry.value_ptr.waiters = waiters - 1;\n                }\n                return .{\n                    .buffer = buffer,\n                    .shared = shared,\n                    .script = script,\n                };\n            },\n            .err => return error.Failed,\n        }\n    }\n}\n\npub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {\n    const page = self.page;\n    const arena = try page.getArena(.{ .debug = \"getAsyncImport\" });\n    errdefer page.releaseArena(arena);\n\n    const script = try arena.create(Script);\n    script.* = .{\n        .kind = .module,\n        .arena = arena,\n        .url = url,\n        .node = .{},\n        .manager = self,\n        .complete = false,\n        .script_element = null,\n        .source = .{ .remote = .{} },\n        .mode = .{ .import_async = .{\n            .callback = cb,\n            .data = cb_data,\n        } },\n    };\n\n    if (comptime IS_DEBUG) {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        log.debug(.http, \"script queue\", .{\n            .url = url,\n            .ctx = \"dynamic module\",\n            .referrer = referrer,\n            .stack = ls.local.stackTrace() catch \"???\",\n        });\n    }\n\n    // It's possible, but unlikely, for client.request to immediately finish\n    // a request, thus calling our callback. We generally don't want a call\n    // from v8 (which is why we're here), to result in a new script evaluation.\n    // So we block even the slightest change that `client.request` immediately\n    // executes a callback.\n    const was_evaluating = self.is_evaluating;\n    self.is_evaluating = true;\n    defer self.is_evaluating = was_evaluating;\n\n    self.async_scripts.append(&script.node);\n    self.client.request(.{\n        .url = url,\n        .method = .GET,\n        .frame_id = page._frame_id,\n        .headers = try self.getHeaders(arena, url),\n        .ctx = script,\n        .resource_type = .script,\n        .cookie_jar = &page._session.cookie_jar,\n        .notification = page._session.notification,\n        .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,\n        .header_callback = Script.headerCallback,\n        .data_callback = Script.dataCallback,\n        .done_callback = Script.doneCallback,\n        .error_callback = Script.errorCallback,\n    }) catch |err| {\n        self.async_scripts.remove(&script.node);\n        return err;\n    };\n}\n\n// Called from the Page to let us know it's done parsing the HTML. Necessary that\n// we know this so that we know that we can start evaluating deferred scripts.\npub fn staticScriptsDone(self: *ScriptManager) void {\n    lp.assert(self.static_scripts_done == false, \"ScriptManager.staticScriptsDone\", .{});\n    self.static_scripts_done = true;\n    self.evaluate();\n}\n\nfn evaluate(self: *ScriptManager) void {\n    if (self.is_evaluating) {\n        // It's possible for a script.eval to cause evaluate to be called again.\n        return;\n    }\n\n    const page = self.page;\n    self.is_evaluating = true;\n    defer self.is_evaluating = false;\n\n    while (self.ready_scripts.popFirst()) |n| {\n        var script: *Script = @fieldParentPtr(\"node\", n);\n        switch (script.mode) {\n            .async => {\n                defer script.deinit();\n                script.eval(page);\n            },\n            .import_async => |ia| {\n                if (script.status < 200 or script.status > 299) {\n                    script.deinit();\n                    ia.callback(ia.data, error.FailedToLoad);\n                } else {\n                    ia.callback(ia.data, .{\n                        .shared = false,\n                        .script = script,\n                        .buffer = script.source.remote,\n                    });\n                }\n            },\n            else => unreachable, // no other script is put in this list\n        }\n    }\n\n    if (self.static_scripts_done == false) {\n        // We can only execute deferred scripts if\n        // 1 - all the normal scripts are done\n        // 2 - we've finished parsing the HTML and at least queued all the scripts\n        // The last one isn't obvious, but it's possible for self.scripts to\n        // be empty not because we're done executing all the normal scripts\n        // but because we're done executing some (or maybe none), but we're still\n        // parsing the HTML.\n        return;\n    }\n\n    while (self.defer_scripts.first) |n| {\n        var script: *Script = @fieldParentPtr(\"node\", n);\n        if (script.complete == false) {\n            return;\n        }\n        defer {\n            _ = self.defer_scripts.popFirst();\n            script.deinit();\n        }\n        script.eval(page);\n    }\n\n    // At this point all normal scripts and deferred scripts are done, PLUS\n    // the page has signaled that it's done parsing HTML (static_scripts_done == true).\n    //\n\n    // When all scripts (normal and deferred) are done loading, the document\n    // state changes (this ultimately triggers the DOMContentLoaded event).\n    // Page makes this safe to call multiple times.\n    page.documentIsLoaded();\n\n    if (self.async_scripts.first == null and self.page_notified_of_completion == false) {\n        self.page_notified_of_completion = true;\n        page.scriptsCompletedLoading();\n    }\n}\n\nfn parseImportmap(self: *ScriptManager, script: *const Script) !void {\n    const content = script.source.content();\n\n    const Imports = struct {\n        imports: std.json.ArrayHashMap([]const u8),\n    };\n\n    const imports = try std.json.parseFromSliceLeaky(\n        Imports,\n        self.page.arena,\n        content,\n        .{ .allocate = .alloc_always },\n    );\n\n    var iter = imports.imports.map.iterator();\n    while (iter.next()) |entry| {\n        // > Relative URLs are resolved to absolute URL addresses using the\n        // > base URL of the document containing the import map.\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps\n        const resolved_url = try URL.resolve(\n            self.page.arena,\n            self.page.base(),\n            entry.value_ptr.*,\n            .{},\n        );\n\n        try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);\n    }\n}\n\npub const Script = struct {\n    kind: Kind,\n    complete: bool,\n    status: u16 = 0,\n    source: Source,\n    url: []const u8,\n    arena: Allocator,\n    mode: ExecutionMode,\n    node: std.DoublyLinkedList.Node,\n    script_element: ?*Element.Html.Script,\n    manager: *ScriptManager,\n\n    // for debugging a rare production issue\n    header_callback_called: bool = false,\n\n    // for debugging a rare production issue\n    debug_transfer_id: u32 = 0,\n    debug_transfer_tries: u8 = 0,\n    debug_transfer_aborted: bool = false,\n    debug_transfer_bytes_received: usize = 0,\n    debug_transfer_notified_fail: bool = false,\n    debug_transfer_redirecting: bool = false,\n    debug_transfer_intercept_state: u8 = 0,\n    debug_transfer_auth_challenge: bool = false,\n    debug_transfer_easy_id: usize = 0,\n\n    const Kind = enum {\n        module,\n        javascript,\n        importmap,\n    };\n\n    const Callback = union(enum) {\n        string: []const u8,\n        function: js.Function,\n    };\n\n    const Source = union(enum) {\n        @\"inline\": []const u8,\n        remote: std.ArrayList(u8),\n\n        fn content(self: Source) []const u8 {\n            return switch (self) {\n                .remote => |buf| buf.items,\n                .@\"inline\" => |c| c,\n            };\n        }\n    };\n\n    const ExecutionMode = union(enum) {\n        normal,\n        @\"defer\",\n        async,\n        import,\n        import_async: ImportAsync,\n    };\n\n    fn deinit(self: *Script) void {\n        self.manager.page.releaseArena(self.arena);\n    }\n\n    fn startCallback(transfer: *HttpClient.Transfer) !void {\n        log.debug(.http, \"script fetch start\", .{ .req = transfer });\n    }\n\n    fn headerCallback(transfer: *HttpClient.Transfer) !bool {\n        const self: *Script = @ptrCast(@alignCast(transfer.ctx));\n        const header = &transfer.response_header.?;\n        self.status = header.status;\n        if (header.status != 200) {\n            log.info(.http, \"script header\", .{\n                .req = transfer,\n                .status = header.status,\n                .content_type = header.contentType(),\n            });\n            return false;\n        }\n\n        if (comptime IS_DEBUG) {\n            log.debug(.http, \"script header\", .{\n                .req = transfer,\n                .status = header.status,\n                .content_type = header.contentType(),\n            });\n        }\n\n        {\n            // temp debug, trying to figure out why the next assert sometimes\n            // fails. Is the buffer just corrupt or is headerCallback really\n            // being called twice?\n            lp.assert(self.header_callback_called == false, \"ScriptManager.Header recall\", .{\n                .m = @tagName(std.meta.activeTag(self.mode)),\n                .a1 = self.debug_transfer_id,\n                .a2 = self.debug_transfer_tries,\n                .a3 = self.debug_transfer_aborted,\n                .a4 = self.debug_transfer_bytes_received,\n                .a5 = self.debug_transfer_notified_fail,\n                .a6 = self.debug_transfer_redirecting,\n                .a7 = self.debug_transfer_intercept_state,\n                .a8 = self.debug_transfer_auth_challenge,\n                .a9 = self.debug_transfer_easy_id,\n                .b1 = transfer.id,\n                .b2 = transfer._tries,\n                .b3 = transfer.aborted,\n                .b4 = transfer.bytes_received,\n                .b5 = transfer._notified_fail,\n                .b6 = transfer._redirecting,\n                .b7 = @intFromEnum(transfer._intercept_state),\n                .b8 = transfer._auth_challenge != null,\n                .b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0,\n            });\n            self.header_callback_called = true;\n            self.debug_transfer_id = transfer.id;\n            self.debug_transfer_tries = transfer._tries;\n            self.debug_transfer_aborted = transfer.aborted;\n            self.debug_transfer_bytes_received = transfer.bytes_received;\n            self.debug_transfer_notified_fail = transfer._notified_fail;\n            self.debug_transfer_redirecting = transfer._redirecting;\n            self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);\n            self.debug_transfer_auth_challenge = transfer._auth_challenge != null;\n            self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0;\n        }\n\n        lp.assert(self.source.remote.capacity == 0, \"ScriptManager.Header buffer\", .{ .capacity = self.source.remote.capacity });\n        var buffer: std.ArrayList(u8) = .empty;\n        if (transfer.getContentLength()) |cl| {\n            try buffer.ensureTotalCapacity(self.arena, cl);\n        }\n        self.source = .{ .remote = buffer };\n        return true;\n    }\n\n    fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {\n        const self: *Script = @ptrCast(@alignCast(transfer.ctx));\n        self._dataCallback(transfer, data) catch |err| {\n            log.err(.http, \"SM.dataCallback\", .{ .err = err, .transfer = transfer, .len = data.len });\n            return err;\n        };\n    }\n    fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {\n        try self.source.remote.appendSlice(self.arena, data);\n    }\n\n    fn doneCallback(ctx: *anyopaque) !void {\n        const self: *Script = @ptrCast(@alignCast(ctx));\n        self.complete = true;\n        if (comptime IS_DEBUG) {\n            log.debug(.http, \"script fetch complete\", .{ .req = self.url });\n        }\n\n        const manager = self.manager;\n        if (self.mode == .async or self.mode == .import_async) {\n            manager.async_scripts.remove(&self.node);\n            manager.ready_scripts.append(&self.node);\n        } else if (self.mode == .import) {\n            manager.async_scripts.remove(&self.node);\n            const entry = manager.imported_modules.getPtr(self.url).?;\n            entry.state = .{ .done = self };\n            entry.buffer = self.source.remote;\n        }\n        manager.evaluate();\n    }\n\n    fn errorCallback(ctx: *anyopaque, err: anyerror) void {\n        const self: *Script = @ptrCast(@alignCast(ctx));\n        log.warn(.http, \"script fetch error\", .{\n            .err = err,\n            .req = self.url,\n            .mode = std.meta.activeTag(self.mode),\n            .kind = self.kind,\n            .status = self.status,\n        });\n\n        if (self.mode == .normal) {\n            // This is blocked in a loop at the end of addFromElement, setting\n            // it to complete with a status of 0 will signal the error.\n            self.status = 0;\n            self.complete = true;\n            return;\n        }\n\n        const manager = self.manager;\n        manager.scriptList(self).remove(&self.node);\n        if (manager.shutdown) {\n            self.deinit();\n            return;\n        }\n\n        switch (self.mode) {\n            .import_async => |ia| ia.callback(ia.data, error.FailedToLoad),\n            .import => {\n                const entry = manager.imported_modules.getPtr(self.url).?;\n                entry.state = .err;\n            },\n            else => {},\n        }\n        self.deinit();\n        manager.evaluate();\n    }\n\n    fn eval(self: *Script, page: *Page) void {\n        // never evaluated, source is passed back to v8, via callbacks.\n        if (comptime IS_DEBUG) {\n            std.debug.assert(self.mode != .import_async);\n\n            // never evaluated, source is passed back to v8 when asked for it.\n            std.debug.assert(self.mode != .import);\n        }\n\n        if (page.isGoingAway()) {\n            // don't evaluate scripts for a dying page.\n            return;\n        }\n\n        const script_element = self.script_element.?;\n\n        const previous_script = page.document._current_script;\n        page.document._current_script = script_element;\n        defer page.document._current_script = previous_script;\n\n        // Clear the document.write insertion point for this script\n        const previous_write_insertion_point = page.document._write_insertion_point;\n        page.document._write_insertion_point = null;\n        defer page.document._write_insertion_point = previous_write_insertion_point;\n\n        // inline scripts aren't cached. remote ones are.\n        const cacheable = self.source == .remote;\n\n        const url = self.url;\n\n        log.info(.browser, \"executing script\", .{\n            .src = url,\n            .kind = self.kind,\n            .cacheable = cacheable,\n        });\n\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        const local = &ls.local;\n\n        // Handle importmap special case here: the content is a JSON containing\n        // imports.\n        if (self.kind == .importmap) {\n            page._script_manager.parseImportmap(self) catch |err| {\n                log.err(.browser, \"parse importmap script\", .{\n                    .err = err,\n                    .src = url,\n                    .kind = self.kind,\n                    .cacheable = cacheable,\n                });\n                self.executeCallback(comptime .wrap(\"error\"), page);\n                return;\n            };\n            self.executeCallback(comptime .wrap(\"load\"), page);\n            return;\n        }\n\n        defer page._event_manager.clearIgnoreList();\n\n        var try_catch: js.TryCatch = undefined;\n        try_catch.init(local);\n        defer try_catch.deinit();\n\n        const success = blk: {\n            const content = self.source.content();\n            switch (self.kind) {\n                .javascript => _ = local.eval(content, url) catch break :blk false,\n                .module => {\n                    // We don't care about waiting for the evaluation here.\n                    page.js.module(false, local, content, url, cacheable) catch break :blk false;\n                },\n                .importmap => unreachable, // handled before the try/catch.\n            }\n            break :blk true;\n        };\n\n        if (comptime IS_DEBUG) {\n            log.debug(.browser, \"executed script\", .{ .src = url, .success = success });\n        }\n\n        defer {\n            local.runMacrotasks(); // also runs microtasks\n            _ = page.js.scheduler.run() catch |err| {\n                log.err(.page, \"scheduler\", .{ .err = err });\n            };\n        }\n\n        if (success) {\n            self.executeCallback(comptime .wrap(\"load\"), page);\n            return;\n        }\n\n        const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);\n        log.warn(.js, \"eval script\", .{\n            .url = url,\n            .caught = caught,\n            .cacheable = cacheable,\n        });\n\n        self.executeCallback(comptime .wrap(\"error\"), page);\n    }\n\n    fn executeCallback(self: *const Script, typ: String, page: *Page) void {\n        const Event = @import(\"webapi/Event.zig\");\n        const event = Event.initTrusted(typ, .{}, page) catch |err| {\n            log.warn(.js, \"script internal callback\", .{\n                .url = self.url,\n                .type = typ,\n                .err = err,\n            });\n            return;\n        };\n        page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {\n            log.warn(.js, \"script callback\", .{\n                .url = self.url,\n                .type = typ,\n                .err = err,\n            });\n        };\n    }\n};\n\nconst ImportAsync = struct {\n    data: *anyopaque,\n    callback: ImportAsync.Callback,\n\n    pub const Callback = *const fn (ptr: *anyopaque, result: anyerror!ModuleSource) void;\n};\n\npub const ModuleSource = struct {\n    shared: bool,\n    script: *Script,\n    buffer: std.ArrayList(u8),\n\n    pub fn deinit(self: *ModuleSource) void {\n        if (self.shared == false) {\n            self.script.deinit();\n        }\n    }\n\n    pub fn src(self: *const ModuleSource) []const u8 {\n        return self.buffer.items;\n    }\n};\n\nconst ImportedModule = struct {\n    waiters: u16 = 1,\n    state: State = .loading,\n    buffer: std.ArrayList(u8) = .{},\n\n    const State = union(enum) {\n        err,\n        loading,\n        done: *Script,\n    };\n};\n\n// Parses data:[<media-type>][;base64],<data>\nfn parseDataURI(allocator: Allocator, src: []const u8) !?[]const u8 {\n    if (!std.mem.startsWith(u8, src, \"data:\")) {\n        return null;\n    }\n\n    const uri = src[5..];\n    const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;\n    const data = uri[data_starts + 1 ..];\n\n    const unescaped = try URL.unescape(allocator, data);\n\n    const metadata = uri[0..data_starts];\n    if (std.mem.endsWith(u8, metadata, \";base64\") == false) {\n        return unescaped;\n    }\n\n    // Forgiving base64 decode per WHATWG spec:\n    // https://infra.spec.whatwg.org/#forgiving-base64-decode\n    // Step 1: Remove all ASCII whitespace\n    var stripped = try std.ArrayList(u8).initCapacity(allocator, unescaped.len);\n    for (unescaped) |c| {\n        if (!std.ascii.isWhitespace(c)) {\n            stripped.appendAssumeCapacity(c);\n        }\n    }\n    const trimmed = std.mem.trimRight(u8, stripped.items, \"=\");\n\n    // Length % 4 == 1 is invalid\n    if (trimmed.len % 4 == 1) {\n        return error.InvalidCharacterError;\n    }\n\n    const decoded_size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;\n    const buffer = try allocator.alloc(u8, decoded_size);\n    std.base64.standard_no_pad.Decoder.decode(buffer, trimmed) catch return error.InvalidCharacterError;\n    return buffer;\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"DataURI: parse valid\" {\n    try assertValidDataURI(\"data:text/javascript; charset=utf-8;base64,Zm9v\", \"foo\");\n    try assertValidDataURI(\"data:text/javascript; charset=utf-8;,foo\", \"foo\");\n    try assertValidDataURI(\"data:,foo\", \"foo\");\n}\n\ntest \"DataURI: parse invalid\" {\n    try assertInvalidDataURI(\"atad:,foo\");\n    try assertInvalidDataURI(\"data:foo\");\n    try assertInvalidDataURI(\"data:\");\n}\n\nfn assertValidDataURI(uri: []const u8, expected: []const u8) !void {\n    defer testing.reset();\n    const data_uri = try parseDataURI(testing.arena_allocator, uri) orelse return error.TestFailed;\n    try testing.expectEqual(expected, data_uri);\n}\n\nfn assertInvalidDataURI(uri: []const u8) !void {\n    try testing.expectEqual(null, parseDataURI(undefined, uri));\n}\n"
  },
  {
    "path": "src/browser/Session.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst builtin = @import(\"builtin\");\n\nconst log = @import(\"../log.zig\");\nconst App = @import(\"../App.zig\");\n\nconst js = @import(\"js/js.zig\");\nconst storage = @import(\"webapi/storage/storage.zig\");\nconst Navigation = @import(\"webapi/navigation/Navigation.zig\");\nconst History = @import(\"webapi/History.zig\");\n\nconst Page = @import(\"Page.zig\");\nconst Browser = @import(\"Browser.zig\");\nconst Factory = @import(\"Factory.zig\");\nconst Notification = @import(\"../Notification.zig\");\nconst QueuedNavigation = Page.QueuedNavigation;\n\nconst Allocator = std.mem.Allocator;\nconst ArenaPool = App.ArenaPool;\nconst IS_DEBUG = builtin.mode == .Debug;\n\n// You can create successively multiple pages for a session, but you must\n// deinit a page before running another one. It manages two distinct lifetimes.\n//\n// The first is the lifetime of the Session itself, where pages are created and\n// removed, but share the same cookie jar and navigation history (etc...)\n//\n// The second is as a container the data needed by the full page hierarchy, i.e. \\\n// the root page and all of its frames (and all of their frames.)\nconst Session = @This();\n\n// These are the fields that remain intact for the duration of the Session\nbrowser: *Browser,\narena: Allocator,\nhistory: History,\nnavigation: Navigation,\nstorage_shed: storage.Shed,\nnotification: *Notification,\ncookie_jar: storage.Cookie.Jar,\n\n// These are the fields that get reset whenever the Session's page (the root) is reset.\nfactory: Factory,\n\npage_arena: Allocator,\n\n// Origin map for same-origin context sharing. Scoped to the root page lifetime.\norigins: std.StringHashMapUnmanaged(*js.Origin) = .empty,\n\n// Shared resources for all pages in this session.\n// These live for the duration of the page tree (root + frames).\narena_pool: *ArenaPool,\n\n// In Debug, we use this to see if anything fails to release an arena back to\n// the pool.\n_arena_pool_leak_track: if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {\n    owner: []const u8,\n    count: usize,\n}) else void = if (IS_DEBUG) .empty else {},\n\npage: ?Page,\n\nqueued_navigation: std.ArrayList(*Page),\n// Temporary buffer for about:blank navigations during processing.\n// We process async navigations first (safe from re-entrance), then sync\n// about:blank navigations (which may add to queued_navigation).\nqueued_queued_navigation: std.ArrayList(*Page),\n\npage_id_gen: u32,\nframe_id_gen: u32,\n\npub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {\n    const allocator = browser.app.allocator;\n    const arena_pool = browser.arena_pool;\n\n    const arena = try arena_pool.acquire();\n    errdefer arena_pool.release(arena);\n\n    const page_arena = try arena_pool.acquire();\n    errdefer arena_pool.release(page_arena);\n\n    self.* = .{\n        .page = null,\n        .arena = arena,\n        .arena_pool = arena_pool,\n        .page_arena = page_arena,\n        .factory = Factory.init(page_arena),\n        .history = .{},\n        .page_id_gen = 0,\n        .frame_id_gen = 0,\n        // The prototype (EventTarget) for Navigation is created when a Page is created.\n        .navigation = .{ ._proto = undefined },\n        .storage_shed = .{},\n        .browser = browser,\n        .queued_navigation = .{},\n        .queued_queued_navigation = .{},\n        .notification = notification,\n        .cookie_jar = storage.Cookie.Jar.init(allocator),\n    };\n}\n\npub fn deinit(self: *Session) void {\n    if (self.page != null) {\n        self.removePage();\n    }\n    self.cookie_jar.deinit();\n\n    self.storage_shed.deinit(self.browser.app.allocator);\n    self.arena_pool.release(self.page_arena);\n    self.arena_pool.release(self.arena);\n}\n\n// NOTE: the caller is not the owner of the returned value,\n// the pointer on Page is just returned as a convenience\npub fn createPage(self: *Session) !*Page {\n    lp.assert(self.page == null, \"Session.createPage - page not null\", .{});\n\n    self.page = @as(Page, undefined);\n    const page = &self.page.?;\n    try Page.init(page, self.nextFrameId(), self, null);\n\n    // Creates a new NavigationEventTarget for this page.\n    try self.navigation.onNewPage(page);\n\n    if (comptime IS_DEBUG) {\n        log.debug(.browser, \"create page\", .{});\n    }\n    // start JS env\n    // Inform CDP the main page has been created such that additional context for other Worlds can be created as well\n    self.notification.dispatch(.page_created, page);\n\n    return page;\n}\n\npub fn removePage(self: *Session) void {\n    // Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one\n    self.notification.dispatch(.page_remove, .{});\n    lp.assert(self.page != null, \"Session.removePage - page is null\", .{});\n\n    self.page.?.deinit(false);\n    self.page = null;\n\n    self.navigation.onRemovePage();\n    self.resetPageResources();\n\n    if (comptime IS_DEBUG) {\n        log.debug(.browser, \"remove page\", .{});\n    }\n}\n\npub const GetArenaOpts = struct {\n    debug: []const u8,\n};\n\npub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {\n    const allocator = try self.arena_pool.acquire();\n    if (comptime IS_DEBUG) {\n        // Use session's arena (not page_arena) since page_arena gets reset between pages\n        const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr));\n        if (gop.found_existing and gop.value_ptr.count != 0) {\n            log.err(.bug, \"ArenaPool Double Use\", .{ .owner = gop.value_ptr.*.owner });\n            @panic(\"ArenaPool Double Use\");\n        }\n        gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };\n    }\n    return allocator;\n}\n\npub fn releaseArena(self: *Session, allocator: Allocator) void {\n    if (comptime IS_DEBUG) {\n        const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;\n        if (found.count != 1) {\n            log.err(.bug, \"ArenaPool Double Free\", .{ .owner = found.owner, .count = found.count });\n            if (comptime builtin.is_test) {\n                @panic(\"ArenaPool Double Free\");\n            }\n            return;\n        }\n        found.count = 0;\n    }\n    return self.arena_pool.release(allocator);\n}\n\npub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {\n    const key = key_ orelse {\n        var opaque_origin: [36]u8 = undefined;\n        @import(\"../id.zig\").uuidv4(&opaque_origin);\n        // Origin.init will dupe opaque_origin. It's fine that this doesn't\n        // get added to self.origins. In fact, it further isolates it. When the\n        // context is freed, it'll call session.releaseOrigin which will free it.\n        return js.Origin.init(self.browser.app, self.browser.env.isolate, &opaque_origin);\n    };\n\n    const gop = try self.origins.getOrPut(self.arena, key);\n    if (gop.found_existing) {\n        const origin = gop.value_ptr.*;\n        origin.rc += 1;\n        return origin;\n    }\n\n    errdefer _ = self.origins.remove(key);\n\n    const origin = try js.Origin.init(self.browser.app, self.browser.env.isolate, key);\n    gop.key_ptr.* = origin.key;\n    gop.value_ptr.* = origin;\n    return origin;\n}\n\npub fn releaseOrigin(self: *Session, origin: *js.Origin) void {\n    const rc = origin.rc;\n    if (rc == 1) {\n        _ = self.origins.remove(origin.key);\n        origin.deinit(self.browser.app);\n    } else {\n        origin.rc = rc - 1;\n    }\n}\n\n/// Reset page_arena and factory for a clean slate.\n/// Called when root page is removed.\nfn resetPageResources(self: *Session) void {\n    // Check for arena leaks before releasing\n    if (comptime IS_DEBUG) {\n        var it = self._arena_pool_leak_track.valueIterator();\n        while (it.next()) |value_ptr| {\n            if (value_ptr.count > 0) {\n                log.err(.bug, \"ArenaPool Leak\", .{ .owner = value_ptr.owner });\n            }\n        }\n        self._arena_pool_leak_track.clearRetainingCapacity();\n    }\n\n    // All origins should have been released when contexts were destroyed\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.origins.count() == 0);\n    }\n    // Defensive cleanup in case origins leaked\n    {\n        const app = self.browser.app;\n        var it = self.origins.valueIterator();\n        while (it.next()) |value| {\n            value.*.deinit(app);\n        }\n        self.origins.clearRetainingCapacity();\n    }\n\n    // Release old page_arena and acquire fresh one\n    self.frame_id_gen = 0;\n    self.arena_pool.reset(self.page_arena, 64 * 1024);\n    self.factory = Factory.init(self.page_arena);\n}\n\npub fn replacePage(self: *Session) !*Page {\n    if (comptime IS_DEBUG) {\n        log.debug(.browser, \"replace page\", .{});\n    }\n\n    lp.assert(self.page != null, \"Session.replacePage null page\", .{});\n    lp.assert(self.page.?.parent == null, \"Session.replacePage with parent\", .{});\n\n    var current = self.page.?;\n    const frame_id = current._frame_id;\n    current.deinit(true);\n\n    self.resetPageResources();\n    self.browser.env.memoryPressureNotification(.moderate);\n\n    self.page = @as(Page, undefined);\n    const page = &self.page.?;\n    try Page.init(page, frame_id, self, null);\n    return page;\n}\n\npub fn currentPage(self: *Session) ?*Page {\n    return &(self.page orelse return null);\n}\n\npub const WaitResult = enum {\n    done,\n    no_page,\n    cdp_socket,\n};\n\npub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page {\n    const page = self.currentPage() orelse return null;\n    return findPageBy(page, \"_frame_id\", frame_id);\n}\n\npub fn findPageById(self: *Session, id: u32) ?*Page {\n    const page = self.currentPage() orelse return null;\n    return findPageBy(page, \"id\", id);\n}\n\nfn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page {\n    if (@field(page, field) == id) return page;\n    for (page.frames.items) |f| {\n        if (findPageBy(f, field, id)) |found| {\n            return found;\n        }\n    }\n    return null;\n}\n\npub fn wait(self: *Session, wait_ms: u32) WaitResult {\n    var page = &(self.page orelse return .no_page);\n    while (true) {\n        const wait_result = self._wait(page, wait_ms) catch |err| {\n            switch (err) {\n                error.JsError => {}, // already logged (with hopefully more context)\n                else => log.err(.browser, \"session wait\", .{\n                    .err = err,\n                    .url = page.url,\n                }),\n            }\n            return .done;\n        };\n\n        switch (wait_result) {\n            .done => {\n                if (self.queued_navigation.items.len == 0) {\n                    return .done;\n                }\n                self.processQueuedNavigation() catch return .done;\n                page = &self.page.?; // might have changed\n            },\n            else => |result| return result,\n        }\n    }\n}\n\nfn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {\n    var timer = try std.time.Timer.start();\n    var ms_remaining = wait_ms;\n\n    const browser = self.browser;\n    var http_client = browser.http_client;\n\n    // I'd like the page to know NOTHING about cdp_socket / CDP, but the\n    // fact is that the behavior of wait changes depending on whether or\n    // not we're using CDP.\n    // If we aren't using CDP, as soon as we think there's nothing left\n    // to do, we can exit - we'de done.\n    // But if we are using CDP, we should wait for the whole `wait_ms`\n    // because the http_click.tick() also monitors the CDP socket. And while\n    // we could let CDP poll http (like it does for HTTP requests), the fact\n    // is that we know more about the timing of stuff (e.g. how long to\n    // poll/sleep) in the page.\n    const exit_when_done = http_client.cdp_client == null;\n\n    while (true) {\n        switch (page._parse_state) {\n            .pre, .raw, .text, .image => {\n                // The main page hasn't started/finished navigating.\n                // There's no JS to run, and no reason to run the scheduler.\n                if (http_client.active == 0 and exit_when_done) {\n                    // haven't started navigating, I guess.\n                    return .done;\n                }\n                // Either we have active http connections, or we're in CDP\n                // mode with an extra socket. Either way, we're waiting\n                // for http traffic\n                if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {\n                    // exit_when_done is explicitly set when there isn't\n                    // an extra socket, so it should not be possibl to\n                    // get an cdp_socket message when exit_when_done\n                    // is true.\n                    if (IS_DEBUG) {\n                        std.debug.assert(exit_when_done == false);\n                    }\n\n                    // data on a socket we aren't handling, return to caller\n                    return .cdp_socket;\n                }\n            },\n            .html, .complete => {\n                if (self.queued_navigation.items.len != 0) {\n                    return .done;\n                }\n\n                // The HTML page was parsed. We now either have JS scripts to\n                // download, or scheduled tasks to execute, or both.\n\n                // scheduler.run could trigger new http transfers, so do not\n                // store http_client.active BEFORE this call and then use\n                // it AFTER.\n                try browser.runMacrotasks();\n\n                // Each call to this runs scheduled load events.\n                try page.dispatchLoad();\n\n                const http_active = http_client.active;\n                const total_network_activity = http_active + http_client.intercepted;\n                if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {\n                    page.notifyNetworkAlmostIdle();\n                }\n                if (page._notified_network_idle.check(total_network_activity == 0)) {\n                    page.notifyNetworkIdle();\n                }\n\n                if (http_active == 0 and exit_when_done) {\n                    // we don't need to consider http_client.intercepted here\n                    // because exit_when_done is true, and that can only be\n                    // the case when interception isn't possible.\n                    if (comptime IS_DEBUG) {\n                        std.debug.assert(http_client.intercepted == 0);\n                    }\n\n                    var ms = blk: {\n                        // if (wait_ms - ms_remaining < 100) {\n                        //     if (comptime builtin.is_test) {\n                        //         return .done;\n                        //     }\n                        //     // Look, we want to exit ASAP, but we don't want\n                        //     // to exit so fast that we've run none of the\n                        //     // background jobs.\n                        //     break :blk 50;\n                        // }\n\n                        if (browser.hasBackgroundTasks()) {\n                            // _we_ have nothing to run, but v8 is working on\n                            // background tasks. We'll wait for them.\n                            browser.waitForBackgroundTasks();\n                            break :blk 20;\n                        }\n\n                        break :blk browser.msToNextMacrotask() orelse return .done;\n                    };\n\n                    if (ms > ms_remaining) {\n                        // Same as above, except we have a scheduled task,\n                        // it just happens to be too far into the future\n                        // compared to how long we were told to wait.\n                        if (!browser.hasBackgroundTasks()) {\n                            return .done;\n                        }\n                        // _we_ have nothing to run, but v8 is working on\n                        // background tasks. We'll wait for them.\n                        browser.waitForBackgroundTasks();\n                        ms = 20;\n                    }\n\n                    // We have a task to run in the not-so-distant future.\n                    // You might think we can just sleep until that task is\n                    // ready, but we should continue to run lowPriority tasks\n                    // in the meantime, and that could unblock things. So\n                    // we'll just sleep for a bit, and then restart our wait\n                    // loop to see if anything new can be processed.\n                    std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));\n                } else {\n                    // We're here because we either have active HTTP\n                    // connections, or exit_when_done == false (aka, there's\n                    // an cdp_socket registered with the http client).\n                    // We should continue to run tasks, so we minimize how long\n                    // we'll poll for network I/O.\n                    var ms_to_wait = @min(200, browser.msToNextMacrotask() orelse 200);\n                    if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {\n                        // if we have background tasks, we don't want to wait too\n                        // long for a message from the client. We want to go back\n                        // to the top of the loop and run macrotasks.\n                        ms_to_wait = 10;\n                    }\n                    if (try http_client.tick(@min(ms_remaining, ms_to_wait)) == .cdp_socket) {\n                        // data on a socket we aren't handling, return to caller\n                        return .cdp_socket;\n                    }\n                }\n            },\n            .err => |err| {\n                page._parse_state = .{ .raw_done = @errorName(err) };\n                return err;\n            },\n            .raw_done => {\n                if (exit_when_done) {\n                    return .done;\n                }\n                // we _could_ http_client.tick(ms_to_wait), but this has\n                // the same result, and I feel is more correct.\n                return .no_page;\n            },\n        }\n\n        const ms_elapsed = timer.lap() / 1_000_000;\n        if (ms_elapsed >= ms_remaining) {\n            return .done;\n        }\n        ms_remaining -= @intCast(ms_elapsed);\n    }\n}\n\npub fn scheduleNavigation(self: *Session, page: *Page) !void {\n    const list = &self.queued_navigation;\n\n    // Check if page is already queued\n    for (list.items) |existing| {\n        if (existing == page) {\n            // Already queued\n            return;\n        }\n    }\n\n    return list.append(self.arena, page);\n}\n\nfn processQueuedNavigation(self: *Session) !void {\n    const navigations = &self.queued_navigation;\n\n    if (self.page.?._queued_navigation != null) {\n        // This is both an optimization and a simplification of sorts. If the\n        // root page is navigating, then we don't need to process any other\n        // navigation. Also, the navigation for the root page and for a frame\n        // is different enough that have two distinct code blocks is, imo,\n        // better. Yes, there will be duplication.\n        navigations.clearRetainingCapacity();\n        return self.processRootQueuedNavigation();\n    }\n\n    const about_blank_queue = &self.queued_queued_navigation;\n    defer about_blank_queue.clearRetainingCapacity();\n\n    // First pass: process async navigations (non-about:blank)\n    // These cannot cause re-entrant navigation scheduling\n    for (navigations.items) |page| {\n        const qn = page._queued_navigation.?;\n\n        if (qn.is_about_blank) {\n            // Defer about:blank to second pass\n            try about_blank_queue.append(self.arena, page);\n            continue;\n        }\n\n        self.processFrameNavigation(page, qn) catch |err| {\n            log.warn(.page, \"frame navigation\", .{ .url = qn.url, .err = err });\n        };\n    }\n\n    // Clear the queue after first pass\n    navigations.clearRetainingCapacity();\n\n    // Second pass: process synchronous navigations (about:blank)\n    // These may trigger new navigations which go into queued_navigation\n    for (about_blank_queue.items) |page| {\n        const qn = page._queued_navigation.?;\n        try self.processFrameNavigation(page, qn);\n    }\n\n    // Safety: Remove any about:blank navigations that were queued during the\n    // second pass to prevent infinite loops\n    var i: usize = 0;\n    while (i < navigations.items.len) {\n        const page = navigations.items[i];\n        if (page._queued_navigation) |qn| {\n            if (qn.is_about_blank) {\n                log.warn(.page, \"recursive about    blank\", .{});\n                _ = navigations.swapRemove(i);\n                continue;\n            }\n        }\n        i += 1;\n    }\n}\n\nfn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void {\n    lp.assert(page.parent != null, \"root queued navigation\", .{});\n\n    const iframe = page.iframe.?;\n    const parent = page.parent.?;\n\n    page._queued_navigation = null;\n    defer self.releaseArena(qn.arena);\n\n    errdefer iframe._window = null;\n\n    const parent_notified = page._parent_notified;\n    if (parent_notified) {\n        // we already notified the parent that we had loaded\n        parent._pending_loads += 1;\n    }\n\n    const frame_id = page._frame_id;\n    page.deinit(true);\n    page.* = undefined;\n\n    try Page.init(page, frame_id, self, parent);\n    errdefer {\n        for (parent.frames.items, 0..) |frame, i| {\n            if (frame == page) {\n                parent.frames_sorted = false;\n                _ = parent.frames.swapRemove(i);\n                break;\n            }\n        }\n        if (parent_notified) {\n            parent._pending_loads -= 1;\n        }\n        page.deinit(true);\n    }\n\n    page.iframe = iframe;\n    iframe._window = page.window;\n\n    page.navigate(qn.url, qn.opts) catch |err| {\n        log.err(.browser, \"queued frame navigation error\", .{ .err = err });\n        return err;\n    };\n}\n\nfn processRootQueuedNavigation(self: *Session) !void {\n    const current_page = &self.page.?;\n    const frame_id = current_page._frame_id;\n\n    // create a copy before the page is cleared\n    const qn = current_page._queued_navigation.?;\n    current_page._queued_navigation = null;\n\n    defer self.arena_pool.release(qn.arena);\n\n    // HACK\n    // Mark as released in tracking BEFORE removePage clears the map.\n    // We can't call releaseArena() because that would also return the arena\n    // to the pool, making the memory invalid before we use qn.url/qn.opts.\n    if (comptime IS_DEBUG) {\n        if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| {\n            found.count = 0;\n        }\n    }\n\n    self.removePage();\n\n    self.page = @as(Page, undefined);\n    const new_page = &self.page.?;\n    try Page.init(new_page, frame_id, self, null);\n\n    // Creates a new NavigationEventTarget for this page.\n    try self.navigation.onNewPage(new_page);\n\n    // start JS env\n    // Inform CDP the main page has been created such that additional context for other Worlds can be created as well\n    self.notification.dispatch(.page_created, new_page);\n\n    new_page.navigate(qn.url, qn.opts) catch |err| {\n        log.err(.browser, \"queued navigation error\", .{ .err = err });\n        return err;\n    };\n}\n\npub fn nextFrameId(self: *Session) u32 {\n    const id = self.frame_id_gen +% 1;\n    self.frame_id_gen = id;\n    return id;\n}\n\npub fn nextPageId(self: *Session) u32 {\n    const id = self.page_id_gen +% 1;\n    self.page_id_gen = id;\n    return id;\n}\n"
  },
  {
    "path": "src/browser/URL.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ResolveOpts = struct {\n    encode: bool = false,\n    always_dupe: bool = false,\n};\n\n// path is anytype, so that it can be used with both []const u8 and [:0]const u8\npub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {\n    const PT = @TypeOf(path);\n    if (base.len == 0 or isCompleteHTTPUrl(path)) {\n        if (comptime opts.always_dupe or !isNullTerminated(PT)) {\n            const duped = try allocator.dupeZ(u8, path);\n            return processResolved(allocator, duped, opts);\n        }\n        if (comptime opts.encode) {\n            return processResolved(allocator, path, opts);\n        }\n        return path;\n    }\n\n    if (path.len == 0) {\n        if (comptime opts.always_dupe) {\n            const duped = try allocator.dupeZ(u8, base);\n            return processResolved(allocator, duped, opts);\n        }\n        if (comptime opts.encode) {\n            return processResolved(allocator, base, opts);\n        }\n        return base;\n    }\n\n    if (path[0] == '?') {\n        const base_path_end = std.mem.indexOfAny(u8, base, \"?#\") orelse base.len;\n        const result = try std.mem.joinZ(allocator, \"\", &.{ base[0..base_path_end], path });\n        return processResolved(allocator, result, opts);\n    }\n    if (path[0] == '#') {\n        const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;\n        const result = try std.mem.joinZ(allocator, \"\", &.{ base[0..base_fragment_start], path });\n        return processResolved(allocator, result, opts);\n    }\n\n    if (std.mem.startsWith(u8, path, \"//\")) {\n        // network-path reference\n        const index = std.mem.indexOfScalar(u8, base, ':') orelse {\n            if (comptime isNullTerminated(PT)) {\n                if (comptime opts.encode) {\n                    return processResolved(allocator, path, opts);\n                }\n                return path;\n            }\n            const duped = try allocator.dupeZ(u8, path);\n            return processResolved(allocator, duped, opts);\n        };\n        const protocol = base[0 .. index + 1];\n        const result = try std.mem.joinZ(allocator, \"\", &.{ protocol, path });\n        return processResolved(allocator, result, opts);\n    }\n\n    const scheme_end = std.mem.indexOf(u8, base, \"://\");\n    const authority_start = if (scheme_end) |end| end + 3 else 0;\n    const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;\n\n    if (path[0] == '/') {\n        const result = try std.mem.joinZ(allocator, \"\", &.{ base[0..path_start], path });\n        return processResolved(allocator, result, opts);\n    }\n\n    var normalized_base: []const u8 = base[0..path_start];\n    if (path_start < base.len) {\n        if (std.mem.lastIndexOfScalar(u8, base[path_start + 1 ..], '/')) |pos| {\n            normalized_base = base[0 .. path_start + 1 + pos];\n        }\n    }\n\n    // trailing space so that we always have space to append the null terminator\n    // and so that we can compare the next two characters without needing to length check\n    var out = try std.mem.join(allocator, \"\", &.{ normalized_base, \"/\", path, \"  \" });\n    const end = out.len - 2;\n\n    const path_marker = path_start + 1;\n\n    // Strip out ./ and ../. This is done in-place, because doing so can\n    // only ever make `out` smaller. After this, `out` cannot be freed by\n    // an allocator, which is ok, because we expect allocator to be an arena.\n    var in_i: usize = 0;\n    var out_i: usize = 0;\n    while (in_i < end) {\n        if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {\n            if (out[in_i + 1] == '/') { // always safe, because we added a whitespace\n                // /./\n                in_i += 2;\n                continue;\n            }\n            if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces\n                // /../\n                if (out_i > path_marker) {\n                    // go back before the /\n                    out_i -= 2;\n                    while (out_i > 1 and out[out_i - 1] != '/') {\n                        out_i -= 1;\n                    }\n                } else {\n                    // if out_i == path_marker, than we've reached the start of\n                    // the path. We can't ../ any more. E.g.:\n                    //    http://www.example.com/../hello.\n                    // You might think that's an error, but, at least with\n                    //     new URL('../hello', 'http://www.example.com/')\n                    // it just ignores the extra ../\n                }\n                in_i += 3;\n                continue;\n            }\n            if (in_i == end - 1) {\n                // ignore trailing dot\n                break;\n            }\n        }\n\n        const c = out[in_i];\n        out[out_i] = c;\n        in_i += 1;\n        out_i += 1;\n    }\n\n    // we always have an extra space\n    out[out_i] = 0;\n    return processResolved(allocator, out[0..out_i :0], opts);\n}\n\nfn processResolved(allocator: Allocator, url: [:0]const u8, comptime opts: ResolveOpts) ![:0]const u8 {\n    if (!comptime opts.encode) {\n        return url;\n    }\n    return ensureEncoded(allocator, url);\n}\n\npub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {\n    const scheme_end = std.mem.indexOf(u8, url, \"://\");\n    const authority_start = if (scheme_end) |end| end + 3 else 0;\n    const path_start = std.mem.indexOfScalarPos(u8, url, authority_start, '/') orelse return url;\n\n    const query_start = std.mem.indexOfScalarPos(u8, url, path_start, '?');\n    const fragment_start = std.mem.indexOfScalarPos(u8, url, query_start orelse path_start, '#');\n\n    const path_end = query_start orelse fragment_start orelse url.len;\n    const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;\n\n    const path_to_encode = url[path_start..path_end];\n    const encoded_path = try percentEncodeSegment(allocator, path_to_encode, .path);\n\n    const encoded_query = if (query_start) |qs| blk: {\n        const query_to_encode = url[qs + 1 .. query_end];\n        const encoded = try percentEncodeSegment(allocator, query_to_encode, .query);\n        break :blk encoded;\n    } else null;\n\n    const encoded_fragment = if (fragment_start) |fs| blk: {\n        const fragment_to_encode = url[fs + 1 ..];\n        const encoded = try percentEncodeSegment(allocator, fragment_to_encode, .query);\n        break :blk encoded;\n    } else null;\n\n    if (encoded_path.ptr == path_to_encode.ptr and\n        (encoded_query == null or encoded_query.?.ptr == url[query_start.? + 1 .. query_end].ptr) and\n        (encoded_fragment == null or encoded_fragment.?.ptr == url[fragment_start.? + 1 ..].ptr))\n    {\n        // nothing has changed\n        return url;\n    }\n\n    var buf = try std.ArrayList(u8).initCapacity(allocator, url.len + 20);\n    try buf.appendSlice(allocator, url[0..path_start]);\n    try buf.appendSlice(allocator, encoded_path);\n    if (encoded_query) |eq| {\n        try buf.append(allocator, '?');\n        try buf.appendSlice(allocator, eq);\n    }\n    if (encoded_fragment) |ef| {\n        try buf.append(allocator, '#');\n        try buf.appendSlice(allocator, ef);\n    }\n    try buf.append(allocator, 0);\n    return buf.items[0 .. buf.items.len - 1 :0];\n}\n\nconst EncodeSet = enum { path, query, userinfo };\n\nfn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {\n    // Check if encoding is needed\n    var needs_encoding = false;\n    for (segment) |c| {\n        if (shouldPercentEncode(c, encode_set)) {\n            needs_encoding = true;\n            break;\n        }\n    }\n    if (!needs_encoding) {\n        return segment;\n    }\n\n    var buf = try std.ArrayList(u8).initCapacity(allocator, segment.len + 10);\n\n    var i: usize = 0;\n    while (i < segment.len) : (i += 1) {\n        const c = segment[i];\n\n        // Check if this is an already-encoded sequence (%XX)\n        if (c == '%' and i + 2 < segment.len) {\n            const end = i + 2;\n            const h1 = segment[i + 1];\n            const h2 = segment[end];\n            if (std.ascii.isHex(h1) and std.ascii.isHex(h2)) {\n                try buf.appendSlice(allocator, segment[i .. end + 1]);\n                i = end;\n                continue;\n            }\n        }\n\n        if (shouldPercentEncode(c, encode_set)) {\n            try buf.writer(allocator).print(\"%{X:0>2}\", .{c});\n        } else {\n            try buf.append(allocator, c);\n        }\n    }\n\n    return buf.items;\n}\n\nfn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {\n    return switch (c) {\n        // Unreserved characters (RFC 3986)\n        'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,\n        // sub-delims allowed in path/query but some must be encoded in userinfo\n        '!', '$', '&', '\\'', '(', ')', '*', '+', ',' => false,\n        ';', '=' => encode_set == .userinfo,\n        // Separators: userinfo must encode these\n        '/', ':', '@' => encode_set == .userinfo,\n        // '?' is allowed in queries but not in paths or userinfo\n        '?' => encode_set != .query,\n        // Everything else needs encoding (including space)\n        else => true,\n    };\n}\n\nfn isNullTerminated(comptime value: type) bool {\n    return @typeInfo(value).pointer.sentinel_ptr != null;\n}\n\npub fn isCompleteHTTPUrl(url: []const u8) bool {\n    if (url.len < 3) { // Minimum is \"x://\"\n        return false;\n    }\n\n    // very common case\n    if (url[0] == '/') {\n        return false;\n    }\n\n    // blob: and data: URLs are complete but don't follow scheme:// pattern\n    if (std.mem.startsWith(u8, url, \"blob:\") or std.mem.startsWith(u8, url, \"data:\")) {\n        return true;\n    }\n\n    // Check if there's a scheme (protocol) ending with ://\n    const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;\n\n    // Check if it's followed by //\n    if (colon_pos + 2 >= url.len or url[colon_pos + 1] != '/' or url[colon_pos + 2] != '/') {\n        return false;\n    }\n\n    // Validate that everything before the colon is a valid scheme\n    // A scheme must start with a letter and contain only letters, digits, +, -, .\n    if (colon_pos == 0) {\n        return false;\n    }\n\n    const scheme = url[0..colon_pos];\n    if (!std.ascii.isAlphabetic(scheme[0])) {\n        return false;\n    }\n\n    for (scheme[1..]) |c| {\n        if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '-' and c != '.') {\n            return false;\n        }\n    }\n\n    return true;\n}\n\npub fn getUsername(raw: [:0]const u8) []const u8 {\n    const user_info = getUserInfo(raw) orelse return \"\";\n    const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info;\n    return user_info[0..pos];\n}\n\npub fn getPassword(raw: [:0]const u8) []const u8 {\n    const user_info = getUserInfo(raw) orelse return \"\";\n    const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return \"\";\n    return user_info[pos + 1 ..];\n}\n\npub fn getPathname(raw: [:0]const u8) []const u8 {\n    const protocol_end = std.mem.indexOf(u8, raw, \"://\") orelse 0;\n    const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len;\n\n    const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, \"?#\") orelse raw.len;\n\n    if (path_start >= query_or_hash_start) {\n        if (std.mem.indexOf(u8, raw, \"://\") != null) return \"/\";\n        return \"\";\n    }\n\n    return raw[path_start..query_or_hash_start];\n}\n\npub fn getProtocol(raw: [:0]const u8) []const u8 {\n    const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return \"\";\n    return raw[0 .. pos + 1];\n}\n\npub fn isHTTPS(raw: [:0]const u8) bool {\n    return std.mem.startsWith(u8, raw, \"https:\");\n}\n\npub fn getHostname(raw: [:0]const u8) []const u8 {\n    const host = getHost(raw);\n    const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host;\n    return host[0..pos];\n}\n\npub fn getPort(raw: [:0]const u8) []const u8 {\n    const host = getHost(raw);\n    const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return \"\";\n\n    if (pos + 1 >= host.len) {\n        return \"\";\n    }\n\n    for (host[pos + 1 ..]) |c| {\n        if (c < '0' or c > '9') {\n            return \"\";\n        }\n    }\n\n    return host[pos + 1 ..];\n}\n\npub fn getSearch(raw: [:0]const u8) []const u8 {\n    const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return \"\";\n    const query_part = raw[pos..];\n\n    if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| {\n        return query_part[0..fragment_start];\n    }\n\n    return query_part;\n}\n\npub fn getHash(raw: [:0]const u8) []const u8 {\n    const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return \"\";\n    return raw[start..];\n}\n\npub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {\n    const scheme_end = std.mem.indexOf(u8, raw, \"://\") orelse return null;\n\n    // Only HTTP and HTTPS schemes have origins\n    const protocol = raw[0 .. scheme_end + 1];\n    if (!std.mem.eql(u8, protocol, \"http:\") and !std.mem.eql(u8, protocol, \"https:\")) {\n        return null;\n    }\n\n    var authority_start = scheme_end + 3;\n    const has_user_info = if (std.mem.indexOf(u8, raw[authority_start..], \"@\")) |pos| blk: {\n        authority_start += pos + 1;\n        break :blk true;\n    } else false;\n\n    // Find end of authority (start of path/query/fragment or end of string)\n    const authority_end_relative = std.mem.indexOfAny(u8, raw[authority_start..], \"/?#\");\n    const authority_end = if (authority_end_relative) |end|\n        authority_start + end\n    else\n        raw.len;\n\n    // Check for port in the host:port section\n    const host_part = raw[authority_start..authority_end];\n    if (std.mem.lastIndexOfScalar(u8, host_part, ':')) |colon_pos_in_host| {\n        const port = host_part[colon_pos_in_host + 1 ..];\n\n        // Validate it's actually a port (all digits)\n        for (port) |c| {\n            if (c < '0' or c > '9') {\n                // Not a port (probably IPv6)\n                if (has_user_info) {\n                    // Need to allocate to exclude user info\n                    return try std.fmt.allocPrint(allocator, \"{s}//{s}\", .{ raw[0 .. scheme_end + 1], host_part });\n                }\n                // Can return a slice\n                return raw[0..authority_end];\n            }\n        }\n\n        // Check if it's a default port that should be excluded from origin\n        const is_default =\n            (std.mem.eql(u8, protocol, \"http:\") and std.mem.eql(u8, port, \"80\")) or\n            (std.mem.eql(u8, protocol, \"https:\") and std.mem.eql(u8, port, \"443\"));\n\n        if (is_default or has_user_info) {\n            // Need to allocate to build origin without default port and/or user info\n            const hostname = host_part[0..colon_pos_in_host];\n            if (is_default) {\n                return try std.fmt.allocPrint(allocator, \"{s}//{s}\", .{ protocol, hostname });\n            } else {\n                return try std.fmt.allocPrint(allocator, \"{s}//{s}\", .{ protocol, host_part });\n            }\n        }\n    } else if (has_user_info) {\n        // No port, but has user info - need to allocate\n        return try std.fmt.allocPrint(allocator, \"{s}//{s}\", .{ raw[0 .. scheme_end + 1], host_part });\n    }\n\n    // Common case: no user info, no default port - return slice (zero allocation!)\n    return raw[0..authority_end];\n}\n\nfn getUserInfo(raw: [:0]const u8) ?[]const u8 {\n    const scheme_end = std.mem.indexOf(u8, raw, \"://\") orelse return null;\n    const authority_start = scheme_end + 3;\n\n    const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null;\n    const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len;\n\n    const full_pos = authority_start + pos;\n    if (full_pos < path_start) {\n        return raw[authority_start..full_pos];\n    }\n\n    return null;\n}\n\npub fn getHost(raw: [:0]const u8) []const u8 {\n    const scheme_end = std.mem.indexOf(u8, raw, \"://\") orelse return \"\";\n\n    var authority_start = scheme_end + 3;\n    if (std.mem.indexOf(u8, raw[authority_start..], \"@\")) |pos| {\n        authority_start += pos + 1;\n    }\n\n    const authority = raw[authority_start..];\n    const path_start = std.mem.indexOfAny(u8, authority, \"/?#\") orelse return authority;\n    return authority[0..path_start];\n}\n\n// Returns true if these two URLs point to the same document.\npub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool {\n    // First '#' signifies the start of the fragment.\n    const first_hash_index = std.mem.indexOfScalar(u8, first, '#') orelse first.len;\n    const second_hash_index = std.mem.indexOfScalar(u8, second, '#') orelse second.len;\n    return std.mem.eql(u8, first[0..first_hash_index], second[0..second_hash_index]);\n}\n\n// Helper function to build a URL from components\npub fn buildUrl(\n    allocator: Allocator,\n    protocol: []const u8,\n    host: []const u8,\n    pathname: []const u8,\n    search: []const u8,\n    hash: []const u8,\n) ![:0]const u8 {\n    return std.fmt.allocPrintSentinel(allocator, \"{s}//{s}{s}{s}{s}\", .{\n        protocol,\n        host,\n        pathname,\n        search,\n        hash,\n    }, 0);\n}\n\npub fn setProtocol(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const host = getHost(current);\n    const pathname = getPathname(current);\n    const search = getSearch(current);\n    const hash = getHash(current);\n\n    // Add : suffix if not present\n    const protocol = if (value.len > 0 and value[value.len - 1] != ':')\n        try std.fmt.allocPrint(allocator, \"{s}:\", .{value})\n    else\n        value;\n\n    return buildUrl(allocator, protocol, host, pathname, search, hash);\n}\n\npub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const protocol = getProtocol(current);\n    const pathname = getPathname(current);\n    const search = getSearch(current);\n    const hash = getHash(current);\n\n    // Check if the new value includes a port\n    const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':');\n    const clean_host = if (colon_pos) |pos| blk: {\n        const port_str = value[pos + 1 ..];\n        // Remove default ports\n        if (std.mem.eql(u8, protocol, \"https:\") and std.mem.eql(u8, port_str, \"443\")) {\n            break :blk value[0..pos];\n        }\n        if (std.mem.eql(u8, protocol, \"http:\") and std.mem.eql(u8, port_str, \"80\")) {\n            break :blk value[0..pos];\n        }\n        break :blk value;\n    } else blk: {\n        // No port in new value - preserve existing port\n        const current_port = getPort(current);\n        if (current_port.len > 0) {\n            break :blk try std.fmt.allocPrint(allocator, \"{s}:{s}\", .{ value, current_port });\n        }\n        break :blk value;\n    };\n\n    return buildUrl(allocator, protocol, clean_host, pathname, search, hash);\n}\n\npub fn setHostname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const current_port = getPort(current);\n    const new_host = if (current_port.len > 0)\n        try std.fmt.allocPrint(allocator, \"{s}:{s}\", .{ value, current_port })\n    else\n        value;\n\n    return setHost(current, new_host, allocator);\n}\n\npub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {\n    const hostname = getHostname(current);\n    const protocol = getProtocol(current);\n    const pathname = getPathname(current);\n    const search = getSearch(current);\n    const hash = getHash(current);\n\n    // Handle null or default ports\n    const new_host = if (value) |port_str| blk: {\n        if (port_str.len == 0) {\n            break :blk hostname;\n        }\n        // Check if this is a default port for the protocol\n        if (std.mem.eql(u8, protocol, \"https:\") and std.mem.eql(u8, port_str, \"443\")) {\n            break :blk hostname;\n        }\n        if (std.mem.eql(u8, protocol, \"http:\") and std.mem.eql(u8, port_str, \"80\")) {\n            break :blk hostname;\n        }\n        break :blk try std.fmt.allocPrint(allocator, \"{s}:{s}\", .{ hostname, port_str });\n    } else hostname;\n\n    return buildUrl(allocator, protocol, new_host, pathname, search, hash);\n}\n\npub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const protocol = getProtocol(current);\n    const host = getHost(current);\n    const search = getSearch(current);\n    const hash = getHash(current);\n\n    // Add / prefix if not present and value is not empty\n    const pathname = if (value.len > 0 and value[0] != '/')\n        try std.fmt.allocPrint(allocator, \"/{s}\", .{value})\n    else\n        value;\n\n    return buildUrl(allocator, protocol, host, pathname, search, hash);\n}\n\npub fn setSearch(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const protocol = getProtocol(current);\n    const host = getHost(current);\n    const pathname = getPathname(current);\n    const hash = getHash(current);\n\n    // Add ? prefix if not present and value is not empty\n    const search = if (value.len > 0 and value[0] != '?')\n        try std.fmt.allocPrint(allocator, \"?{s}\", .{value})\n    else\n        value;\n\n    return buildUrl(allocator, protocol, host, pathname, search, hash);\n}\n\npub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const protocol = getProtocol(current);\n    const host = getHost(current);\n    const pathname = getPathname(current);\n    const search = getSearch(current);\n\n    // Add # prefix if not present and value is not empty\n    const hash = if (value.len > 0 and value[0] != '#')\n        try std.fmt.allocPrint(allocator, \"#{s}\", .{value})\n    else\n        value;\n\n    return buildUrl(allocator, protocol, host, pathname, search, hash);\n}\n\npub fn setUsername(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const protocol = getProtocol(current);\n    const host = getHost(current);\n    const pathname = getPathname(current);\n    const search = getSearch(current);\n    const hash = getHash(current);\n    const password = getPassword(current);\n\n    const encoded_username = try percentEncodeSegment(allocator, value, .userinfo);\n    return buildUrlWithUserInfo(allocator, protocol, encoded_username, password, host, pathname, search, hash);\n}\n\npub fn setPassword(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {\n    const protocol = getProtocol(current);\n    const host = getHost(current);\n    const pathname = getPathname(current);\n    const search = getSearch(current);\n    const hash = getHash(current);\n    const username = getUsername(current);\n\n    const encoded_password = try percentEncodeSegment(allocator, value, .userinfo);\n    return buildUrlWithUserInfo(allocator, protocol, username, encoded_password, host, pathname, search, hash);\n}\n\nfn buildUrlWithUserInfo(\n    allocator: Allocator,\n    protocol: []const u8,\n    username: []const u8,\n    password: []const u8,\n    host: []const u8,\n    pathname: []const u8,\n    search: []const u8,\n    hash: []const u8,\n) ![:0]const u8 {\n    if (username.len == 0 and password.len == 0) {\n        return buildUrl(allocator, protocol, host, pathname, search, hash);\n    } else if (password.len == 0) {\n        return std.fmt.allocPrintSentinel(allocator, \"{s}//{s}@{s}{s}{s}{s}\", .{\n            protocol,\n            username,\n            host,\n            pathname,\n            search,\n            hash,\n        }, 0);\n    } else {\n        return std.fmt.allocPrintSentinel(allocator, \"{s}//{s}:{s}@{s}{s}{s}{s}\", .{\n            protocol,\n            username,\n            password,\n            host,\n            pathname,\n            search,\n            hash,\n        }, 0);\n    }\n}\n\npub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {\n    if (query_string.len == 0) {\n        return arena.dupeZ(u8, url);\n    }\n\n    var buf: std.ArrayList(u8) = .empty;\n\n    // the most space well need is the url + ('?' or '&') + the query_string + null terminator\n    try buf.ensureTotalCapacity(arena, url.len + 2 + query_string.len);\n    buf.appendSliceAssumeCapacity(url);\n\n    if (std.mem.indexOfScalar(u8, url, '?')) |index| {\n        const last_index = url.len - 1;\n        if (index != last_index and url[last_index] != '&') {\n            buf.appendAssumeCapacity('&');\n        }\n    } else {\n        buf.appendAssumeCapacity('?');\n    }\n    buf.appendSliceAssumeCapacity(query_string);\n    buf.appendAssumeCapacity(0);\n    return buf.items[0 .. buf.items.len - 1 :0];\n}\n\npub fn getRobotsUrl(arena: Allocator, url: [:0]const u8) ![:0]const u8 {\n    const origin = try getOrigin(arena, url) orelse return error.NoOrigin;\n    return try std.fmt.allocPrintSentinel(\n        arena,\n        \"{s}/robots.txt\",\n        .{origin},\n        0,\n    );\n}\n\npub fn unescape(arena: Allocator, input: []const u8) ![]const u8 {\n    if (std.mem.indexOfScalar(u8, input, '%') == null) {\n        return input;\n    }\n\n    var result = try std.ArrayList(u8).initCapacity(arena, input.len);\n\n    var i: usize = 0;\n    while (i < input.len) {\n        if (input[i] == '%' and i + 2 < input.len) {\n            const hex = input[i + 1 .. i + 3];\n            const byte = std.fmt.parseInt(u8, hex, 16) catch {\n                result.appendAssumeCapacity(input[i]);\n                i += 1;\n                continue;\n            };\n            result.appendAssumeCapacity(byte);\n            i += 3;\n        } else {\n            result.appendAssumeCapacity(input[i]);\n            i += 1;\n        }\n    }\n\n    return result.items;\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"URL: isCompleteHTTPUrl\" {\n    try testing.expectEqual(true, isCompleteHTTPUrl(\"http://example.com/about\"));\n    try testing.expectEqual(true, isCompleteHTTPUrl(\"HttP://example.com/about\"));\n    try testing.expectEqual(true, isCompleteHTTPUrl(\"httpS://example.com/about\"));\n    try testing.expectEqual(true, isCompleteHTTPUrl(\"HTTPs://example.com/about\"));\n    try testing.expectEqual(true, isCompleteHTTPUrl(\"ftp://example.com/about\"));\n\n    try testing.expectEqual(false, isCompleteHTTPUrl(\"/example.com\"));\n    try testing.expectEqual(false, isCompleteHTTPUrl(\"../../about\"));\n    try testing.expectEqual(false, isCompleteHTTPUrl(\"about\"));\n}\n\ntest \"URL: resolve regression (#1093)\" {\n    defer testing.reset();\n\n    const Case = struct {\n        base: [:0]const u8,\n        path: [:0]const u8,\n        expected: [:0]const u8,\n    };\n\n    const cases = [_]Case{\n        .{\n            .base = \"https://alas.aws.amazon.com/alas2.html\",\n            .path = \"../static/bootstrap.min.css\",\n            .expected = \"https://alas.aws.amazon.com/static/bootstrap.min.css\",\n        },\n    };\n\n    for (cases) |case| {\n        const result = try resolve(testing.arena_allocator, case.base, case.path, .{});\n        try testing.expectString(case.expected, result);\n    }\n}\n\ntest \"URL: resolve\" {\n    defer testing.reset();\n\n    const Case = struct {\n        base: [:0]const u8,\n        path: [:0]const u8,\n        expected: [:0]const u8,\n    };\n\n    const cases = [_]Case{\n        .{\n            .base = \"https://example/dir\",\n            .path = \"abc../test\",\n            .expected = \"https://example/abc../test\",\n        },\n        .{\n            .base = \"https://example/dir\",\n            .path = \"abc.\",\n            .expected = \"https://example/abc.\",\n        },\n        .{\n            .base = \"https://example/dir\",\n            .path = \"abc/.\",\n            .expected = \"https://example/abc/\",\n        },\n        .{\n            .base = \"https://example/xyz/abc/123\",\n            .path = \"something.js\",\n            .expected = \"https://example/xyz/abc/something.js\",\n        },\n        .{\n            .base = \"https://example/xyz/abc/123\",\n            .path = \"/something.js\",\n            .expected = \"https://example/something.js\",\n        },\n        .{\n            .base = \"https://example/\",\n            .path = \"something.js\",\n            .expected = \"https://example/something.js\",\n        },\n        .{\n            .base = \"https://example/\",\n            .path = \"/something.js\",\n            .expected = \"https://example/something.js\",\n        },\n        .{\n            .base = \"https://example\",\n            .path = \"something.js\",\n            .expected = \"https://example/something.js\",\n        },\n        .{\n            .base = \"https://example\",\n            .path = \"abc/something.js\",\n            .expected = \"https://example/abc/something.js\",\n        },\n        .{\n            .base = \"https://example/nested\",\n            .path = \"abc/something.js\",\n            .expected = \"https://example/abc/something.js\",\n        },\n        .{\n            .base = \"https://example/nested/\",\n            .path = \"abc/something.js\",\n            .expected = \"https://example/nested/abc/something.js\",\n        },\n        .{\n            .base = \"https://example/nested/\",\n            .path = \"/abc/something.js\",\n            .expected = \"https://example/abc/something.js\",\n        },\n        .{\n            .base = \"https://example/nested/\",\n            .path = \"http://www.github.com/example/\",\n            .expected = \"http://www.github.com/example/\",\n        },\n        .{\n            .base = \"https://example/nested/\",\n            .path = \"\",\n            .expected = \"https://example/nested/\",\n        },\n        .{\n            .base = \"https://example/abc/aaa\",\n            .path = \"./hello/./world\",\n            .expected = \"https://example/abc/hello/world\",\n        },\n        .{\n            .base = \"https://example/abc/aaa/\",\n            .path = \"../hello\",\n            .expected = \"https://example/abc/hello\",\n        },\n        .{\n            .base = \"https://example/abc/aaa\",\n            .path = \"../hello\",\n            .expected = \"https://example/hello\",\n        },\n        .{\n            .base = \"https://example/abc/aaa/\",\n            .path = \"./.././.././hello\",\n            .expected = \"https://example/hello\",\n        },\n        .{\n            .base = \"some/page\",\n            .path = \"hello\",\n            .expected = \"some/hello\",\n        },\n        .{\n            .base = \"some/page/\",\n            .path = \"hello\",\n            .expected = \"some/page/hello\",\n        },\n        .{\n            .base = \"some/page/other\",\n            .path = \".././hello\",\n            .expected = \"some/hello\",\n        },\n        .{\n            .base = \"https://www.example.com/hello/world\",\n            .path = \"//example/about\",\n            .expected = \"https://example/about\",\n        },\n        .{\n            .base = \"http:\",\n            .path = \"//example.com/over/9000\",\n            .expected = \"http://example.com/over/9000\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"../hello\",\n            .expected = \"https://example.com/hello\",\n        },\n        .{\n            .base = \"https://www.example.com/hello/world/\",\n            .path = \"../../../../example/about\",\n            .expected = \"https://www.example.com/example/about\",\n        },\n    };\n\n    for (cases) |case| {\n        const result = try resolve(testing.arena_allocator, case.base, case.path, .{});\n        try testing.expectString(case.expected, result);\n    }\n}\n\ntest \"URL: ensureEncoded\" {\n    defer testing.reset();\n\n    const Case = struct {\n        url: [:0]const u8,\n        expected: [:0]const u8,\n    };\n\n    const cases = [_]Case{\n        .{\n            .url = \"https://example.com/over 9000!\",\n            .expected = \"https://example.com/over%209000!\",\n        },\n        .{\n            .url = \"http://example.com/hello world.html\",\n            .expected = \"http://example.com/hello%20world.html\",\n        },\n        .{\n            .url = \"https://example.com/file[1].html\",\n            .expected = \"https://example.com/file%5B1%5D.html\",\n        },\n        .{\n            .url = \"https://example.com/file{name}.html\",\n            .expected = \"https://example.com/file%7Bname%7D.html\",\n        },\n        .{\n            .url = \"https://example.com/page?query=hello world\",\n            .expected = \"https://example.com/page?query=hello%20world\",\n        },\n        .{\n            .url = \"https://example.com/page?a=1&b=value with spaces\",\n            .expected = \"https://example.com/page?a=1&b=value%20with%20spaces\",\n        },\n        .{\n            .url = \"https://example.com/page#section one\",\n            .expected = \"https://example.com/page#section%20one\",\n        },\n        .{\n            .url = \"https://example.com/my path?query=my value#my anchor\",\n            .expected = \"https://example.com/my%20path?query=my%20value#my%20anchor\",\n        },\n        .{\n            .url = \"https://example.com/already%20encoded\",\n            .expected = \"https://example.com/already%20encoded\",\n        },\n        .{\n            .url = \"https://example.com/file%5B1%5D.html\",\n            .expected = \"https://example.com/file%5B1%5D.html\",\n        },\n        .{\n            .url = \"https://example.com/caf%C3%A9\",\n            .expected = \"https://example.com/caf%C3%A9\",\n        },\n        .{\n            .url = \"https://example.com/page?query=already%20encoded\",\n            .expected = \"https://example.com/page?query=already%20encoded\",\n        },\n        .{\n            .url = \"https://example.com/page?a=1&b=value%20here\",\n            .expected = \"https://example.com/page?a=1&b=value%20here\",\n        },\n        .{\n            .url = \"https://example.com/page#section%20one\",\n            .expected = \"https://example.com/page#section%20one\",\n        },\n        .{\n            .url = \"https://example.com/part%20encoded and not\",\n            .expected = \"https://example.com/part%20encoded%20and%20not\",\n        },\n        .{\n            .url = \"https://example.com/page?a=encoded%20value&b=not encoded\",\n            .expected = \"https://example.com/page?a=encoded%20value&b=not%20encoded\",\n        },\n        .{\n            .url = \"https://example.com/my%20path?query=not encoded#encoded%20anchor\",\n            .expected = \"https://example.com/my%20path?query=not%20encoded#encoded%20anchor\",\n        },\n        .{\n            .url = \"https://example.com/fully%20encoded?query=also%20encoded#and%20this\",\n            .expected = \"https://example.com/fully%20encoded?query=also%20encoded#and%20this\",\n        },\n        .{\n            .url = \"https://example.com/path-with_under~tilde\",\n            .expected = \"https://example.com/path-with_under~tilde\",\n        },\n        .{\n            .url = \"https://example.com/sub-delims!$&'()*+,;=\",\n            .expected = \"https://example.com/sub-delims!$&'()*+,;=\",\n        },\n        .{\n            .url = \"https://example.com\",\n            .expected = \"https://example.com\",\n        },\n        .{\n            .url = \"https://example.com?query=value\",\n            .expected = \"https://example.com?query=value\",\n        },\n        .{\n            .url = \"https://example.com/clean/path\",\n            .expected = \"https://example.com/clean/path\",\n        },\n        .{\n            .url = \"https://example.com/path?clean=query#clean-fragment\",\n            .expected = \"https://example.com/path?clean=query#clean-fragment\",\n        },\n        .{\n            .url = \"https://example.com/100% complete\",\n            .expected = \"https://example.com/100%25%20complete\",\n        },\n        .{\n            .url = \"https://example.com/path?value=100% done\",\n            .expected = \"https://example.com/path?value=100%25%20done\",\n        },\n        .{\n            .url = \"about:blank\",\n            .expected = \"about:blank\",\n        },\n    };\n\n    for (cases) |case| {\n        const result = try ensureEncoded(testing.arena_allocator, case.url);\n        try testing.expectString(case.expected, result);\n    }\n}\n\ntest \"URL: resolve with encoding\" {\n    defer testing.reset();\n\n    const Case = struct {\n        base: [:0]const u8,\n        path: [:0]const u8,\n        expected: [:0]const u8,\n    };\n\n    const cases = [_]Case{\n        // Spaces should be encoded as %20, but ! is allowed\n        .{\n            .base = \"https://example.com/dir/\",\n            .path = \"over 9000!\",\n            .expected = \"https://example.com/dir/over%209000!\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"hello world.html\",\n            .expected = \"https://example.com/hello%20world.html\",\n        },\n        // Multiple spaces\n        .{\n            .base = \"https://example.com/\",\n            .path = \"path with  multiple   spaces\",\n            .expected = \"https://example.com/path%20with%20%20multiple%20%20%20spaces\",\n        },\n        // Special characters that need encoding\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file[1].html\",\n            .expected = \"https://example.com/file%5B1%5D.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file{name}.html\",\n            .expected = \"https://example.com/file%7Bname%7D.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file<test>.html\",\n            .expected = \"https://example.com/file%3Ctest%3E.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file\\\"quote\\\".html\",\n            .expected = \"https://example.com/file%22quote%22.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file|pipe.html\",\n            .expected = \"https://example.com/file%7Cpipe.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file\\\\backslash.html\",\n            .expected = \"https://example.com/file%5Cbackslash.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file^caret.html\",\n            .expected = \"https://example.com/file%5Ecaret.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file`backtick`.html\",\n            .expected = \"https://example.com/file%60backtick%60.html\",\n        },\n        // Characters that should NOT be encoded\n        .{\n            .base = \"https://example.com/\",\n            .path = \"path-with_under~tilde.html\",\n            .expected = \"https://example.com/path-with_under~tilde.html\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"path/with/slashes\",\n            .expected = \"https://example.com/path/with/slashes\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"sub-delims!$&'()*+,;=.html\",\n            .expected = \"https://example.com/sub-delims!$&'()*+,;=.html\",\n        },\n        // Already encoded characters should not be double-encoded\n        .{\n            .base = \"https://example.com/\",\n            .path = \"already%20encoded\",\n            .expected = \"https://example.com/already%20encoded\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file%5B1%5D.html\",\n            .expected = \"https://example.com/file%5B1%5D.html\",\n        },\n        // Mix of encoded and unencoded\n        .{\n            .base = \"https://example.com/\",\n            .path = \"part%20encoded and not\",\n            .expected = \"https://example.com/part%20encoded%20and%20not\",\n        },\n        // Query strings and fragments ARE encoded\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file name.html?query=value with spaces\",\n            .expected = \"https://example.com/file%20name.html?query=value%20with%20spaces\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file name.html#anchor with spaces\",\n            .expected = \"https://example.com/file%20name.html#anchor%20with%20spaces\",\n        },\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file.html?hello=world !\",\n            .expected = \"https://example.com/file.html?hello=world%20!\",\n        },\n        // Query structural characters should NOT be encoded\n        .{\n            .base = \"https://example.com/\",\n            .path = \"file.html?a=1&b=2\",\n            .expected = \"https://example.com/file.html?a=1&b=2\",\n        },\n        // Relative paths with encoding\n        .{\n            .base = \"https://example.com/dir/page.html\",\n            .path = \"../other dir/file.html\",\n            .expected = \"https://example.com/other%20dir/file.html\",\n        },\n        .{\n            .base = \"https://example.com/dir/\",\n            .path = \"./sub dir/file.html\",\n            .expected = \"https://example.com/dir/sub%20dir/file.html\",\n        },\n        // Absolute paths with encoding\n        .{\n            .base = \"https://example.com/some/path\",\n            .path = \"/absolute path/file.html\",\n            .expected = \"https://example.com/absolute%20path/file.html\",\n        },\n        // Unicode/high bytes (though ideally these should be UTF-8 encoded first)\n        .{\n            .base = \"https://example.com/\",\n            .path = \"café\",\n            .expected = \"https://example.com/caf%C3%A9\",\n        },\n        // Empty path\n        .{\n            .base = \"https://example.com/\",\n            .path = \"\",\n            .expected = \"https://example.com/\",\n        },\n        // Complete URL as path (should not be encoded)\n        .{\n            .base = \"https://example.com/\",\n            .path = \"https://other.com/path with spaces\",\n            .expected = \"https://other.com/path%20with%20spaces\",\n        },\n    };\n\n    for (cases) |case| {\n        const result = try resolve(testing.arena_allocator, case.base, case.path, .{ .encode = true });\n        try testing.expectString(case.expected, result);\n    }\n}\n\ntest \"URL: eqlDocument\" {\n    defer testing.reset();\n    {\n        const url = \"https://lightpanda.io/about\";\n        try testing.expectEqual(true, eqlDocument(url, url));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about\";\n        const url2 = \"http://lightpanda.io/about\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about\";\n        const url2 = \"https://example.com/about\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io:8080/about\";\n        const url2 = \"https://lightpanda.io:9090/about\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about\";\n        const url2 = \"https://lightpanda.io/contact\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about?foo=bar\";\n        const url2 = \"https://lightpanda.io/about?baz=qux\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about#section1\";\n        const url2 = \"https://lightpanda.io/about#section2\";\n        try testing.expectEqual(true, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about\";\n        const url2 = \"https://lightpanda.io/about/\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about?foo=bar\";\n        const url2 = \"https://lightpanda.io/about\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about\";\n        const url2 = \"https://lightpanda.io/about?foo=bar\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about?foo=bar\";\n        const url2 = \"https://lightpanda.io/about?foo=bar\";\n        try testing.expectEqual(true, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://lightpanda.io/about?\";\n        const url2 = \"https://lightpanda.io/about\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n    {\n        const url1 = \"https://duckduckgo.com/\";\n        const url2 = \"https://duckduckgo.com/?q=lightpanda\";\n        try testing.expectEqual(false, eqlDocument(url1, url2));\n    }\n}\n\ntest \"URL: concatQueryString\" {\n    defer testing.reset();\n    const arena = testing.arena_allocator;\n\n    {\n        const url = try concatQueryString(arena, \"https://www.lightpanda.io/\", \"\");\n        try testing.expectEqual(\"https://www.lightpanda.io/\", url);\n    }\n\n    {\n        const url = try concatQueryString(arena, \"https://www.lightpanda.io/index?\", \"\");\n        try testing.expectEqual(\"https://www.lightpanda.io/index?\", url);\n    }\n\n    {\n        const url = try concatQueryString(arena, \"https://www.lightpanda.io/index?\", \"a=b\");\n        try testing.expectEqual(\"https://www.lightpanda.io/index?a=b\", url);\n    }\n\n    {\n        const url = try concatQueryString(arena, \"https://www.lightpanda.io/index?1=2\", \"a=b\");\n        try testing.expectEqual(\"https://www.lightpanda.io/index?1=2&a=b\", url);\n    }\n\n    {\n        const url = try concatQueryString(arena, \"https://www.lightpanda.io/index?1=2&\", \"a=b\");\n        try testing.expectEqual(\"https://www.lightpanda.io/index?1=2&a=b\", url);\n    }\n}\n\ntest \"URL: getRobotsUrl\" {\n    defer testing.reset();\n    const arena = testing.arena_allocator;\n\n    {\n        const url = try getRobotsUrl(arena, \"https://www.lightpanda.io\");\n        try testing.expectEqual(\"https://www.lightpanda.io/robots.txt\", url);\n    }\n\n    {\n        const url = try getRobotsUrl(arena, \"https://www.lightpanda.io/some/path\");\n        try testing.expectString(\"https://www.lightpanda.io/robots.txt\", url);\n    }\n\n    {\n        const url = try getRobotsUrl(arena, \"https://www.lightpanda.io:8080/page\");\n        try testing.expectString(\"https://www.lightpanda.io:8080/robots.txt\", url);\n    }\n    {\n        const url = try getRobotsUrl(arena, \"http://example.com/deep/nested/path?query=value#fragment\");\n        try testing.expectString(\"http://example.com/robots.txt\", url);\n    }\n    {\n        const url = try getRobotsUrl(arena, \"https://user:pass@example.com/page\");\n        try testing.expectString(\"https://example.com/robots.txt\", url);\n    }\n}\n\ntest \"URL: unescape\" {\n    defer testing.reset();\n    const arena = testing.arena_allocator;\n\n    {\n        const result = try unescape(arena, \"hello world\");\n        try testing.expectEqual(\"hello world\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"hello%20world\");\n        try testing.expectEqual(\"hello world\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"%48%65%6c%6c%6f\");\n        try testing.expectEqual(\"Hello\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"%48%65%6C%6C%6F\");\n        try testing.expectEqual(\"Hello\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"a%3Db\");\n        try testing.expectEqual(\"a=b\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"a%3DB\");\n        try testing.expectEqual(\"a=B\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"ZDIgPSAndHdvJzs%3D\");\n        try testing.expectEqual(\"ZDIgPSAndHdvJzs=\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"%5a%44%4d%67%50%53%41%6e%64%47%68%79%5a%57%55%6e%4f%77%3D%3D\");\n        try testing.expectEqual(\"ZDMgPSAndGhyZWUnOw==\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"hello%2world\");\n        try testing.expectEqual(\"hello%2world\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"hello%ZZworld\");\n        try testing.expectEqual(\"hello%ZZworld\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"hello%\");\n        try testing.expectEqual(\"hello%\", result);\n    }\n\n    {\n        const result = try unescape(arena, \"hello%2\");\n        try testing.expectEqual(\"hello%2\", result);\n    }\n}\n\ntest \"URL: getHost\" {\n    try testing.expectEqualSlices(u8, \"example.com:8080\", getHost(\"https://example.com:8080/path\"));\n    try testing.expectEqualSlices(u8, \"example.com\", getHost(\"https://example.com/path\"));\n    try testing.expectEqualSlices(u8, \"example.com:443\", getHost(\"https://example.com:443/\"));\n    try testing.expectEqualSlices(u8, \"example.com\", getHost(\"https://user:pass@example.com/page\"));\n    try testing.expectEqualSlices(u8, \"example.com:8080\", getHost(\"https://user:pass@example.com:8080/page\"));\n    try testing.expectEqualSlices(u8, \"\", getHost(\"not-a-url\"));\n}\n"
  },
  {
    "path": "src/browser/actions.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"../lightpanda.zig\");\nconst DOMNode = @import(\"webapi/Node.zig\");\nconst Element = @import(\"webapi/Element.zig\");\nconst Event = @import(\"webapi/Event.zig\");\nconst MouseEvent = @import(\"webapi/event/MouseEvent.zig\");\nconst Page = @import(\"Page.zig\");\n\npub fn click(node: *DOMNode, page: *Page) !void {\n    const el = node.is(Element) orelse return error.InvalidNodeType;\n\n    const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap(\"click\"), .{\n        .bubbles = true,\n        .cancelable = true,\n        .composed = true,\n        .clientX = 0,\n        .clientY = 0,\n    }, page);\n\n    page._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| {\n        lp.log.err(.app, \"click failed\", .{ .err = err });\n        return error.ActionFailed;\n    };\n}\n\npub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {\n    const el = node.is(Element) orelse return error.InvalidNodeType;\n\n    if (el.is(Element.Html.Input)) |input| {\n        input.setValue(text, page) catch |err| {\n            lp.log.err(.app, \"fill input failed\", .{ .err = err });\n            return error.ActionFailed;\n        };\n    } else if (el.is(Element.Html.TextArea)) |textarea| {\n        textarea.setValue(text, page) catch |err| {\n            lp.log.err(.app, \"fill textarea failed\", .{ .err = err });\n            return error.ActionFailed;\n        };\n    } else if (el.is(Element.Html.Select)) |select| {\n        select.setValue(text, page) catch |err| {\n            lp.log.err(.app, \"fill select failed\", .{ .err = err });\n            return error.ActionFailed;\n        };\n    } else {\n        return error.InvalidNodeType;\n    }\n\n    const input_evt: *Event = try .initTrusted(comptime .wrap(\"input\"), .{ .bubbles = true }, page);\n    page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {\n        lp.log.err(.app, \"dispatch input event failed\", .{ .err = err });\n    };\n\n    const change_evt: *Event = try .initTrusted(comptime .wrap(\"change\"), .{ .bubbles = true }, page);\n    page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {\n        lp.log.err(.app, \"dispatch change event failed\", .{ .err = err });\n    };\n}\n\npub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {\n    if (node) |n| {\n        const el = n.is(Element) orelse return error.InvalidNodeType;\n\n        if (x) |val| {\n            el.setScrollLeft(val, page) catch |err| {\n                lp.log.err(.app, \"setScrollLeft failed\", .{ .err = err });\n                return error.ActionFailed;\n            };\n        }\n        if (y) |val| {\n            el.setScrollTop(val, page) catch |err| {\n                lp.log.err(.app, \"setScrollTop failed\", .{ .err = err });\n                return error.ActionFailed;\n            };\n        }\n\n        const scroll_evt: *Event = try .initTrusted(comptime .wrap(\"scroll\"), .{ .bubbles = true }, page);\n        page._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {\n            lp.log.err(.app, \"dispatch scroll event failed\", .{ .err = err });\n        };\n    } else {\n        page.window.scrollTo(.{ .x = x orelse 0 }, y, page) catch |err| {\n            lp.log.err(.app, \"scroll failed\", .{ .err = err });\n            return error.ActionFailed;\n        };\n    }\n}\n"
  },
  {
    "path": "src/browser/color.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Io = std.Io;\n\npub fn isHexColor(value: []const u8) bool {\n    if (value.len == 0) {\n        return false;\n    }\n\n    if (value[0] != '#') {\n        return false;\n    }\n\n    const hex_part = value[1..];\n    switch (hex_part.len) {\n        3, 4, 6, 8 => for (hex_part) |c| if (!std.ascii.isHex(c)) return false,\n        else => return false,\n    }\n\n    return true;\n}\n\npub const RGBA = packed struct(u32) {\n    r: u8,\n    g: u8,\n    b: u8,\n    /// Opaque by default.\n    a: u8 = std.math.maxInt(u8),\n\n    pub const Named = struct {\n        // Basic colors (CSS Level 1)\n        pub const black: RGBA = .init(0, 0, 0, 1);\n        pub const silver: RGBA = .init(192, 192, 192, 1);\n        pub const gray: RGBA = .init(128, 128, 128, 1);\n        pub const white: RGBA = .init(255, 255, 255, 1);\n        pub const maroon: RGBA = .init(128, 0, 0, 1);\n        pub const red: RGBA = .init(255, 0, 0, 1);\n        pub const purple: RGBA = .init(128, 0, 128, 1);\n        pub const fuchsia: RGBA = .init(255, 0, 255, 1);\n        pub const green: RGBA = .init(0, 128, 0, 1);\n        pub const lime: RGBA = .init(0, 255, 0, 1);\n        pub const olive: RGBA = .init(128, 128, 0, 1);\n        pub const yellow: RGBA = .init(255, 255, 0, 1);\n        pub const navy: RGBA = .init(0, 0, 128, 1);\n        pub const blue: RGBA = .init(0, 0, 255, 1);\n        pub const teal: RGBA = .init(0, 128, 128, 1);\n        pub const aqua: RGBA = .init(0, 255, 255, 1);\n\n        // Extended colors (CSS Level 2+)\n        pub const aliceblue: RGBA = .init(240, 248, 255, 1);\n        pub const antiquewhite: RGBA = .init(250, 235, 215, 1);\n        pub const aquamarine: RGBA = .init(127, 255, 212, 1);\n        pub const azure: RGBA = .init(240, 255, 255, 1);\n        pub const beige: RGBA = .init(245, 245, 220, 1);\n        pub const bisque: RGBA = .init(255, 228, 196, 1);\n        pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);\n        pub const blueviolet: RGBA = .init(138, 43, 226, 1);\n        pub const brown: RGBA = .init(165, 42, 42, 1);\n        pub const burlywood: RGBA = .init(222, 184, 135, 1);\n        pub const cadetblue: RGBA = .init(95, 158, 160, 1);\n        pub const chartreuse: RGBA = .init(127, 255, 0, 1);\n        pub const chocolate: RGBA = .init(210, 105, 30, 1);\n        pub const coral: RGBA = .init(255, 127, 80, 1);\n        pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);\n        pub const cornsilk: RGBA = .init(255, 248, 220, 1);\n        pub const crimson: RGBA = .init(220, 20, 60, 1);\n        pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua\n        pub const darkblue: RGBA = .init(0, 0, 139, 1);\n        pub const darkcyan: RGBA = .init(0, 139, 139, 1);\n        pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);\n        pub const darkgray: RGBA = .init(169, 169, 169, 1);\n        pub const darkgreen: RGBA = .init(0, 100, 0, 1);\n        pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray\n        pub const darkkhaki: RGBA = .init(189, 183, 107, 1);\n        pub const darkmagenta: RGBA = .init(139, 0, 139, 1);\n        pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);\n        pub const darkorange: RGBA = .init(255, 140, 0, 1);\n        pub const darkorchid: RGBA = .init(153, 50, 204, 1);\n        pub const darkred: RGBA = .init(139, 0, 0, 1);\n        pub const darksalmon: RGBA = .init(233, 150, 122, 1);\n        pub const darkseagreen: RGBA = .init(143, 188, 143, 1);\n        pub const darkslateblue: RGBA = .init(72, 61, 139, 1);\n        pub const darkslategray: RGBA = .init(47, 79, 79, 1);\n        pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray\n        pub const darkturquoise: RGBA = .init(0, 206, 209, 1);\n        pub const darkviolet: RGBA = .init(148, 0, 211, 1);\n        pub const deeppink: RGBA = .init(255, 20, 147, 1);\n        pub const deepskyblue: RGBA = .init(0, 191, 255, 1);\n        pub const dimgray: RGBA = .init(105, 105, 105, 1);\n        pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray\n        pub const dodgerblue: RGBA = .init(30, 144, 255, 1);\n        pub const firebrick: RGBA = .init(178, 34, 34, 1);\n        pub const floralwhite: RGBA = .init(255, 250, 240, 1);\n        pub const forestgreen: RGBA = .init(34, 139, 34, 1);\n        pub const gainsboro: RGBA = .init(220, 220, 220, 1);\n        pub const ghostwhite: RGBA = .init(248, 248, 255, 1);\n        pub const gold: RGBA = .init(255, 215, 0, 1);\n        pub const goldenrod: RGBA = .init(218, 165, 32, 1);\n        pub const greenyellow: RGBA = .init(173, 255, 47, 1);\n        pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray\n        pub const honeydew: RGBA = .init(240, 255, 240, 1);\n        pub const hotpink: RGBA = .init(255, 105, 180, 1);\n        pub const indianred: RGBA = .init(205, 92, 92, 1);\n        pub const indigo: RGBA = .init(75, 0, 130, 1);\n        pub const ivory: RGBA = .init(255, 255, 240, 1);\n        pub const khaki: RGBA = .init(240, 230, 140, 1);\n        pub const lavender: RGBA = .init(230, 230, 250, 1);\n        pub const lavenderblush: RGBA = .init(255, 240, 245, 1);\n        pub const lawngreen: RGBA = .init(124, 252, 0, 1);\n        pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);\n        pub const lightblue: RGBA = .init(173, 216, 230, 1);\n        pub const lightcoral: RGBA = .init(240, 128, 128, 1);\n        pub const lightcyan: RGBA = .init(224, 255, 255, 1);\n        pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);\n        pub const lightgray: RGBA = .init(211, 211, 211, 1);\n        pub const lightgreen: RGBA = .init(144, 238, 144, 1);\n        pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray\n        pub const lightpink: RGBA = .init(255, 182, 193, 1);\n        pub const lightsalmon: RGBA = .init(255, 160, 122, 1);\n        pub const lightseagreen: RGBA = .init(32, 178, 170, 1);\n        pub const lightskyblue: RGBA = .init(135, 206, 250, 1);\n        pub const lightslategray: RGBA = .init(119, 136, 153, 1);\n        pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray\n        pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);\n        pub const lightyellow: RGBA = .init(255, 255, 224, 1);\n        pub const limegreen: RGBA = .init(50, 205, 50, 1);\n        pub const linen: RGBA = .init(250, 240, 230, 1);\n        pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia\n        pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);\n        pub const mediumblue: RGBA = .init(0, 0, 205, 1);\n        pub const mediumorchid: RGBA = .init(186, 85, 211, 1);\n        pub const mediumpurple: RGBA = .init(147, 112, 219, 1);\n        pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);\n        pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);\n        pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);\n        pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);\n        pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);\n        pub const midnightblue: RGBA = .init(25, 25, 112, 1);\n        pub const mintcream: RGBA = .init(245, 255, 250, 1);\n        pub const mistyrose: RGBA = .init(255, 228, 225, 1);\n        pub const moccasin: RGBA = .init(255, 228, 181, 1);\n        pub const navajowhite: RGBA = .init(255, 222, 173, 1);\n        pub const oldlace: RGBA = .init(253, 245, 230, 1);\n        pub const olivedrab: RGBA = .init(107, 142, 35, 1);\n        pub const orange: RGBA = .init(255, 165, 0, 1);\n        pub const orangered: RGBA = .init(255, 69, 0, 1);\n        pub const orchid: RGBA = .init(218, 112, 214, 1);\n        pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);\n        pub const palegreen: RGBA = .init(152, 251, 152, 1);\n        pub const paleturquoise: RGBA = .init(175, 238, 238, 1);\n        pub const palevioletred: RGBA = .init(219, 112, 147, 1);\n        pub const papayawhip: RGBA = .init(255, 239, 213, 1);\n        pub const peachpuff: RGBA = .init(255, 218, 185, 1);\n        pub const peru: RGBA = .init(205, 133, 63, 1);\n        pub const pink: RGBA = .init(255, 192, 203, 1);\n        pub const plum: RGBA = .init(221, 160, 221, 1);\n        pub const powderblue: RGBA = .init(176, 224, 230, 1);\n        pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);\n        pub const rosybrown: RGBA = .init(188, 143, 143, 1);\n        pub const royalblue: RGBA = .init(65, 105, 225, 1);\n        pub const saddlebrown: RGBA = .init(139, 69, 19, 1);\n        pub const salmon: RGBA = .init(250, 128, 114, 1);\n        pub const sandybrown: RGBA = .init(244, 164, 96, 1);\n        pub const seagreen: RGBA = .init(46, 139, 87, 1);\n        pub const seashell: RGBA = .init(255, 245, 238, 1);\n        pub const sienna: RGBA = .init(160, 82, 45, 1);\n        pub const skyblue: RGBA = .init(135, 206, 235, 1);\n        pub const slateblue: RGBA = .init(106, 90, 205, 1);\n        pub const slategray: RGBA = .init(112, 128, 144, 1);\n        pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray\n        pub const snow: RGBA = .init(255, 250, 250, 1);\n        pub const springgreen: RGBA = .init(0, 255, 127, 1);\n        pub const steelblue: RGBA = .init(70, 130, 180, 1);\n        pub const tan: RGBA = .init(210, 180, 140, 1);\n        pub const thistle: RGBA = .init(216, 191, 216, 1);\n        pub const tomato: RGBA = .init(255, 99, 71, 1);\n        pub const transparent: RGBA = .init(0, 0, 0, 0);\n        pub const turquoise: RGBA = .init(64, 224, 208, 1);\n        pub const violet: RGBA = .init(238, 130, 238, 1);\n        pub const wheat: RGBA = .init(245, 222, 179, 1);\n        pub const whitesmoke: RGBA = .init(245, 245, 245, 1);\n        pub const yellowgreen: RGBA = .init(154, 205, 50, 1);\n    };\n\n    pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {\n        const clamped = std.math.clamp(a, 0, 1);\n        return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };\n    }\n\n    /// Finds a color by its name.\n    pub fn find(name: []const u8) ?RGBA {\n        const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;\n\n        return switch (match) {\n            inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),\n        };\n    }\n\n    /// Parses the given color.\n    /// Currently we only parse hex colors and named colors; other variants\n    /// require CSS evaluation.\n    pub fn parse(input: []const u8) !RGBA {\n        if (!isHexColor(input)) {\n            // Try named colors.\n            return find(input) orelse return error.Invalid;\n        }\n\n        const slice = input[1..];\n        switch (slice.len) {\n            // This means the digit for a color is repeated.\n            // Given HEX is #f0c, its interpreted the same as #FF00CC.\n            3 => {\n                const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);\n                const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);\n                const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);\n                return .{ .r = r, .g = g, .b = b, .a = 255 };\n            },\n            4 => {\n                const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);\n                const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);\n                const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);\n                const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);\n                return .{ .r = r, .g = g, .b = b, .a = a };\n            },\n            // Regular HEX format.\n            6 => {\n                const r = try std.fmt.parseInt(u8, slice[0..2], 16);\n                const g = try std.fmt.parseInt(u8, slice[2..4], 16);\n                const b = try std.fmt.parseInt(u8, slice[4..6], 16);\n                return .{ .r = r, .g = g, .b = b, .a = 255 };\n            },\n            8 => {\n                const r = try std.fmt.parseInt(u8, slice[0..2], 16);\n                const g = try std.fmt.parseInt(u8, slice[2..4], 16);\n                const b = try std.fmt.parseInt(u8, slice[4..6], 16);\n                const a = try std.fmt.parseInt(u8, slice[6..8], 16);\n                return .{ .r = r, .g = g, .b = b, .a = a };\n            },\n            else => return error.Invalid,\n        }\n    }\n\n    /// By default, browsers prefer lowercase formatting.\n    const format_upper = false;\n\n    /// Formats the `Color` according to web expectations.\n    /// If color is opaque, HEX is preferred; RGBA otherwise.\n    pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {\n        if (self.isOpaque()) {\n            // Convert RGB to HEX.\n            // https://gristle.tripod.com/hexconv.html\n            // Hexadecimal characters up to 15.\n            const char: []const u8 = \"0123456789\" ++ if (format_upper) \"ABCDEF\" else \"abcdef\";\n            // This variant always prefers 6 digit format, +1 is for hash char.\n            const buffer = [7]u8{\n                '#',\n                char[self.r >> 4],\n                char[self.r & 15],\n                char[self.g >> 4],\n                char[self.g & 15],\n                char[self.b >> 4],\n                char[self.b & 15],\n            };\n\n            return writer.writeAll(&buffer);\n        }\n\n        // Prefer RGBA format for everything else.\n        return writer.print(\"rgba({d}, {d}, {d}, {d:.2})\", .{ self.r, self.g, self.b, self.normalizedAlpha() });\n    }\n\n    /// Returns true if `Color` is opaque.\n    pub inline fn isOpaque(self: *const RGBA) bool {\n        return self.a == std.math.maxInt(u8);\n    }\n\n    /// Returns the normalized alpha value.\n    pub inline fn normalizedAlpha(self: *const RGBA) f32 {\n        return @as(f32, @floatFromInt(self.a)) / 255;\n    }\n};\n"
  },
  {
    "path": "src/browser/css/Parser.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Tokenizer = @import(\"Tokenizer.zig\");\n\npub const Declaration = struct {\n    name: []const u8,\n    value: []const u8,\n    important: bool,\n};\n\nconst TokenSpan = struct {\n    token: Tokenizer.Token,\n    start: usize,\n    end: usize,\n};\n\nconst TokenStream = struct {\n    tokenizer: Tokenizer,\n    peeked: ?TokenSpan = null,\n\n    fn init(input: []const u8) TokenStream {\n        return .{ .tokenizer = .{ .input = input } };\n    }\n\n    fn nextRaw(self: *TokenStream) ?TokenSpan {\n        const start = self.tokenizer.position;\n        const token = self.tokenizer.next() orelse return null;\n        const end = self.tokenizer.position;\n        return .{ .token = token, .start = start, .end = end };\n    }\n\n    fn next(self: *TokenStream) ?TokenSpan {\n        if (self.peeked) |token| {\n            self.peeked = null;\n            return token;\n        }\n        return self.nextRaw();\n    }\n\n    fn peek(self: *TokenStream) ?TokenSpan {\n        if (self.peeked == null) {\n            self.peeked = self.nextRaw();\n        }\n        return self.peeked;\n    }\n};\n\npub fn parseDeclarationsList(input: []const u8) DeclarationsIterator {\n    return DeclarationsIterator.init(input);\n}\n\npub const DeclarationsIterator = struct {\n    input: []const u8,\n    stream: TokenStream,\n\n    pub fn init(input: []const u8) DeclarationsIterator {\n        return .{\n            .input = input,\n            .stream = TokenStream.init(input),\n        };\n    }\n\n    pub fn next(self: *DeclarationsIterator) ?Declaration {\n        while (true) {\n            self.skipTriviaAndSemicolons();\n            const peeked = self.stream.peek() orelse return null;\n\n            switch (peeked.token) {\n                .at_keyword => {\n                    _ = self.stream.next();\n                    self.skipAtRule();\n                },\n                .ident => |name| {\n                    _ = self.stream.next();\n                    if (self.consumeDeclaration(name)) |declaration| {\n                        return declaration;\n                    }\n                },\n                else => {\n                    _ = self.stream.next();\n                    self.skipInvalidDeclaration();\n                },\n            }\n        }\n\n        return null;\n    }\n\n    fn consumeDeclaration(self: *DeclarationsIterator, name: []const u8) ?Declaration {\n        self.skipTrivia();\n\n        const colon = self.stream.next() orelse return null;\n        if (!isColon(colon.token)) {\n            self.skipInvalidDeclaration();\n            return null;\n        }\n\n        const value = self.consumeValue() orelse return null;\n        return .{\n            .name = name,\n            .value = value.value,\n            .important = value.important,\n        };\n    }\n\n    const ValueResult = struct {\n        value: []const u8,\n        important: bool,\n    };\n\n    fn consumeValue(self: *DeclarationsIterator) ?ValueResult {\n        self.skipTrivia();\n\n        var depth: usize = 0;\n        var start: ?usize = null;\n        var last_sig: ?TokenSpan = null;\n        var prev_sig: ?TokenSpan = null;\n\n        while (true) {\n            const peeked = self.stream.peek() orelse break;\n            if (isSemicolon(peeked.token) and depth == 0) {\n                _ = self.stream.next();\n                break;\n            }\n\n            const span = self.stream.next() orelse break;\n            if (isWhitespaceOrComment(span.token)) {\n                continue;\n            }\n\n            if (start == null) start = span.start;\n            prev_sig = last_sig;\n            last_sig = span;\n            updateDepth(span.token, &depth);\n        }\n\n        const value_start = start orelse return null;\n        const last = last_sig orelse return null;\n\n        var important = false;\n        var end_pos = last.end;\n\n        if (isImportantPair(prev_sig, last)) {\n            important = true;\n            const bang = prev_sig orelse return null;\n            if (value_start >= bang.start) return null;\n            end_pos = bang.start;\n        }\n\n        var value_slice = self.input[value_start..end_pos];\n        value_slice = std.mem.trim(u8, value_slice, &std.ascii.whitespace);\n        if (value_slice.len == 0) return null;\n\n        return .{ .value = value_slice, .important = important };\n    }\n\n    fn skipTrivia(self: *DeclarationsIterator) void {\n        while (self.stream.peek()) |peeked| {\n            if (!isWhitespaceOrComment(peeked.token)) break;\n            _ = self.stream.next();\n        }\n    }\n\n    fn skipTriviaAndSemicolons(self: *DeclarationsIterator) void {\n        while (self.stream.peek()) |peeked| {\n            if (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token)) {\n                _ = self.stream.next();\n            } else {\n                break;\n            }\n        }\n    }\n\n    fn skipAtRule(self: *DeclarationsIterator) void {\n        var depth: usize = 0;\n        var saw_block = false;\n\n        while (true) {\n            const peeked = self.stream.peek() orelse return;\n            if (!saw_block and isSemicolon(peeked.token) and depth == 0) {\n                _ = self.stream.next();\n                return;\n            }\n\n            const span = self.stream.next() orelse return;\n            if (isWhitespaceOrComment(span.token)) continue;\n\n            if (isBlockStart(span.token)) {\n                depth += 1;\n                saw_block = true;\n            } else if (isBlockEnd(span.token)) {\n                if (depth > 0) depth -= 1;\n                if (saw_block and depth == 0) return;\n            }\n        }\n    }\n\n    fn skipInvalidDeclaration(self: *DeclarationsIterator) void {\n        var depth: usize = 0;\n\n        while (self.stream.peek()) |peeked| {\n            if (isSemicolon(peeked.token) and depth == 0) {\n                _ = self.stream.next();\n                return;\n            }\n\n            const span = self.stream.next() orelse return;\n            if (isWhitespaceOrComment(span.token)) continue;\n            updateDepth(span.token, &depth);\n        }\n    }\n};\n\nfn isWhitespaceOrComment(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .white_space, .comment => true,\n        else => false,\n    };\n}\n\nfn isSemicolon(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .semicolon => true,\n        else => false,\n    };\n}\n\nfn isColon(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .colon => true,\n        else => false,\n    };\n}\n\nfn isBlockStart(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .curly_bracket_block, .square_bracket_block, .parenthesis_block, .function => true,\n        else => false,\n    };\n}\n\nfn isBlockEnd(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .close_curly_bracket, .close_parenthesis, .close_square_bracket => true,\n        else => false,\n    };\n}\n\nfn updateDepth(token: Tokenizer.Token, depth: *usize) void {\n    if (isBlockStart(token)) {\n        depth.* += 1;\n        return;\n    }\n\n    if (isBlockEnd(token)) {\n        if (depth.* > 0) depth.* -= 1;\n    }\n}\n\nfn isImportantPair(prev_sig: ?TokenSpan, last_sig: TokenSpan) bool {\n    if (!isIdentImportant(last_sig.token)) return false;\n    const prev = prev_sig orelse return false;\n    return isBang(prev.token);\n}\n\nfn isIdentImportant(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .ident => |name| std.ascii.eqlIgnoreCase(name, \"important\"),\n        else => false,\n    };\n}\n\nfn isBang(token: Tokenizer.Token) bool {\n    return switch (token) {\n        .delim => |c| c == '!',\n        else => false,\n    };\n}\n"
  },
  {
    "path": "src/browser/css/Tokenizer.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n//! This file implements the tokenization step defined in the CSS Syntax Module Level 3 specification.\n//!\n//! The algorithm accepts a valid UTF-8 string and returns a stream of tokens.\n//! The tokenization step never fails, even for complete gibberish.\n//! Validity must then be checked by the parser.\n//!\n//! NOTE: The tokenizer is not thread-safe and does not own any memory, and does not check the validity of utf8.\n//!\n//! See spec for more info: https://drafts.csswg.org/css-syntax/#tokenization\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst assert = std.debug.assert;\n\nconst Tokenizer = @This();\n\npub const Token = union(enum) {\n    /// A `<ident-token>`\n    ident: []const u8,\n\n    /// A `<function-token>`\n    ///\n    /// The value (name) does not include the `(` marker.\n    function: []const u8,\n\n    /// A `<at-keyword-token>`\n    ///\n    /// The value does not include the `@` marker.\n    at_keyword: []const u8,\n\n    /// A `<hash-token>` with the type flag set to \"id\"\n    ///\n    /// The value does not include the `#` marker.\n    id_hash: []const u8, // Hash that is a valid ID selector.\n\n    /// A `<hash-token>` with the type flag set to \"unrestricted\"\n    ///\n    /// The value does not include the `#` marker.\n    unrestricted_hash: []const u8,\n\n    /// A `<string-token>`\n    ///\n    /// The value does not include the quotes.\n    string: []const u8,\n\n    /// A `<bad-string-token>`\n    ///\n    /// This token always indicates a parse error.\n    bad_string: []const u8,\n\n    /// A `<url-token>`\n    ///\n    /// The value does not include the `url(` `)` markers.  Note that `url( <string-token> )` is represented by a\n    /// `Function` token.\n    url: []const u8,\n\n    /// A `<bad-url-token>`\n    ///\n    /// This token always indicates a parse error.\n    bad_url: []const u8,\n\n    /// A `<delim-token>`\n    delim: u8,\n\n    /// A `<number-token>`\n    number: struct {\n        /// Whether the number had a `+` or `-` sign.\n        ///\n        /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)\n        has_sign: bool,\n\n        /// If the origin source did not include a fractional part, the value as an integer.\n        int_value: ?i32,\n\n        /// The value as a float\n        value: f32,\n    },\n\n    /// A `<percentage-token>`\n    percentage: struct {\n        /// Whether the number had a `+` or `-` sign.\n        has_sign: bool,\n\n        /// If the origin source did not include a fractional part, the value as an integer.\n        /// It is **not** divided by 100.\n        int_value: ?i32,\n\n        /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.\n        unit_value: f32,\n    },\n\n    /// A `<dimension-token>`\n    dimension: struct {\n        /// Whether the number had a `+` or `-` sign.\n        ///\n        /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)\n        has_sign: bool,\n\n        /// If the origin source did not include a fractional part, the value as an integer.\n        int_value: ?i32,\n\n        /// The value as a float\n        value: f32,\n\n        /// The unit, e.g. \"px\" in `12px`\n        unit: []const u8,\n    },\n\n    /// A `<unicode-range-token>`\n    unicode_range: struct { bgn: u32, end: i32 },\n\n    /// A `<whitespace-token>`\n    white_space: []const u8,\n\n    /// A `<!--` `<CDO-token>`\n    cdo,\n\n    /// A `-->` `<CDC-token>`\n    cdc,\n\n    /// A `:` `<colon-token>`\n    colon, // :\n\n    /// A `;` `<semicolon-token>`\n    semicolon, // ;\n\n    /// A `,` `<comma-token>`\n    comma, // ,\n\n    /// A `<[-token>`\n    square_bracket_block,\n\n    /// A `<]-token>`\n    ///\n    /// When obtained from one of the `Parser::next*` methods,\n    /// this token is always unmatched and indicates a parse error.\n    close_square_bracket,\n\n    /// A `<(-token>`\n    parenthesis_block,\n\n    /// A `<)-token>`\n    ///\n    /// When obtained from one of the `Parser::next*` methods,\n    /// this token is always unmatched and indicates a parse error.\n    close_parenthesis,\n\n    /// A `<{-token>`\n    curly_bracket_block,\n\n    /// A `<}-token>`\n    ///\n    /// When obtained from one of the `Parser::next*` methods,\n    /// this token is always unmatched and indicates a parse error.\n    close_curly_bracket,\n\n    /// A comment.\n    ///\n    /// The CSS Syntax spec does not generate tokens for comments,\n    /// But we do for simplicity of the interface.\n    ///\n    /// The value does not include the `/*` `*/` markers.\n    comment: []const u8,\n};\n\ninput: []const u8,\n\n/// Counted in bytes, not code points. From 0.\nposition: usize = 0,\n\n// If true, the input has at least `n` bytes left *after* the current one.\n// That is, `Lexer.byteAt(n)` will not panic.\nfn hasAtLeast(self: *const Tokenizer, n: usize) bool {\n    return self.position + n < self.input.len;\n}\n\nfn isEof(self: *const Tokenizer) bool {\n    return !self.hasAtLeast(0);\n}\n\nfn byteAt(self: *const Tokenizer, offset: usize) u8 {\n    return self.input[self.position + offset];\n}\n\n// Assumes non-EOF\nfn nextByteUnchecked(self: *const Tokenizer) u8 {\n    return self.byteAt(0);\n}\n\nfn nextByte(self: *const Tokenizer) ?u8 {\n    return if (self.isEof())\n        null\n    else\n        self.input[self.position];\n}\n\nfn startsWith(self: *const Tokenizer, needle: []const u8) bool {\n    return std.mem.startsWith(u8, self.input[self.position..], needle);\n}\n\nfn slice(self: *const Tokenizer, start: usize, end: usize) []const u8 {\n    return self.input[start..end];\n}\n\nfn sliceFrom(self: *const Tokenizer, start_pos: usize) []const u8 {\n    return self.slice(start_pos, self.position);\n}\n\n// Advance over N bytes in the input.  This function can advance\n// over ASCII bytes (excluding newlines), or UTF-8 sequence\n// leaders (excluding leaders for 4-byte sequences).\nfn advance(self: *Tokenizer, n: usize) void {\n    if (builtin.mode == .Debug) {\n        // Each byte must either be an ASCII byte or a sequence leader,\n        // but not a 4-byte leader; also newlines are rejected.\n        for (0..n) |i| {\n            const b = self.byteAt(i);\n            assert(b != '\\r' and b != '\\n' and b != '\\x0C');\n            assert(b <= 0x7F or (b & 0xF0 != 0xF0 and b & 0xC0 != 0x80));\n        }\n    }\n    self.position += n;\n}\n\nfn hasNewlineAt(self: *const Tokenizer, offset: usize) bool {\n    if (!self.hasAtLeast(offset)) return false;\n\n    return switch (self.byteAt(offset)) {\n        '\\n', '\\r', '\\x0C' => true,\n        else => false,\n    };\n}\n\nfn hasNonAsciiAt(self: *const Tokenizer, offset: usize) bool {\n    if (!self.hasAtLeast(offset)) return false;\n\n    const byte = self.byteAt(offset);\n    const len_utf8 = std.unicode.utf8ByteSequenceLength(byte) catch return false;\n\n    if (!self.hasAtLeast(offset + len_utf8 - 1)) return false;\n\n    const start = self.position + offset;\n    const bytes = self.slice(start, start + len_utf8);\n\n    const codepoint = std.unicode.utf8Decode(bytes) catch return false;\n\n    // https://drafts.csswg.org/css-syntax/#non-ascii-ident-code-point\n    return switch (codepoint) {\n        '\\u{00B7}', '\\u{200C}', '\\u{200D}', '\\u{203F}', '\\u{2040}' => true,\n        '\\u{00C0}'...'\\u{00D6}' => true,\n        '\\u{00D8}'...'\\u{00F6}' => true,\n        '\\u{00F8}'...'\\u{037D}' => true,\n        '\\u{037F}'...'\\u{1FFF}' => true,\n        '\\u{2070}'...'\\u{218F}' => true,\n        '\\u{2C00}'...'\\u{2FEF}' => true,\n        '\\u{3001}'...'\\u{D7FF}' => true,\n        '\\u{F900}'...'\\u{FDCF}' => true,\n        '\\u{FDF0}'...'\\u{FFFD}' => true,\n        else => codepoint >= '\\u{10000}',\n    };\n}\n\nfn isIdentStart(self: *Tokenizer) bool {\n    if (self.isEof()) return false;\n\n    var b = self.nextByteUnchecked();\n    if (b == '-') {\n        b = if (self.hasAtLeast(1)) self.byteAt(1) else return false;\n    }\n\n    return switch (b) {\n        'a'...'z', 'A'...'Z', '_', 0x0 => true,\n        '\\\\' => !self.hasNewlineAt(1),\n        else => b > 0x7F, // not is ascii\n    };\n}\n\nfn consumeChar(self: *Tokenizer) void {\n    const byte = self.nextByteUnchecked();\n    const len_utf8 = std.unicode.utf8ByteSequenceLength(byte) catch 1;\n    self.position += len_utf8;\n}\n\n// Given that a newline has been seen, advance over the newline\n// and update the state.\nfn consumeNewline(self: *Tokenizer) void {\n    const byte = self.nextByteUnchecked();\n    assert(byte == '\\r' or byte == '\\n' or byte == '\\x0C');\n\n    self.position += 1;\n    if (byte == '\\r' and self.nextByte() == '\\n') {\n        self.position += 1;\n    }\n}\n\nfn consumeWhiteSpace(self: *Tokenizer, newline: bool) Token {\n    const start_position = self.position;\n    if (newline) {\n        self.consumeNewline();\n    } else {\n        self.advance(1);\n    }\n    while (!self.isEof()) {\n        const b = self.nextByteUnchecked();\n        switch (b) {\n            ' ', '\\t' => {\n                self.advance(1);\n            },\n            '\\n', '\\x0C', '\\r' => {\n                self.consumeNewline();\n            },\n            else => break,\n        }\n    }\n    return .{ .white_space = self.sliceFrom(start_position) };\n}\n\nfn consumeComment(self: *Tokenizer) []const u8 {\n    self.advance(2); // consume \"/*\"\n    const start_position = self.position;\n    while (!self.isEof()) {\n        switch (self.nextByteUnchecked()) {\n            '*' => {\n                const end_position = self.position;\n                self.advance(1);\n                if (self.nextByte() == '/') {\n                    self.advance(1);\n                    return self.slice(start_position, end_position);\n                }\n            },\n            '\\n', '\\x0C', '\\r' => {\n                self.consumeNewline();\n            },\n            0x0 => self.advance(1),\n            else => self.consumeChar(),\n        }\n    }\n    return self.sliceFrom(start_position);\n}\n\nfn byteToHexDigit(b: u8) ?u32 {\n    return switch (b) {\n        '0'...'9' => b - '0',\n        'a'...'f' => b - 'a' + 10,\n        'A'...'F' => b - 'A' + 10,\n        else => null,\n    };\n}\n\nfn byteToDecimalDigit(b: u8) ?u32 {\n    return if (std.ascii.isDigit(b)) b - '0' else null;\n}\n\n// (value, number of digits up to 6)\nfn consumeHexDigits(self: *Tokenizer) void {\n    var value: u32 = 0;\n    var digits: u32 = 0;\n\n    while (digits < 6 and !self.isEof()) {\n        if (byteToHexDigit(self.nextByteUnchecked())) |digit| {\n            value = value * 16 + digit;\n            digits += 1;\n            self.advance(1);\n        } else {\n            break;\n        }\n    }\n\n    _ = &value;\n}\n\n// Assumes that the U+005C REVERSE SOLIDUS (\\) has already been consumed\n// and that the next input character has already been verified\n// to not be a newline.\nfn consumeEscape(self: *Tokenizer) void {\n    if (self.isEof())\n        return; // Escaped EOF\n\n    switch (self.nextByteUnchecked()) {\n        '0'...'9', 'A'...'F', 'a'...'f' => {\n            consumeHexDigits(self);\n\n            if (!self.isEof()) {\n                switch (self.nextByteUnchecked()) {\n                    ' ', '\\t' => {\n                        self.advance(1);\n                    },\n                    '\\n', '\\x0C', '\\r' => {\n                        self.consumeNewline();\n                    },\n                    else => {},\n                }\n            }\n        },\n        else => self.consumeChar(),\n    }\n}\n\n/// https://drafts.csswg.org/css-syntax/#consume-string-token\nfn consumeString(self: *Tokenizer, single_quote: bool) Token {\n    self.advance(1); // Skip the initial quote\n\n    // start_pos is at code point boundary, after \" or '\n    const start_pos = self.position;\n\n    while (!self.isEof()) {\n        switch (self.nextByteUnchecked()) {\n            '\"' => {\n                if (!single_quote) {\n                    const value = self.sliceFrom(start_pos);\n                    self.advance(1);\n                    return .{ .string = value };\n                }\n                self.advance(1);\n            },\n            '\\'' => {\n                if (single_quote) {\n                    const value = self.sliceFrom(start_pos);\n                    self.advance(1);\n                    return .{ .string = value };\n                }\n                self.advance(1);\n            },\n            '\\n', '\\r', '\\x0C' => {\n                return .{ .bad_string = self.sliceFrom(start_pos) };\n            },\n            '\\\\' => {\n                self.advance(1);\n                if (self.isEof())\n                    continue; // escaped EOF, do nothing.\n\n                switch (self.nextByteUnchecked()) {\n                    // Escaped newline\n                    '\\n', '\\x0C', '\\r' => self.consumeNewline(),\n\n                    // Spec calls for replacing escape sequences with characters,\n                    // but this would require allocating a new string.\n                    // Therefore, we leave it as is and let the parser handle the escaping.\n                    else => self.consumeEscape(),\n                }\n            },\n            else => self.consumeChar(),\n        }\n    }\n\n    return .{ .string = self.sliceFrom(start_pos) };\n}\n\nfn consumeName(self: *Tokenizer) []const u8 {\n    // start_pos is the end of the previous token, therefore at a code point boundary\n    const start_pos = self.position;\n\n    while (!self.isEof()) {\n        switch (self.nextByteUnchecked()) {\n            'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => self.advance(1),\n            '\\\\' => {\n                if (self.hasNewlineAt(1)) {\n                    break;\n                }\n\n                self.advance(1);\n                self.consumeEscape();\n            },\n            0x0 => self.advance(1),\n            '\\x80'...'\\xFF' => {\n                // Non-ASCII: advance over the complete UTF-8 code point in one step.\n                // Using consumeChar() instead of advance(1) ensures we never land on\n                // a continuation byte, which advance() asserts against.\n                self.consumeChar();\n            },\n            else => {\n                if (self.hasNonAsciiAt(0)) {\n                    self.consumeChar();\n                } else {\n                    break; // ASCII\n                }\n            },\n        }\n    }\n\n    return self.sliceFrom(start_pos);\n}\n\nfn consumeMark(self: *Tokenizer) Token {\n    const byte = self.nextByteUnchecked();\n    self.advance(1);\n    return switch (byte) {\n        ',' => .comma,\n        ':' => .colon,\n        ';' => .semicolon,\n        '(' => .parenthesis_block,\n        ')' => .close_parenthesis,\n        '{' => .curly_bracket_block,\n        '}' => .close_curly_bracket,\n        '[' => .square_bracket_block,\n        ']' => .close_square_bracket,\n        else => unreachable,\n    };\n}\n\nfn consumeNumeric(self: *Tokenizer) Token {\n    // Parse [+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?\n    // But this is always called so that there is at least one digit in \\d*(\\.\\d+)?\n\n    // Do all the math in f64 so that large numbers overflow to +/-inf\n    // and i32::{MIN, MAX} are within range.\n\n    var sign: f64 = 1.0;\n    var has_sign = false;\n    switch (self.nextByteUnchecked()) {\n        '+' => {\n            has_sign = true;\n        },\n        '-' => {\n            has_sign = true;\n            sign = -1.0;\n        },\n        else => {},\n    }\n    if (has_sign) {\n        self.advance(1);\n    }\n\n    var is_integer = true;\n    var integral_part: f64 = 0.0;\n    var fractional_part: f64 = 0.0;\n\n    while (!self.isEof()) {\n        if (byteToDecimalDigit(self.nextByteUnchecked())) |digit| {\n            integral_part = integral_part * 10.0 + @as(f64, @floatFromInt(digit));\n            self.advance(1);\n        } else {\n            break;\n        }\n    }\n\n    if (self.hasAtLeast(1) and self.nextByteUnchecked() == '.' and std.ascii.isDigit(self.byteAt(1))) {\n        is_integer = false;\n        self.advance(1); // Consume '.'\n\n        var factor: f64 = 0.1;\n        while (!self.isEof()) {\n            if (byteToDecimalDigit(self.nextByteUnchecked())) |digit| {\n                fractional_part += @as(f64, @floatFromInt(digit)) * factor;\n                factor *= 0.1;\n                self.advance(1);\n            } else {\n                break;\n            }\n        }\n    }\n\n    var value = sign * (integral_part + fractional_part);\n\n    blk: {\n        const e = self.nextByte() orelse break :blk;\n        if (e != 'e' and e != 'E') break :blk;\n\n        var mul: f64 = 1.0;\n\n        if (self.hasAtLeast(2) and (self.byteAt(1) == '+' or self.byteAt(1) == '-') and std.ascii.isDigit(self.byteAt(2))) {\n            mul = switch (self.byteAt(1)) {\n                '-' => -1.0,\n                '+' => 1.0,\n                else => unreachable,\n            };\n\n            self.advance(2);\n        } else if (self.hasAtLeast(2) and std.ascii.isDigit(self.byteAt(2))) {\n            self.advance(1);\n        } else {\n            break :blk;\n        }\n\n        is_integer = false;\n\n        var exponent: f64 = 0.0;\n        while (!self.isEof()) {\n            if (byteToDecimalDigit(self.nextByteUnchecked())) |digit| {\n                exponent = exponent * 10.0 + @as(f64, @floatFromInt(digit));\n                self.advance(1);\n            } else {\n                break;\n            }\n        }\n        value *= std.math.pow(f64, 10.0, mul * exponent);\n    }\n\n    const int_value: ?i32 = if (is_integer) blk: {\n        if (value >= std.math.maxInt(i32)) {\n            break :blk std.math.maxInt(i32);\n        }\n\n        if (value <= std.math.minInt(i32)) {\n            break :blk std.math.minInt(i32);\n        }\n\n        break :blk @as(i32, @intFromFloat(value));\n    } else null;\n\n    if (!self.isEof() and self.nextByteUnchecked() == '%') {\n        self.advance(1);\n\n        return .{ .percentage = .{\n            .has_sign = has_sign,\n            .int_value = int_value,\n            .unit_value = @as(f32, @floatCast(value / 100.0)),\n        } };\n    }\n\n    if (isIdentStart(self)) {\n        return .{ .dimension = .{\n            .has_sign = has_sign,\n            .int_value = int_value,\n            .value = @as(f32, @floatCast(value)),\n            .unit = consumeName(self),\n        } };\n    }\n\n    return .{ .number = .{\n        .has_sign = has_sign,\n        .int_value = int_value,\n        .value = @as(f32, @floatCast(value)),\n    } };\n}\n\nfn consumeUnquotedUrl(self: *Tokenizer) ?Token {\n    // TODO: true url parser\n    if (self.nextByte()) |it| {\n        return self.consumeString(it == '\\'');\n    }\n\n    return null;\n}\n\nfn consumeIdentLike(self: *Tokenizer) Token {\n    const value = self.consumeName();\n\n    if (!self.isEof() and self.nextByteUnchecked() == '(') {\n        self.advance(1);\n\n        if (std.ascii.eqlIgnoreCase(value, \"url\")) {\n            if (self.consumeUnquotedUrl()) |result| {\n                return result;\n            }\n        }\n\n        return .{ .function = value };\n    }\n\n    return .{ .ident = value };\n}\n\npub fn next(self: *Tokenizer) ?Token {\n    if (self.isEof()) {\n        return null;\n    }\n\n    const b = self.nextByteUnchecked();\n    return switch (b) {\n        // Consume comments\n        '/' => {\n            if (self.startsWith(\"/*\")) {\n                return .{ .comment = self.consumeComment() };\n            } else {\n                self.advance(1);\n                return .{ .delim = '/' };\n            }\n        },\n\n        // Consume marks\n        '(', ')', '{', '}', '[', ']', ',', ':', ';' => {\n            return self.consumeMark();\n        },\n\n        // Consume as much whitespace as possible. Return a <whitespace-token>.\n        ' ', '\\t' => self.consumeWhiteSpace(false),\n        '\\n', '\\x0C', '\\r' => self.consumeWhiteSpace(true),\n\n        // Consume a string token and return it.\n        '\"' => self.consumeString(false),\n        '\\'' => self.consumeString(true),\n\n        '0'...'9' => self.consumeNumeric(),\n        'a'...'z', 'A'...'Z', '_', 0x0 => self.consumeIdentLike(),\n\n        '+' => {\n            if ((self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(1))) or\n                (self.hasAtLeast(2) and self.byteAt(1) == '.' and std.ascii.isDigit(self.byteAt(2))))\n            {\n                return self.consumeNumeric();\n            }\n            self.advance(1);\n            return .{ .delim = '+' };\n        },\n        '-' => {\n            if ((self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(1))) or\n                (self.hasAtLeast(2) and self.byteAt(1) == '.' and std.ascii.isDigit(self.byteAt(2))))\n            {\n                return self.consumeNumeric();\n            }\n\n            if (self.startsWith(\"-->\")) {\n                self.advance(3);\n                return .cdc;\n            }\n\n            if (isIdentStart(self)) {\n                return self.consumeIdentLike();\n            }\n\n            self.advance(1);\n            return .{ .delim = '-' };\n        },\n        '.' => {\n            if (self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(1))) {\n                return self.consumeNumeric();\n            }\n            self.advance(1);\n            return .{ .delim = '.' };\n        },\n\n        // Consume hash token\n        '#' => {\n            self.advance(1);\n            if (self.isIdentStart()) {\n                return .{ .id_hash = self.consumeName() };\n            }\n            if (self.nextByte()) |it| {\n                switch (it) {\n                    // Any other valid case here already resulted in IDHash.\n                    '0'...'9', '-' => return .{ .unrestricted_hash = self.consumeName() },\n                    else => {},\n                }\n            }\n            return .{ .delim = '#' };\n        },\n\n        // Consume at-rules\n        '@' => {\n            self.advance(1);\n            return if (isIdentStart(self))\n                .{ .at_keyword = consumeName(self) }\n            else\n                .{ .delim = '@' };\n        },\n\n        '<' => {\n            if (self.startsWith(\"<!--\")) {\n                self.advance(4);\n                return .cdo;\n            } else {\n                self.advance(1);\n                return .{ .delim = '<' };\n            }\n        },\n\n        '\\\\' => {\n            if (!self.hasNewlineAt(1)) {\n                return self.consumeIdentLike();\n            }\n\n            self.advance(1);\n            return .{ .delim = '\\\\' };\n        },\n\n        else => {\n            if (b > 0x7F) { // not is ascii\n                return self.consumeIdentLike();\n            }\n\n            self.advance(1);\n            return .{ .delim = b };\n        },\n    };\n}\n\nconst testing = std.testing;\n\nfn expectTokensEqual(input: []const u8, tokens: []const Token) !void {\n    var lexer = Tokenizer{ .input = input };\n\n    var i: usize = 0;\n    while (lexer.next()) |token| : (i += 1) {\n        assert(i < tokens.len);\n        try testing.expectEqualDeep(tokens[i], token);\n    }\n\n    try testing.expectEqual(i, tokens.len);\n    try testing.expectEqualDeep(null, lexer.next());\n}\n\ntest \"smoke\" {\n    try expectTokensEqual(\n        \\\\.lightpanda  {color:red;}\n    , &.{\n        .{ .delim = '.' },\n        .{ .ident = \"lightpanda\" },\n        .{ .white_space = \"  \" },\n        .curly_bracket_block,\n        .{ .ident = \"color\" },\n        .colon,\n        .{ .ident = \"red\" },\n        .semicolon,\n        .close_curly_bracket,\n    });\n}\n"
  },
  {
    "path": "src/browser/dump.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Page = @import(\"Page.zig\");\nconst Node = @import(\"webapi/Node.zig\");\nconst Slot = @import(\"webapi/element/html/Slot.zig\");\nconst IFrame = @import(\"webapi/element/html/IFrame.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub const Opts = struct {\n    with_base: bool = false,\n    with_frames: bool = false,\n    strip: Opts.Strip = .{},\n    shadow: Opts.Shadow = .rendered,\n\n    pub const Strip = struct {\n        js: bool = false,\n        ui: bool = false,\n        css: bool = false,\n    };\n\n    pub const Shadow = enum {\n        // Skip shadow DOM entirely (innerHTML/outerHTML)\n        skip,\n\n        // Dump everyhting (like \"view source\")\n        complete,\n\n        // Resolve slot elements (like what actually gets rendered)\n        rendered,\n    };\n};\n\npub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {\n    if (doc.is(Node.Document.HTMLDocument)) |html_doc| {\n        blk: {\n            // Ideally we just render the doctype which is part of the document\n            if (doc.asNode().firstChild()) |first| {\n                if (first._type == .document_type) {\n                    break :blk;\n                }\n            }\n            // But if the doc has no child, or the first child isn't a doctype\n            // well force it.\n            try writer.writeAll(\"<!DOCTYPE html>\");\n        }\n\n        if (opts.with_base) {\n            const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();\n            const base = try doc.createElement(\"base\", null, page);\n            try base.setAttributeSafe(comptime .wrap(\"base\"), .wrap(page.base()), page);\n            _ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);\n        }\n    }\n\n    return deep(doc.asNode(), opts, writer, page);\n}\n\npub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {\n    return _deep(node, opts, false, writer, page);\n}\n\nfn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {\n    switch (node._type) {\n        .cdata => |cd| {\n            if (node.is(Node.CData.Comment)) |_| {\n                try writer.writeAll(\"<!--\");\n                try writer.writeAll(cd.getData().str());\n                try writer.writeAll(\"-->\");\n            } else if (node.is(Node.CData.ProcessingInstruction)) |pi| {\n                try writer.writeAll(\"<?\");\n                try writer.writeAll(pi._target);\n                try writer.writeAll(\" \");\n                try writer.writeAll(cd.getData().str());\n                try writer.writeAll(\"?>\");\n            } else {\n                if (shouldEscapeText(node._parent)) {\n                    try writeEscapedText(cd.getData().str(), writer);\n                } else {\n                    try writer.writeAll(cd.getData().str());\n                }\n            }\n        },\n        .element => |el| {\n            if (shouldStripElement(el, opts)) {\n                return;\n            }\n\n            // When opts.shadow == .rendered, we normally skip any element with\n            // a slot attribute. Only the \"active\" element will get rendered into\n            // the <slot name=\"X\">. However, the `deep` function is itself used\n            // to render that \"active\" content, so when we're trying to render\n            // it, we don't want to skip it.\n            if ((comptime force_slot == false) and opts.shadow == .rendered) {\n                if (el.getAttributeSafe(comptime .wrap(\"slot\"))) |_| {\n                    // Skip - will be rendered by the Slot if it's the active container\n                    return;\n                }\n            }\n\n            try el.format(writer);\n\n            if (opts.shadow == .rendered) {\n                if (el.is(Slot)) |slot| {\n                    try dumpSlotContent(slot, opts, writer, page);\n                    return writer.writeAll(\"</slot>\");\n                }\n            }\n            if (opts.shadow != .skip) {\n                if (page._element_shadow_roots.get(el)) |shadow| {\n                    try children(shadow.asNode(), opts, writer, page);\n                    // In rendered mode, light DOM is only shown through slots, not directly\n                    if (opts.shadow == .rendered) {\n                        // Skip rendering light DOM children\n                        if (!isVoidElement(el)) {\n                            try writer.writeAll(\"</\");\n                            try writer.writeAll(el.getTagNameDump());\n                            try writer.writeByte('>');\n                        }\n                        return;\n                    }\n                }\n            }\n\n            if (opts.with_frames and el.is(IFrame) != null) {\n                const frame = el.as(IFrame);\n                if (frame.getContentDocument()) |doc| {\n                    // A frame's document should always ahave a page, but\n                    // I'm not willing to crash a release build on that assertion.\n                    if (comptime IS_DEBUG) {\n                        std.debug.assert(doc._page != null);\n                    }\n                    if (doc._page) |frame_page| {\n                        try writer.writeByte('\\n');\n                        root(doc, opts, writer, frame_page) catch return error.WriteFailed;\n                        try writer.writeByte('\\n');\n                    }\n                }\n            } else {\n                try children(node, opts, writer, page);\n            }\n\n            if (!isVoidElement(el)) {\n                try writer.writeAll(\"</\");\n                try writer.writeAll(el.getTagNameDump());\n                try writer.writeByte('>');\n            }\n        },\n        .document => try children(node, opts, writer, page),\n        .document_type => |dt| {\n            try writer.writeAll(\"<!DOCTYPE \");\n            try writer.writeAll(dt.getName());\n\n            const public_id = dt.getPublicId();\n            const system_id = dt.getSystemId();\n            if (public_id.len != 0 and system_id.len != 0) {\n                try writer.writeAll(\" PUBLIC \\\"\");\n                try writeEscapedText(public_id, writer);\n                try writer.writeAll(\"\\\" \\\"\");\n                try writeEscapedText(system_id, writer);\n                try writer.writeByte('\"');\n            } else if (public_id.len != 0) {\n                try writer.writeAll(\" PUBLIC \\\"\");\n                try writeEscapedText(public_id, writer);\n                try writer.writeByte('\"');\n            } else if (system_id.len != 0) {\n                try writer.writeAll(\" SYSTEM \\\"\");\n                try writeEscapedText(system_id, writer);\n                try writer.writeByte('\"');\n            }\n            try writer.writeAll(\">\\n\");\n        },\n        .document_fragment => try children(node, opts, writer, page),\n        .attribute => {\n            // Not called normally, but can be called via XMLSerializer.serializeToString\n            // in which case it should return an empty string\n            try writer.writeAll(\"\");\n        },\n    }\n}\n\npub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        try deep(child, opts, writer, page);\n    }\n}\n\npub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {\n    try writer.beginObject();\n\n    try writer.objectField(\"type\");\n    switch (node.type) {\n        .cdata => {\n            try writer.write(\"cdata\");\n        },\n        .document => {\n            try writer.write(\"document\");\n        },\n        .document_type => {\n            try writer.write(\"document_type\");\n        },\n        .element => |*el| {\n            try writer.write(\"element\");\n            try writer.objectField(\"tag\");\n            try writer.write(el.tagName());\n\n            try writer.objectField(\"attributes\");\n            try writer.beginObject();\n            var it = el.attributeIterator();\n            while (it.next()) |attr| {\n                try writer.objectField(attr.name);\n                try writer.write(attr.value);\n            }\n            try writer.endObject();\n        },\n    }\n\n    try writer.objectField(\"children\");\n    try writer.beginArray();\n    var it = node.childrenIterator();\n    while (it.next()) |child| {\n        try toJSON(child, writer);\n    }\n    try writer.endArray();\n    try writer.endObject();\n}\n\nfn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {\n    const assigned = slot.assignedNodes(null, page) catch return;\n\n    if (assigned.len > 0) {\n        for (assigned) |assigned_node| {\n            try _deep(assigned_node, opts, true, writer, page);\n        }\n    } else {\n        try children(slot.asNode(), opts, writer, page);\n    }\n}\n\nfn isVoidElement(el: *const Node.Element) bool {\n    return switch (el._type) {\n        .html => |html| switch (html._type) {\n            .br, .hr, .img, .input, .link, .meta => true,\n            else => false,\n        },\n        .svg => false,\n    };\n}\n\nfn shouldStripElement(el: *const Node.Element, opts: Opts) bool {\n    const tag_name = el.getTagNameDump();\n\n    if (opts.strip.js) {\n        if (std.mem.eql(u8, tag_name, \"script\")) return true;\n        if (std.mem.eql(u8, tag_name, \"noscript\")) return true;\n\n        if (std.mem.eql(u8, tag_name, \"link\")) {\n            if (el.getAttributeSafe(comptime .wrap(\"as\"))) |as| {\n                if (std.mem.eql(u8, as, \"script\")) return true;\n            }\n            if (el.getAttributeSafe(comptime .wrap(\"rel\"))) |rel| {\n                if (std.mem.eql(u8, rel, \"modulepreload\") or std.mem.eql(u8, rel, \"preload\")) {\n                    if (el.getAttributeSafe(comptime .wrap(\"as\"))) |as| {\n                        if (std.mem.eql(u8, as, \"script\")) return true;\n                    }\n                }\n            }\n        }\n    }\n\n    if (opts.strip.css or opts.strip.ui) {\n        if (std.mem.eql(u8, tag_name, \"style\")) return true;\n\n        if (std.mem.eql(u8, tag_name, \"link\")) {\n            if (el.getAttributeSafe(comptime .wrap(\"rel\"))) |rel| {\n                if (std.mem.eql(u8, rel, \"stylesheet\")) return true;\n            }\n        }\n    }\n\n    if (opts.strip.ui) {\n        if (std.mem.eql(u8, tag_name, \"img\")) return true;\n        if (std.mem.eql(u8, tag_name, \"picture\")) return true;\n        if (std.mem.eql(u8, tag_name, \"video\")) return true;\n        if (std.mem.eql(u8, tag_name, \"audio\")) return true;\n        if (std.mem.eql(u8, tag_name, \"svg\")) return true;\n        if (std.mem.eql(u8, tag_name, \"canvas\")) return true;\n        if (std.mem.eql(u8, tag_name, \"iframe\")) return true;\n    }\n\n    return false;\n}\n\nfn shouldEscapeText(node_: ?*Node) bool {\n    const node = node_ orelse return true;\n    if (node.is(Node.Element.Html.Script) != null) {\n        return false;\n    }\n    // When scripting is enabled, <noscript> is a raw text element per the HTML spec\n    // (https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments).\n    // Its text content must not be HTML-escaped during serialization.\n    if (node.is(Node.Element.Html.Generic)) |generic| {\n        if (generic._tag == .noscript) return false;\n    }\n    return true;\n}\nfn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {\n    // Fast path: if no special characters, write directly\n    const first_special = std.mem.indexOfAnyPos(u8, text, 0, &.{ '&', '<', '>', 194 }) orelse {\n        return writer.writeAll(text);\n    };\n\n    try writer.writeAll(text[0..first_special]);\n    var remaining = try writeEscapedByte(text, first_special, writer);\n\n    while (std.mem.indexOfAnyPos(u8, remaining, 0, &.{ '&', '<', '>', 194 })) |offset| {\n        try writer.writeAll(remaining[0..offset]);\n        remaining = try writeEscapedByte(remaining, offset, writer);\n    }\n\n    if (remaining.len > 0) {\n        try writer.writeAll(remaining);\n    }\n}\n\nfn writeEscapedByte(input: []const u8, index: usize, writer: *std.Io.Writer) ![]const u8 {\n    switch (input[index]) {\n        '&' => try writer.writeAll(\"&amp;\"),\n        '<' => try writer.writeAll(\"&lt;\"),\n        '>' => try writer.writeAll(\"&gt;\"),\n        194 => {\n            // non breaking space\n            if (input.len > index + 1 and input[index + 1] == 160) {\n                try writer.writeAll(\"&nbsp;\");\n                return input[index + 2 ..];\n            }\n            try writer.writeByte(194);\n        },\n        else => unreachable,\n    }\n    return input[index + 1 ..];\n}\n"
  },
  {
    "path": "src/browser/interactive.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Page = @import(\"Page.zig\");\nconst URL = @import(\"URL.zig\");\nconst TreeWalker = @import(\"webapi/TreeWalker.zig\");\nconst Element = @import(\"webapi/Element.zig\");\nconst Node = @import(\"webapi/Node.zig\");\nconst EventTarget = @import(\"webapi/EventTarget.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub const InteractivityType = enum {\n    native,\n    aria,\n    contenteditable,\n    listener,\n    focusable,\n};\n\npub const InteractiveElement = struct {\n    node: *Node,\n    tag_name: []const u8,\n    role: ?[]const u8,\n    name: ?[]const u8,\n    interactivity_type: InteractivityType,\n    listener_types: []const []const u8,\n    disabled: bool,\n    tab_index: i32,\n    id: ?[]const u8,\n    class: ?[]const u8,\n    href: ?[]const u8,\n    input_type: ?[]const u8,\n    value: ?[]const u8,\n    element_name: ?[]const u8,\n    placeholder: ?[]const u8,\n\n    pub fn jsonStringify(self: *const InteractiveElement, jw: anytype) !void {\n        try jw.beginObject();\n\n        try jw.objectField(\"tagName\");\n        try jw.write(self.tag_name);\n\n        try jw.objectField(\"role\");\n        try jw.write(self.role);\n\n        try jw.objectField(\"name\");\n        try jw.write(self.name);\n\n        try jw.objectField(\"type\");\n        try jw.write(@tagName(self.interactivity_type));\n\n        if (self.listener_types.len > 0) {\n            try jw.objectField(\"listeners\");\n            try jw.beginArray();\n            for (self.listener_types) |lt| {\n                try jw.write(lt);\n            }\n            try jw.endArray();\n        }\n\n        if (self.disabled) {\n            try jw.objectField(\"disabled\");\n            try jw.write(true);\n        }\n\n        try jw.objectField(\"tabIndex\");\n        try jw.write(self.tab_index);\n\n        if (self.id) |v| {\n            try jw.objectField(\"id\");\n            try jw.write(v);\n        }\n\n        if (self.class) |v| {\n            try jw.objectField(\"class\");\n            try jw.write(v);\n        }\n\n        if (self.href) |v| {\n            try jw.objectField(\"href\");\n            try jw.write(v);\n        }\n\n        if (self.input_type) |v| {\n            try jw.objectField(\"inputType\");\n            try jw.write(v);\n        }\n\n        if (self.value) |v| {\n            try jw.objectField(\"value\");\n            try jw.write(v);\n        }\n\n        if (self.element_name) |v| {\n            try jw.objectField(\"elementName\");\n            try jw.write(v);\n        }\n\n        if (self.placeholder) |v| {\n            try jw.objectField(\"placeholder\");\n            try jw.write(v);\n        }\n\n        try jw.endObject();\n    }\n};\n\n/// Collect all interactive elements under `root`.\npub fn collectInteractiveElements(\n    root: *Node,\n    arena: Allocator,\n    page: *Page,\n) ![]InteractiveElement {\n    // Pre-build a map of event_target pointer → event type names,\n    // so classify and getListenerTypes are both O(1) per element.\n    const listener_targets = try buildListenerTargetMap(page, arena);\n\n    var results: std.ArrayList(InteractiveElement) = .empty;\n\n    var tw = TreeWalker.Full.init(root, .{});\n    while (tw.next()) |node| {\n        const el = node.is(Element) orelse continue;\n        const html_el = el.is(Element.Html) orelse continue;\n\n        // Skip non-visual elements that are never user-interactive.\n        switch (el.getTag()) {\n            .script, .style, .link, .meta, .head, .noscript, .template => continue,\n            else => {},\n        }\n\n        const itype = classifyInteractivity(el, html_el, listener_targets) orelse continue;\n\n        const listener_types = getListenerTypes(\n            el.asEventTarget(),\n            listener_targets,\n        );\n\n        try results.append(arena, .{\n            .node = node,\n            .tag_name = el.getTagNameLower(),\n            .role = getRole(el),\n            .name = try getAccessibleName(el, arena),\n            .interactivity_type = itype,\n            .listener_types = listener_types,\n            .disabled = isDisabled(el),\n            .tab_index = html_el.getTabIndex(),\n            .id = el.getAttributeSafe(comptime .wrap(\"id\")),\n            .class = el.getAttributeSafe(comptime .wrap(\"class\")),\n            .href = if (el.getAttributeSafe(comptime .wrap(\"href\"))) |href|\n                URL.resolve(arena, page.base(), href, .{ .encode = true }) catch href\n            else\n                null,\n            .input_type = getInputType(el),\n            .value = getInputValue(el),\n            .element_name = el.getAttributeSafe(comptime .wrap(\"name\")),\n            .placeholder = el.getAttributeSafe(comptime .wrap(\"placeholder\")),\n        });\n    }\n\n    return results.items;\n}\n\npub const ListenerTargetMap = std.AutoHashMapUnmanaged(usize, std.ArrayList([]const u8));\n\n/// Pre-build a map from event_target pointer → list of event type names.\n/// This lets both classifyInteractivity (O(1) \"has any?\") and\n/// getListenerTypes (O(1) \"which ones?\") avoid re-iterating per element.\npub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap {\n    var map = ListenerTargetMap{};\n\n    // addEventListener registrations\n    var it = page._event_manager.lookup.iterator();\n    while (it.next()) |entry| {\n        const list = entry.value_ptr.*;\n        if (list.first != null) {\n            const gop = try map.getOrPut(arena, entry.key_ptr.event_target);\n            if (!gop.found_existing) gop.value_ptr.* = .empty;\n            try gop.value_ptr.append(arena, entry.key_ptr.type_string.str());\n        }\n    }\n\n    // Inline handlers (onclick, onmousedown, etc.)\n    var attr_it = page._event_target_attr_listeners.iterator();\n    while (attr_it.next()) |entry| {\n        const gop = try map.getOrPut(arena, @intFromPtr(entry.key_ptr.target));\n        if (!gop.found_existing) gop.value_ptr.* = .empty;\n        // Strip \"on\" prefix to get the event type name.\n        try gop.value_ptr.append(arena, @tagName(entry.key_ptr.handler)[2..]);\n    }\n\n    return map;\n}\n\npub fn classifyInteractivity(\n    el: *Element,\n    html_el: *Element.Html,\n    listener_targets: ListenerTargetMap,\n) ?InteractivityType {\n    // 1. Native interactive by tag\n    switch (el.getTag()) {\n        .button, .summary, .details, .select, .textarea => return .native,\n        .anchor, .area => {\n            if (el.getAttributeSafe(comptime .wrap(\"href\")) != null) return .native;\n        },\n        .input => {\n            if (el.is(Element.Html.Input)) |input| {\n                if (input._input_type != .hidden) return .native;\n            }\n        },\n        else => {},\n    }\n\n    // 2. ARIA interactive role\n    if (el.getAttributeSafe(comptime .wrap(\"role\"))) |role| {\n        if (isInteractiveRole(role)) return .aria;\n    }\n\n    // 3. contenteditable (15 bytes, exceeds SSO limit for comptime)\n    if (el.getAttributeSafe(.wrap(\"contenteditable\"))) |ce| {\n        if (ce.len == 0 or std.ascii.eqlIgnoreCase(ce, \"true\")) return .contenteditable;\n    }\n\n    // 4. Event listeners (addEventListener or inline handlers)\n    const et_ptr = @intFromPtr(html_el.asEventTarget());\n    if (listener_targets.get(et_ptr) != null) return .listener;\n\n    // 5. Explicitly focusable via tabindex.\n    // Only count elements with an EXPLICIT tabindex attribute,\n    // since getTabIndex() returns 0 for all interactive tags by default\n    // (including anchors without href and hidden inputs).\n    if (el.getAttributeSafe(comptime .wrap(\"tabindex\"))) |_| {\n        if (html_el.getTabIndex() >= 0) return .focusable;\n    }\n\n    return null;\n}\n\npub fn isInteractiveRole(role: []const u8) bool {\n    const MAX_LEN = \"menuitemcheckbox\".len;\n    if (role.len > MAX_LEN) return false;\n    var buf: [MAX_LEN]u8 = undefined;\n    const lowered = std.ascii.lowerString(&buf, role);\n    const interactive_roles = std.StaticStringMap(void).initComptime(.{\n        .{ \"button\", {} },\n        .{ \"checkbox\", {} },\n        .{ \"combobox\", {} },\n        .{ \"iframe\", {} },\n        .{ \"link\", {} },\n        .{ \"listbox\", {} },\n        .{ \"menuitem\", {} },\n        .{ \"menuitemcheckbox\", {} },\n        .{ \"menuitemradio\", {} },\n        .{ \"option\", {} },\n        .{ \"radio\", {} },\n        .{ \"searchbox\", {} },\n        .{ \"slider\", {} },\n        .{ \"spinbutton\", {} },\n        .{ \"switch\", {} },\n        .{ \"tab\", {} },\n        .{ \"textbox\", {} },\n        .{ \"treeitem\", {} },\n    });\n    return interactive_roles.has(lowered);\n}\n\npub fn isContentRole(role: []const u8) bool {\n    const MAX_LEN = \"columnheader\".len;\n    if (role.len > MAX_LEN) return false;\n    var buf: [MAX_LEN]u8 = undefined;\n    const lowered = std.ascii.lowerString(&buf, role);\n    const content_roles = std.StaticStringMap(void).initComptime(.{\n        .{ \"article\", {} },\n        .{ \"cell\", {} },\n        .{ \"columnheader\", {} },\n        .{ \"gridcell\", {} },\n        .{ \"heading\", {} },\n        .{ \"listitem\", {} },\n        .{ \"main\", {} },\n        .{ \"navigation\", {} },\n        .{ \"region\", {} },\n        .{ \"rowheader\", {} },\n    });\n    return content_roles.has(lowered);\n}\n\nfn getRole(el: *Element) ?[]const u8 {\n    // Explicit role attribute takes precedence\n    if (el.getAttributeSafe(comptime .wrap(\"role\"))) |role| return role;\n\n    // Implicit role from tag\n    return switch (el.getTag()) {\n        .button, .summary => \"button\",\n        .anchor, .area => if (el.getAttributeSafe(comptime .wrap(\"href\")) != null) \"link\" else null,\n        .input => blk: {\n            if (el.is(Element.Html.Input)) |input| {\n                break :blk switch (input._input_type) {\n                    .text, .tel, .url, .email => \"textbox\",\n                    .checkbox => \"checkbox\",\n                    .radio => \"radio\",\n                    .button, .submit, .reset, .image => \"button\",\n                    .range => \"slider\",\n                    .number => \"spinbutton\",\n                    .search => \"searchbox\",\n                    else => null,\n                };\n            }\n            break :blk null;\n        },\n        .select => \"combobox\",\n        .textarea => \"textbox\",\n        .details => \"group\",\n        else => null,\n    };\n}\n\nfn getAccessibleName(el: *Element, arena: Allocator) !?[]const u8 {\n    // aria-label\n    if (el.getAttributeSafe(comptime .wrap(\"aria-label\"))) |v| {\n        if (v.len > 0) return v;\n    }\n\n    // alt (for img, input[type=image])\n    if (el.getAttributeSafe(comptime .wrap(\"alt\"))) |v| {\n        if (v.len > 0) return v;\n    }\n\n    // title\n    if (el.getAttributeSafe(comptime .wrap(\"title\"))) |v| {\n        if (v.len > 0) return v;\n    }\n\n    // placeholder\n    if (el.getAttributeSafe(comptime .wrap(\"placeholder\"))) |v| {\n        if (v.len > 0) return v;\n    }\n\n    // value (for buttons)\n    if (el.getTag() == .input) {\n        if (el.getAttributeSafe(comptime .wrap(\"value\"))) |v| {\n            if (v.len > 0) return v;\n        }\n    }\n\n    // Text content (first non-empty text node, trimmed)\n    return try getTextContent(el.asNode(), arena);\n}\n\nfn getTextContent(node: *Node, arena: Allocator) !?[]const u8 {\n    var tw: TreeWalker.FullExcludeSelf = .init(node, .{});\n\n    var arr: std.ArrayList(u8) = .empty;\n    var single_chunk: ?[]const u8 = null;\n\n    while (tw.next()) |child| {\n        // Skip text inside script/style elements.\n        if (child.is(Element)) |el| {\n            switch (el.getTag()) {\n                .script, .style => {\n                    tw.skipChildren();\n                    continue;\n                },\n                else => {},\n            }\n        }\n        if (child.is(Node.CData)) |cdata| {\n            if (cdata.is(Node.CData.Text)) |text| {\n                const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace);\n                if (content.len > 0) {\n                    if (single_chunk == null and arr.items.len == 0) {\n                        single_chunk = content;\n                    } else {\n                        if (single_chunk) |sc| {\n                            try arr.appendSlice(arena, sc);\n                            try arr.append(arena, ' ');\n                            single_chunk = null;\n                        }\n                        try arr.appendSlice(arena, content);\n                        try arr.append(arena, ' ');\n                    }\n                }\n            }\n        }\n    }\n\n    if (single_chunk) |sc| return sc;\n    if (arr.items.len == 0) return null;\n\n    // strip out trailing space\n    return arr.items[0 .. arr.items.len - 1];\n}\nfn isDisabled(el: *Element) bool {\n    if (el.getAttributeSafe(comptime .wrap(\"disabled\")) != null) return true;\n    return isDisabledByFieldset(el);\n}\n\n/// Check if an element is disabled by an ancestor <fieldset disabled>.\n/// Per spec, elements inside the first <legend> child of a disabled fieldset\n/// are NOT disabled by that fieldset.\nfn isDisabledByFieldset(el: *Element) bool {\n    const element_node = el.asNode();\n    var current: ?*Node = element_node._parent;\n    while (current) |node| {\n        current = node._parent;\n        const ancestor = node.is(Element) orelse continue;\n\n        if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap(\"disabled\")) != null) {\n            // Check if element is inside the first <legend> child of this fieldset\n            var child = ancestor.firstElementChild();\n            while (child) |c| {\n                if (c.getTag() == .legend) {\n                    if (c.asNode().contains(element_node)) return false;\n                    break;\n                }\n                child = c.nextElementSibling();\n            }\n            return true;\n        }\n    }\n    return false;\n}\n\nfn getInputType(el: *Element) ?[]const u8 {\n    if (el.is(Element.Html.Input)) |input| {\n        return input._input_type.toString();\n    }\n    return null;\n}\n\nfn getInputValue(el: *Element) ?[]const u8 {\n    if (el.is(Element.Html.Input)) |input| {\n        return input.getValue();\n    }\n    return null;\n}\n\n/// Get all event listener types registered on this target.\nfn getListenerTypes(target: *EventTarget, listener_targets: ListenerTargetMap) []const []const u8 {\n    if (listener_targets.get(@intFromPtr(target))) |types| return types.items;\n    return &.{};\n}\n\nconst testing = @import(\"../testing.zig\");\n\nfn testInteractive(html: []const u8) ![]InteractiveElement {\n    const page = try testing.test_session.createPage();\n    defer testing.test_session.removePage();\n\n    const doc = page.window._document;\n    const div = try doc.createElement(\"div\", null, page);\n    try page.parseHtmlAsChildren(div.asNode(), html);\n\n    return collectInteractiveElements(div.asNode(), page.call_arena, page);\n}\n\ntest \"browser.interactive: button\" {\n    const elements = try testInteractive(\"<button>Click me</button>\");\n    try testing.expectEqual(1, elements.len);\n    try testing.expectEqual(\"button\", elements[0].tag_name);\n    try testing.expectEqual(\"button\", elements[0].role.?);\n    try testing.expectEqual(\"Click me\", elements[0].name.?);\n    try testing.expectEqual(InteractivityType.native, elements[0].interactivity_type);\n}\n\ntest \"browser.interactive: anchor with href\" {\n    const elements = try testInteractive(\"<a href=\\\"/page\\\">Link</a>\");\n    try testing.expectEqual(1, elements.len);\n    try testing.expectEqual(\"a\", elements[0].tag_name);\n    try testing.expectEqual(\"link\", elements[0].role.?);\n    try testing.expectEqual(\"Link\", elements[0].name.?);\n}\n\ntest \"browser.interactive: anchor without href\" {\n    const elements = try testInteractive(\"<a>Not a link</a>\");\n    try testing.expectEqual(0, elements.len);\n}\n\ntest \"browser.interactive: input types\" {\n    const elements = try testInteractive(\n        \\\\<input type=\"text\" placeholder=\"Search\">\n        \\\\<input type=\"hidden\" name=\"csrf\">\n    );\n    try testing.expectEqual(1, elements.len);\n    try testing.expectEqual(\"input\", elements[0].tag_name);\n    try testing.expectEqual(\"text\", elements[0].input_type.?);\n    try testing.expectEqual(\"Search\", elements[0].placeholder.?);\n}\n\ntest \"browser.interactive: select and textarea\" {\n    const elements = try testInteractive(\n        \\\\<select name=\"color\"><option>Red</option></select>\n        \\\\<textarea name=\"msg\"></textarea>\n    );\n    try testing.expectEqual(2, elements.len);\n    try testing.expectEqual(\"select\", elements[0].tag_name);\n    try testing.expectEqual(\"textarea\", elements[1].tag_name);\n}\n\ntest \"browser.interactive: aria role\" {\n    const elements = try testInteractive(\"<div role=\\\"button\\\">Custom</div>\");\n    try testing.expectEqual(1, elements.len);\n    try testing.expectEqual(\"div\", elements[0].tag_name);\n    try testing.expectEqual(\"button\", elements[0].role.?);\n    try testing.expectEqual(InteractivityType.aria, elements[0].interactivity_type);\n}\n\ntest \"browser.interactive: contenteditable\" {\n    const elements = try testInteractive(\"<div contenteditable=\\\"true\\\">Edit me</div>\");\n    try testing.expectEqual(1, elements.len);\n    try testing.expectEqual(InteractivityType.contenteditable, elements[0].interactivity_type);\n}\n\ntest \"browser.interactive: tabindex\" {\n    const elements = try testInteractive(\"<div tabindex=\\\"0\\\">Focusable</div>\");\n    try testing.expectEqual(1, elements.len);\n    try testing.expectEqual(InteractivityType.focusable, elements[0].interactivity_type);\n    try testing.expectEqual(@as(i32, 0), elements[0].tab_index);\n}\n\ntest \"browser.interactive: disabled\" {\n    const elements = try testInteractive(\"<button disabled>Off</button>\");\n    try testing.expectEqual(1, elements.len);\n    try testing.expect(elements[0].disabled);\n}\n\ntest \"browser.interactive: disabled by fieldset\" {\n    const elements = try testInteractive(\n        \\\\<fieldset disabled>\n        \\\\  <button>Disabled</button>\n        \\\\  <legend><button>In legend</button></legend>\n        \\\\</fieldset>\n    );\n    try testing.expectEqual(2, elements.len);\n    // Button outside legend is disabled by fieldset\n    try testing.expect(elements[0].disabled);\n    // Button inside first legend is NOT disabled\n    try testing.expect(!elements[1].disabled);\n}\n\ntest \"browser.interactive: non-interactive div\" {\n    const elements = try testInteractive(\"<div>Just text</div>\");\n    try testing.expectEqual(0, elements.len);\n}\n\ntest \"browser.interactive: details and summary\" {\n    const elements = try testInteractive(\"<details><summary>More</summary><p>Content</p></details>\");\n    try testing.expectEqual(2, elements.len);\n    try testing.expectEqual(\"details\", elements[0].tag_name);\n    try testing.expectEqual(\"summary\", elements[1].tag_name);\n}\n\ntest \"browser.interactive: mixed elements\" {\n    const elements = try testInteractive(\n        \\\\<div>\n        \\\\  <a href=\"/home\">Home</a>\n        \\\\  <p>Some text</p>\n        \\\\  <button id=\"btn1\">Submit</button>\n        \\\\  <input type=\"email\" placeholder=\"Email\">\n        \\\\  <div>Not interactive</div>\n        \\\\  <div role=\"tab\">Tab</div>\n        \\\\</div>\n    );\n    try testing.expectEqual(4, elements.len);\n}\n"
  },
  {
    "path": "src/browser/js/Array.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst Array = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.Array,\n\npub fn len(self: Array) usize {\n    return v8.v8__Array__Length(self.handle);\n}\n\npub fn get(self: Array, index: u32) !js.Value {\n    const ctx = self.local.ctx;\n\n    const idx = js.Integer.init(ctx.isolate.handle, index);\n    const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {\n        return error.JsException;\n    };\n\n    return .{\n        .local = self.local,\n        .handle = handle,\n    };\n}\n\npub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {\n    const js_value = try self.local.zigValueToJs(value, opts);\n\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);\n    return out.has_value;\n}\n\npub fn toObject(self: Array) js.Object {\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn toValue(self: Array) js.Value {\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n"
  },
  {
    "path": "src/browser/js/BigInt.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst BigInt = @This();\n\nhandle: *const v8.Integer,\n\npub fn init(isolate: *v8.Isolate, val: anytype) BigInt {\n    const handle = switch (@TypeOf(val)) {\n        i8, i16, i32, i64, isize => v8.v8__BigInt__New(isolate, val).?,\n        u8, u16, u32, u64, usize => v8.v8__BigInt__NewFromUnsigned(isolate, val).?,\n        else => |T| @compileError(\"cannot create v8::BigInt from: \" ++ @typeName(T)),\n    };\n    return .{ .handle = handle };\n}\n\npub fn getInt64(self: BigInt) i64 {\n    return v8.v8__BigInt__Int64Value(self.handle, null);\n}\n\npub fn getUint64(self: BigInt) u64 {\n    return v8.v8__BigInt__Uint64Value(self.handle, null);\n}\n"
  },
  {
    "path": "src/browser/js/Caller.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../log.zig\");\nconst string = @import(\"../../string.zig\");\n\nconst Page = @import(\"../Page.zig\");\n\nconst js = @import(\"js.zig\");\nconst Local = @import(\"Local.zig\");\nconst Context = @import(\"Context.zig\");\nconst TaggedOpaque = @import(\"TaggedOpaque.zig\");\n\nconst v8 = js.v8;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst CALL_ARENA_RETAIN = 1024 * 16;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Caller = @This();\nlocal: Local,\nprev_local: ?*const js.Local,\nprev_context: *Context,\n\n// Takes the raw v8 isolate and extracts the context from it.\npub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {\n    const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate });\n    initWithContext(self, ctx, v8_context);\n}\n\nfn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {\n    ctx.call_depth += 1;\n    self.* = Caller{\n        .local = .{\n            .ctx = ctx,\n            .handle = v8_context,\n            .call_arena = ctx.call_arena,\n            .isolate = ctx.isolate,\n        },\n        .prev_local = ctx.local,\n        .prev_context = ctx.page.js,\n    };\n    ctx.page.js = ctx;\n    ctx.local = &self.local;\n}\n\npub fn initFromHandle(self: *Caller, handle: ?*const v8.FunctionCallbackInfo) void {\n    const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;\n    self.init(isolate);\n}\n\npub fn deinit(self: *Caller) void {\n    const ctx = self.local.ctx;\n    const call_depth = ctx.call_depth - 1;\n\n    // Because of callbacks, calls can be nested. Because of this, we\n    // can't clear the call_arena after _every_ call. Imagine we have\n    //    arr.forEach((i) => { console.log(i); }\n    //\n    // First we call forEach. Inside of our forEach call,\n    // we call console.log. If we reset the call_arena after this call,\n    // it'll reset it for the `forEach` call after, which might still\n    // need the data.\n    //\n    // Therefore, we keep a call_depth, and only reset the call_arena\n    // when a top-level (call_depth == 0) function ends.\n    if (call_depth == 0) {\n        const arena: *ArenaAllocator = @ptrCast(@alignCast(ctx.call_arena.ptr));\n        _ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });\n    }\n\n    ctx.call_depth = call_depth;\n    ctx.local = self.prev_local;\n    ctx.page.js = self.prev_context;\n}\n\npub const CallOpts = struct {\n    dom_exception: bool = false,\n    null_as_undefined: bool = false,\n    as_typed_array: bool = false,\n};\n\npub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {\n    const local = &self.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const info = FunctionCallbackInfo{ .handle = handle };\n\n    if (!info.isConstructCall()) {\n        handleError(T, @TypeOf(func), local, error.InvalidArgument, info, opts);\n        return;\n    }\n\n    self._constructor(func, info) catch |err| {\n        handleError(T, @TypeOf(func), local, err, info, opts);\n    };\n}\n\nfn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {\n    const F = @TypeOf(func);\n    const local = &self.local;\n    const args = try getArgs(F, 0, local, info);\n    const res = @call(.auto, func, args);\n\n    const ReturnType = @typeInfo(F).@\"fn\".return_type orelse {\n        @compileError(@typeName(F) ++ \" has a constructor without a return type\");\n    };\n\n    const new_this_handle = info.getThis();\n    var this = js.Object{ .local = local, .handle = new_this_handle };\n    if (@typeInfo(ReturnType) == .error_union) {\n        const non_error_res = res catch |err| return err;\n        this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);\n    } else {\n        this = try local.mapZigInstanceToJs(new_this_handle, res);\n    }\n\n    // If we got back a different object (existing wrapper), copy the prototype\n    // from new object. (this happens when we're upgrading an CustomElement)\n    if (this.handle != new_this_handle) {\n        const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;\n        var out: v8.MaybeBool = undefined;\n        v8.v8__Object__SetPrototype(this.handle, self.local.handle, prototype_handle, &out);\n        if (comptime IS_DEBUG) {\n            std.debug.assert(out.has_value and out.value);\n        }\n    }\n\n    info.getReturnValue().set(this.handle);\n}\n\npub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {\n    const local = &self.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const info = PropertyCallbackInfo{ .handle = handle };\n    return _getIndex(T, local, func, idx, info, opts) catch |err| {\n        handleError(T, @TypeOf(func), local, err, info, opts);\n        // not intercepted\n        return 0;\n    };\n}\n\nfn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {\n    const F = @TypeOf(func);\n    var args: ParameterTypes(F) = undefined;\n    @field(args, \"0\") = try TaggedOpaque.fromJS(*T, info.getThis());\n    @field(args, \"1\") = idx;\n    if (@typeInfo(F).@\"fn\".params.len == 3) {\n        @field(args, \"2\") = local.ctx.page;\n    }\n    const ret = @call(.auto, func, args);\n    return handleIndexedReturn(T, F, true, local, ret, info, opts);\n}\n\npub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {\n    const local = &self.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const info = PropertyCallbackInfo{ .handle = handle };\n    return _getNamedIndex(T, local, func, name, info, opts) catch |err| {\n        handleError(T, @TypeOf(func), local, err, info, opts);\n        // not intercepted\n        return 0;\n    };\n}\n\nfn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {\n    const F = @TypeOf(func);\n    var args: ParameterTypes(F) = undefined;\n    @field(args, \"0\") = try TaggedOpaque.fromJS(*T, info.getThis());\n    @field(args, \"1\") = try nameToString(local, @TypeOf(args.@\"1\"), name);\n    if (@typeInfo(F).@\"fn\".params.len == 3) {\n        @field(args, \"2\") = local.ctx.page;\n    }\n    const ret = @call(.auto, func, args);\n    return handleIndexedReturn(T, F, true, local, ret, info, opts);\n}\n\npub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {\n    const local = &self.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const info = PropertyCallbackInfo{ .handle = handle };\n    return _setNamedIndex(T, local, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {\n        handleError(T, @TypeOf(func), local, err, info, opts);\n        // not intercepted\n        return 0;\n    };\n}\n\nfn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {\n    const F = @TypeOf(func);\n    var args: ParameterTypes(F) = undefined;\n    @field(args, \"0\") = try TaggedOpaque.fromJS(*T, info.getThis());\n    @field(args, \"1\") = try nameToString(local, @TypeOf(args.@\"1\"), name);\n    @field(args, \"2\") = try local.jsValueToZig(@TypeOf(@field(args, \"2\")), js_value);\n    if (@typeInfo(F).@\"fn\".params.len == 4) {\n        @field(args, \"3\") = local.ctx.page;\n    }\n    const ret = @call(.auto, func, args);\n    return handleIndexedReturn(T, F, false, local, ret, info, opts);\n}\n\npub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {\n    const local = &self.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const info = PropertyCallbackInfo{ .handle = handle };\n    return _deleteNamedIndex(T, local, func, name, info, opts) catch |err| {\n        handleError(T, @TypeOf(func), local, err, info, opts);\n        return 0;\n    };\n}\n\nfn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {\n    const F = @TypeOf(func);\n    var args: ParameterTypes(F) = undefined;\n    @field(args, \"0\") = try TaggedOpaque.fromJS(*T, info.getThis());\n    @field(args, \"1\") = try nameToString(local, @TypeOf(args.@\"1\"), name);\n    if (@typeInfo(F).@\"fn\".params.len == 3) {\n        @field(args, \"2\") = local.ctx.page;\n    }\n    const ret = @call(.auto, func, args);\n    return handleIndexedReturn(T, F, false, local, ret, info, opts);\n}\n\npub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {\n    const local = &self.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const info = PropertyCallbackInfo{ .handle = handle };\n    return _getEnumerator(T, local, func, info, opts) catch |err| {\n        handleError(T, @TypeOf(func), local, err, info, opts);\n        // not intercepted\n        return 0;\n    };\n}\n\nfn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {\n    const F = @TypeOf(func);\n    var args: ParameterTypes(F) = undefined;\n    @field(args, \"0\") = try TaggedOpaque.fromJS(*T, info.getThis());\n    if (@typeInfo(F).@\"fn\".params.len == 2) {\n        @field(args, \"1\") = local.ctx.page;\n    }\n    const ret = @call(.auto, func, args);\n    return handleIndexedReturn(T, F, true, local, ret, info, opts);\n}\n\nfn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {\n    // need to unwrap this error immediately for when opts.null_as_undefined == true\n    // and we need to compare it to null;\n    const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {\n        .error_union => |eu| blk: {\n            break :blk ret catch |err| {\n                // We can't compare err == error.NotHandled if error.NotHandled\n                // isn't part of the possible error set. So we first need to check\n                // if error.NotHandled is part of the error set.\n                if (isInErrorSet(error.NotHandled, eu.error_set)) {\n                    if (err == error.NotHandled) {\n                        // not intercepted\n                        return 0;\n                    }\n                }\n                handleError(T, F, local, err, info, opts);\n                // not intercepted\n                return 0;\n            };\n        },\n        else => ret,\n    };\n\n    if (comptime with_value) {\n        info.getReturnValue().set(try local.zigValueToJs(non_error_ret, opts));\n    }\n    // intercepted\n    return 1;\n}\n\nfn isInErrorSet(err: anyerror, comptime T: type) bool {\n    inline for (@typeInfo(T).error_set.?) |e| {\n        if (err == @field(anyerror, e.name)) return true;\n    }\n    return false;\n}\n\nfn nameToString(local: *const Local, comptime T: type, name: *const v8.Name) !T {\n    const handle = @as(*const v8.String, @ptrCast(name));\n    if (T == string.String) {\n        return js.String.toSSO(.{ .local = local, .handle = handle }, false);\n    }\n    if (T == string.Global) {\n        return js.String.toSSO(.{ .local = local, .handle = handle }, true);\n    }\n    return try js.String.toSlice(.{ .local = local, .handle = handle });\n}\n\nfn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype, comptime opts: CallOpts) void {\n    const isolate = local.isolate;\n\n    if (comptime IS_DEBUG and @TypeOf(info) == FunctionCallbackInfo) {\n        if (log.enabled(.js, .debug)) {\n            const DOMException = @import(\"../webapi/DOMException.zig\");\n            if (DOMException.fromError(err) == null) {\n                // This isn't a DOMException, let's log it\n                logFunctionCallError(local, @typeName(T), @typeName(F), err, info);\n            }\n        }\n    }\n\n    const js_err: *const v8.Value = switch (err) {\n        error.TryCatchRethrow => return,\n        error.InvalidArgument => isolate.createTypeError(\"invalid argument\"),\n        error.TypeError => isolate.createTypeError(\"\"),\n        error.OutOfMemory => isolate.createError(\"out of memory\"),\n        error.IllegalConstructor => isolate.createError(\"Illegal Contructor\"),\n        else => blk: {\n            if (comptime opts.dom_exception) {\n                const DOMException = @import(\"../webapi/DOMException.zig\");\n                if (DOMException.fromError(err)) |ex| {\n                    const value = local.zigValueToJs(ex, .{}) catch break :blk isolate.createError(\"internal error\");\n                    break :blk value.handle;\n                }\n            }\n            break :blk isolate.createError(@errorName(err));\n        },\n    };\n\n    const js_exception = isolate.throwException(js_err);\n    info.getReturnValue().setValueHandle(js_exception);\n}\n\n// This is extracted to speed up compilation. When left inlined in handleError,\n// this can add as much as 10 seconds of compilation time.\nfn logFunctionCallError(local: *const Local, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {\n    const args_dump = serializeFunctionArgs(local, info) catch \"failed to serialize args\";\n    log.debug(.js, \"function call error\", .{\n        .type = type_name,\n        .func = func,\n        .err = err,\n        .args = args_dump,\n        .stack = local.stackTrace() catch |err1| @errorName(err1),\n    });\n}\n\nfn serializeFunctionArgs(local: *const Local, info: FunctionCallbackInfo) ![]const u8 {\n    var buf = std.Io.Writer.Allocating.init(local.call_arena);\n\n    const separator = log.separator();\n    for (0..info.length()) |i| {\n        try buf.writer.print(\"{s}{d} - \", .{ separator, i + 1 });\n        const js_value = info.getArg(@intCast(i), local);\n        try local.debugValue(js_value, &buf.writer);\n    }\n    return buf.written();\n}\n\n// Takes a function, and returns a tuple for its argument. Used when we\n// @call a function\nfn ParameterTypes(comptime F: type) type {\n    const params = @typeInfo(F).@\"fn\".params;\n    var fields: [params.len]std.builtin.Type.StructField = undefined;\n\n    inline for (params, 0..) |param, i| {\n        fields[i] = .{\n            .name = tupleFieldName(i),\n            .type = param.type.?,\n            .default_value_ptr = null,\n            .is_comptime = false,\n            .alignment = @alignOf(param.type.?),\n        };\n    }\n\n    return @Type(.{ .@\"struct\" = .{\n        .layout = .auto,\n        .decls = &.{},\n        .fields = &fields,\n        .is_tuple = true,\n    } });\n}\n\nfn tupleFieldName(comptime i: usize) [:0]const u8 {\n    return switch (i) {\n        0 => \"0\",\n        1 => \"1\",\n        2 => \"2\",\n        3 => \"3\",\n        4 => \"4\",\n        5 => \"5\",\n        6 => \"6\",\n        7 => \"7\",\n        8 => \"8\",\n        9 => \"9\",\n        else => std.fmt.comptimePrint(\"{d}\", .{i}),\n    };\n}\n\nfn isPage(comptime T: type) bool {\n    return T == *Page or T == *const Page;\n}\n\n// These wrap the raw v8 C API to provide a cleaner interface.\npub const FunctionCallbackInfo = struct {\n    handle: *const v8.FunctionCallbackInfo,\n\n    pub fn length(self: FunctionCallbackInfo) u32 {\n        return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));\n    }\n\n    pub fn getArg(self: FunctionCallbackInfo, index: u32, local: *const js.Local) js.Value {\n        return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };\n    }\n\n    pub fn getData(self: FunctionCallbackInfo) ?*anyopaque {\n        const data = v8.v8__FunctionCallbackInfo__Data(self.handle) orelse return null;\n        return v8.v8__External__Value(@ptrCast(data));\n    }\n\n    pub fn getThis(self: FunctionCallbackInfo) *const v8.Object {\n        return v8.v8__FunctionCallbackInfo__This(self.handle).?;\n    }\n\n    pub fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {\n        var rv: v8.ReturnValue = undefined;\n        v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);\n        return .{ .handle = rv };\n    }\n\n    fn isConstructCall(self: FunctionCallbackInfo) bool {\n        return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);\n    }\n};\n\npub const PropertyCallbackInfo = struct {\n    handle: *const v8.PropertyCallbackInfo,\n\n    pub fn getThis(self: PropertyCallbackInfo) *const v8.Object {\n        return v8.v8__PropertyCallbackInfo__This(self.handle).?;\n    }\n\n    pub fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {\n        var rv: v8.ReturnValue = undefined;\n        v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);\n        return .{ .handle = rv };\n    }\n};\n\nconst ReturnValue = struct {\n    handle: v8.ReturnValue,\n\n    pub fn set(self: ReturnValue, value: anytype) void {\n        const T = @TypeOf(value);\n        if (T == *const v8.Object) {\n            self.setValueHandle(@ptrCast(value));\n        } else if (T == *const v8.Value) {\n            self.setValueHandle(value);\n        } else if (T == js.Value) {\n            self.setValueHandle(value.handle);\n        } else {\n            @compileError(\"Unsupported type for ReturnValue.set: \" ++ @typeName(T));\n        }\n    }\n\n    pub fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {\n        v8.v8__ReturnValue__Set(self.handle, handle);\n    }\n};\n\npub const Function = struct {\n    pub const Opts = struct {\n        noop: bool = false,\n        static: bool = false,\n        dom_exception: bool = false,\n        as_typed_array: bool = false,\n        null_as_undefined: bool = false,\n        cache: ?Caching = null,\n        embedded_receiver: bool = false,\n\n        // We support two ways to cache a value directly into a v8::Object. The\n        // difference between the two is like the difference between a Map\n        // and a Struct.\n        // 1 - Using the object's internal fields. Think of this as\n        //     adding a field to the struct. It's fast, but the space is reserved\n        //     upfront for _every_ instance, whether we use it or not.\n        //\n        // 2 - Using the object's private state with a v8::Private key. Think of\n        //     this as a HashMap. It takes no memory if the cache isn't used\n        //     but has overhead when used.\n        //\n        // Consider `window.document`, (1) we have relatively few Window objects,\n        // (2) They all have a document and (3) The document is accessed _a lot_.\n        // An internal field makes sense.\n        //\n        // Consider `node.childNodes`, (1) we can have 20K+ node objects, (2)\n        // 95% of nodes will never have their .childNodes access by JavaScript.\n        // Private map lookup makes sense.\n        pub const Caching = union(enum) {\n            internal: u8,\n            private: []const u8,\n        };\n    };\n\n    pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {\n        const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;\n        const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate });\n        const info = FunctionCallbackInfo{ .handle = info_handle };\n\n        var hs: js.HandleScope = undefined;\n        hs.initWithIsolateHandle(v8_isolate);\n        defer hs.deinit();\n\n        var cache_state: CacheState = undefined;\n        if (comptime opts.cache) |cache| {\n            // This API is a bit weird. On\n            if (respondFromCache(cache, ctx, v8_context, info, &cache_state)) {\n                // Value was fetched from the cache and returned already\n                return;\n            } else {\n                // Cache miss: cache_state will have been populated\n            }\n        }\n\n        var caller: Caller = undefined;\n        caller.initWithContext(ctx, v8_context);\n        defer caller.deinit();\n\n        const js_value = _call(T, &caller.local, info, func, opts) catch |err| {\n            handleError(T, @TypeOf(func), &caller.local, err, info, .{\n                .dom_exception = opts.dom_exception,\n                .as_typed_array = opts.as_typed_array,\n                .null_as_undefined = opts.null_as_undefined,\n            });\n            return;\n        };\n\n        if (comptime opts.cache) |cache| {\n            cache_state.save(cache, js_value);\n        }\n    }\n\n    fn _call(comptime T: type, local: *const Local, info: FunctionCallbackInfo, func: anytype, comptime opts: Opts) !js.Value {\n        const F = @TypeOf(func);\n        var args: ParameterTypes(F) = undefined;\n        if (comptime opts.static) {\n            args = try getArgs(F, 0, local, info);\n        } else if (comptime opts.embedded_receiver) {\n            args = try getArgs(F, 1, local, info);\n            @field(args, \"0\") = @ptrCast(@alignCast(info.getData() orelse unreachable));\n        } else {\n            args = try getArgs(F, 1, local, info);\n            @field(args, \"0\") = try TaggedOpaque.fromJS(*T, info.getThis());\n        }\n        const res = @call(.auto, func, args);\n        const js_value = try local.zigValueToJs(res, .{\n            .dom_exception = opts.dom_exception,\n            .as_typed_array = opts.as_typed_array,\n            .null_as_undefined = opts.null_as_undefined,\n        });\n        info.getReturnValue().set(js_value);\n        return js_value;\n    }\n\n    // We can cache a value directly into the v8::Object so that our callback to fetch a property\n    // can be fast. Generally, think of it like this:\n    //   fn callback(handle: *const v8.FunctionCallbackInfo) callconv(.c) void {\n    //       const js_obj = info.getThis();\n    //       const cached_value = js_obj.getFromCache(\"Nodes.childNodes\");\n    //       info.returnValue().set(cached_value);\n    //   }\n    //\n    // That above pseudocode snippet is largely what this respondFromCache is doing.\n    // But on miss, it's also setting the `cache_state` with all of the data it\n    // got checking the cache, so that, once we get the value from our Zig code,\n    // it's quick to store in the v8::Object for subsequent calls.\n    fn respondFromCache(comptime cache: Opts.Caching, ctx: *Context, v8_context: *const v8.Context, info: FunctionCallbackInfo, cache_state: *CacheState) bool {\n        const js_this = info.getThis();\n        const return_value = info.getReturnValue();\n\n        switch (cache) {\n            .internal => |idx| {\n                if (v8.v8__Object__GetInternalField(js_this, idx)) |cached| {\n                    // means we can't cache undefined, since we can't tell the\n                    // difference between \"it isn't in the cache\" and  \"it's\n                    // in the cache with a valud of undefined\"\n                    if (!v8.v8__Value__IsUndefined(cached)) {\n                        return_value.set(cached);\n                        return true;\n                    }\n                }\n\n                // store this so that we can quickly save the result into the cache\n                cache_state.* = .{\n                    .js_this = js_this,\n                    .v8_context = v8_context,\n                    .mode = .{ .internal = idx },\n                };\n            },\n            .private => |private_symbol| {\n                const global_handle = &@field(ctx.env.private_symbols, private_symbol).handle;\n                const private_key: *const v8.Private = v8.v8__Global__Get(global_handle, ctx.isolate.handle).?;\n                if (v8.v8__Object__GetPrivate(js_this, v8_context, private_key)) |cached| {\n                    // This means we can't cache \"undefined\", since we can't tell\n                    // the difference between a (a) undefined == not in the cache\n                    // and (b) undefined == the cache value.  If this becomes\n                    // important, we can check HasPrivate first. But that requires\n                    // calling HasPrivate then GetPrivate.\n                    if (!v8.v8__Value__IsUndefined(cached)) {\n                        return_value.set(cached);\n                        return true;\n                    }\n                }\n\n                // store this so that we can quickly save the result into the cache\n                cache_state.* = .{\n                    .js_this = js_this,\n                    .v8_context = v8_context,\n                    .mode = .{ .private = private_key },\n                };\n            },\n        }\n\n        // cache miss\n        return false;\n    }\n\n    const CacheState = struct {\n        js_this: *const v8.Object,\n        v8_context: *const v8.Context,\n        mode: union(enum) {\n            internal: u8,\n            private: *const v8.Private,\n        },\n\n        pub fn save(self: *const CacheState, comptime cache: Opts.Caching, js_value: js.Value) void {\n            if (comptime cache == .internal) {\n                v8.v8__Object__SetInternalField(self.js_this, self.mode.internal, js_value.handle);\n            } else {\n                var out: v8.MaybeBool = undefined;\n                v8.v8__Object__SetPrivate(self.js_this, self.v8_context, self.mode.private, js_value.handle, &out);\n            }\n        }\n    };\n};\n\n// If we call a method in javascript: cat.lives('nine');\n//\n// Then we'd expect a Zig function with 2 parameters: a self and the string.\n// In this case, offset == 1. Offset is always 1 for setters or methods.\n//\n// Offset is always 0 for constructors.\n//\n// For constructors, setters and methods, we can further increase offset + 1\n// if the first parameter is an instance of Page.\n//\n// Finally, if the JS function is called with _more_ parameters and\n// the last parameter in Zig is an array, we'll try to slurp the additional\n// parameters into the array.\nfn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info: FunctionCallbackInfo) !ParameterTypes(F) {\n    var args: ParameterTypes(F) = undefined;\n\n    const params = @typeInfo(F).@\"fn\".params[offset..];\n    // Except for the constructor, the first parameter is always `self`\n    // This isn't something we'll bind from JS, so skip it.\n    const params_to_map = blk: {\n        if (params.len == 0) {\n            return args;\n        }\n\n        // If the last parameter is the Page, set it, and exclude it\n        // from our params slice, because we don't want to bind it to\n        // a JS argument\n        if (comptime isPage(params[params.len - 1].type.?)) {\n            @field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;\n            break :blk params[0 .. params.len - 1];\n        }\n\n        // we have neither a Page nor a JsObject. All params must be\n        // bound to a JavaScript value.\n        break :blk params;\n    };\n\n    if (params_to_map.len == 0) {\n        return args;\n    }\n\n    const js_parameter_count = info.length();\n    const last_js_parameter = params_to_map.len - 1;\n    var is_variadic = false;\n\n    {\n        // This is going to get complicated. If the last Zig parameter\n        // is a slice AND the corresponding javascript parameter is\n        // NOT an an array, then we'll treat it as a variadic.\n\n        const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;\n        const last_parameter_type_info = @typeInfo(last_parameter_type);\n        if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {\n            const slice_type = last_parameter_type_info.pointer.child;\n            const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);\n            if (slice_type == js.Value or (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8)) {\n                is_variadic = true;\n                if (js_parameter_count == 0) {\n                    @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};\n                } else if (js_parameter_count >= params_to_map.len) {\n                    const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);\n                    for (arr, last_js_parameter..) |*a, i| {\n                        a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));\n                    }\n                    @field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;\n                } else {\n                    @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};\n                }\n            }\n        }\n    }\n\n    inline for (params_to_map, 0..) |param, i| {\n        const field_index = comptime i + offset;\n        if (comptime i == params_to_map.len - 1) {\n            if (is_variadic) {\n                break;\n            }\n        }\n\n        if (comptime isPage(param.type.?)) {\n            @compileError(\"Page must be the last parameter (or 2nd last if there's a JsThis): \" ++ @typeName(F));\n        } else if (i >= js_parameter_count) {\n            if (@typeInfo(param.type.?) != .optional) {\n                return error.InvalidArgument;\n            }\n            @field(args, tupleFieldName(field_index)) = null;\n        } else {\n            const js_val = info.getArg(@intCast(i), local);\n            @field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {\n                return error.InvalidArgument;\n            };\n        }\n    }\n\n    return args;\n}\n"
  },
  {
    "path": "src/browser/js/Context.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"../../log.zig\");\n\nconst js = @import(\"js.zig\");\nconst Env = @import(\"Env.zig\");\nconst bridge = @import(\"bridge.zig\");\nconst Origin = @import(\"Origin.zig\");\nconst Scheduler = @import(\"Scheduler.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\nconst ScriptManager = @import(\"../ScriptManager.zig\");\n\nconst v8 = js.v8;\nconst Caller = js.Caller;\n\nconst Allocator = std.mem.Allocator;\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\n// Loosely maps to a Browser Page.\nconst Context = @This();\n\nid: usize,\nenv: *Env,\npage: *Page,\nsession: *Session,\nisolate: js.Isolate,\n\n// Per-context microtask queue for isolation between contexts\nmicrotask_queue: *v8.MicrotaskQueue,\n\n// The v8::Global<v8::Context>. When necessary, we can create a v8::Local<<v8::Context>>\n// from this, and we can free it when the context is done.\nhandle: v8.Global,\n\ncpu_profiler: ?*v8.CpuProfiler = null,\n\nheap_profiler: ?*v8.HeapProfiler = null,\n\n// references Env.templates\ntemplates: []*const v8.FunctionTemplate,\n\n// Arena for the lifetime of the context\narena: Allocator,\n\n// The page.call_arena\ncall_arena: Allocator,\n\n// Because calls can be nested (i.e.a function calling a callback),\n// we can only reset the call_arena when call_depth == 0. If we were\n// to reset it within a callback, it would invalidate the data of\n// the call which is calling the callback.\ncall_depth: usize = 0,\n\n// When a Caller is active (V8->Zig callback), this points to its Local.\n// When null, Zig->V8 calls must create a js.Local.Scope and initialize via\n// context.localScope\nlocal: ?*const js.Local = null,\n\norigin: *Origin,\n\n// Unlike other v8 types, like functions or objects, modules are not shared\n// across origins.\nglobal_modules: std.ArrayList(v8.Global) = .empty,\n\n// Our module cache: normalized module specifier => module.\nmodule_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty,\n\n// Module => Path. The key is the module hashcode (module.getIdentityHash)\n// and the value is the full path to the module. We need to capture this\n// so that when we're asked to resolve a dependent module, and all we're\n// given is the specifier, we can form the full path. The full path is\n// necessary to lookup/store the dependent module in the module_cache.\nmodule_identifier: std.AutoHashMapUnmanaged(u32, [:0]const u8) = .empty,\n\n// the page's script manager\nscript_manager: ?*ScriptManager,\n\n// Our macrotasks\nscheduler: Scheduler,\n\nunknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},\n\nconst ModuleEntry = struct {\n    // Can be null if we're asynchrously loading the module, in\n    // which case resolver_promise cannot be null.\n    module: ?js.Module.Global = null,\n\n    // The promise of the evaluating module. The resolved value is\n    // meaningless to us, but the resolver promise needs to chain\n    // to this, since we need to know when it's complete.\n    module_promise: ?js.Promise.Global = null,\n\n    // The promise for the resolver which is loading the module.\n    // (AKA, the first time we try to load it). This resolver will\n    // chain to the module_promise  and, when it's done evaluating\n    // will resolve its namespace. Any other attempt to load the\n    // module willchain to this.\n    resolver_promise: ?js.Promise.Global = null,\n};\n\npub fn fromC(c_context: *const v8.Context) ?*Context {\n    return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));\n}\n\n/// Returns the Context and v8::Context for the given isolate.\n/// If the current context is from a destroyed Context (e.g., navigated-away iframe),\n/// falls back to the incumbent context (the calling context).\npub fn fromIsolate(isolate: js.Isolate) struct { *Context, *const v8.Context } {\n    const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?;\n    if (fromC(v8_context)) |ctx| {\n        return .{ ctx, v8_context };\n    }\n    // The current context's Context struct has been freed (e.g., iframe navigated away).\n    // Fall back to the incumbent context (the calling context).\n    const v8_incumbent = v8.v8__Isolate__GetIncumbentContext(isolate.handle).?;\n    return .{ fromC(v8_incumbent).?, v8_incumbent };\n}\n\npub fn deinit(self: *Context) void {\n    if (comptime IS_DEBUG and @import(\"builtin\").is_test == false) {\n        var it = self.unknown_properties.iterator();\n        while (it.next()) |kv| {\n            log.debug(.unknown_prop, \"unknown property\", .{\n                .property = kv.key_ptr.*,\n                .occurrences = kv.value_ptr.count,\n                .first_stack = kv.value_ptr.first_stack,\n            });\n        }\n    }\n\n    const env = self.env;\n    defer env.app.arena_pool.release(self.arena);\n\n    var hs: js.HandleScope = undefined;\n    const entered = self.enter(&hs);\n    defer entered.exit();\n\n    // this can release objects\n    self.scheduler.deinit();\n\n    for (self.global_modules.items) |*global| {\n        v8.v8__Global__Reset(global);\n    }\n\n    self.session.releaseOrigin(self.origin);\n\n    // Clear the embedder data so that if V8 keeps this context alive\n    // (because objects created in it are still referenced), we don't\n    // have a dangling pointer to our freed Context struct.\n    v8.v8__Context__SetAlignedPointerInEmbedderData(entered.handle, 1, null);\n\n    v8.v8__Global__Reset(&self.handle);\n    env.isolate.notifyContextDisposed();\n    // There can be other tasks associated with this context that we need to\n    // purge while the context is still alive.\n    _ = env.pumpMessageLoop();\n    v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);\n}\n\npub fn setOrigin(self: *Context, key: ?[]const u8) !void {\n    const env = self.env;\n    const isolate = env.isolate;\n\n    lp.assert(self.origin.rc == 1, \"Ref opaque origin\", .{ .rc = self.origin.rc });\n\n    const origin = try self.session.getOrCreateOrigin(key);\n    errdefer self.session.releaseOrigin(origin);\n    try origin.takeover(self.origin);\n\n    self.origin = origin;\n\n    {\n        var ls: js.Local.Scope = undefined;\n        self.localScope(&ls);\n        defer ls.deinit();\n\n        // Set the V8::Context SecurityToken, which is a big part of what allows\n        // one context to access another.\n        const token_local = v8.v8__Global__Get(&origin.security_token, isolate.handle);\n        v8.v8__Context__SetSecurityToken(ls.local.handle, token_local);\n    }\n}\n\npub fn trackGlobal(self: *Context, global: v8.Global) !void {\n    return self.origin.trackGlobal(global);\n}\n\npub fn trackTemp(self: *Context, global: v8.Global) !void {\n    return self.origin.trackTemp(global);\n}\n\npub fn weakRef(self: *Context, obj: anytype) void {\n    const resolved = js.Local.resolveValue(obj);\n    const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {\n        if (comptime IS_DEBUG) {\n            // should not be possible\n            std.debug.assert(false);\n        }\n        return;\n    };\n    v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);\n}\n\npub fn safeWeakRef(self: *Context, obj: anytype) void {\n    const resolved = js.Local.resolveValue(obj);\n    const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {\n        if (comptime IS_DEBUG) {\n            // should not be possible\n            std.debug.assert(false);\n        }\n        return;\n    };\n    v8.v8__Global__ClearWeak(&fc.global);\n    v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);\n}\n\npub fn strongRef(self: *Context, obj: anytype) void {\n    const resolved = js.Local.resolveValue(obj);\n    const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {\n        if (comptime IS_DEBUG) {\n            // should not be possible\n            std.debug.assert(false);\n        }\n        return;\n    };\n    v8.v8__Global__ClearWeak(&fc.global);\n}\n\n// Any operation on the context have to be made from a local.\npub fn localScope(self: *Context, ls: *js.Local.Scope) void {\n    const isolate = self.isolate;\n    js.HandleScope.init(&ls.handle_scope, isolate);\n\n    const local_v8_context: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));\n    v8.v8__Context__Enter(local_v8_context);\n\n    // TODO: add and init ls.hs  for the handlescope\n    ls.local = .{\n        .ctx = self,\n        .isolate = isolate,\n        .handle = local_v8_context,\n        .call_arena = self.call_arena,\n    };\n}\n\npub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@TypeOf(global)) {\n    const l = self.local orelse @panic(\"toLocal called without active Caller context\");\n    return l.toLocal(global);\n}\n\npub fn getIncumbent(self: *Context) *Page {\n    return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?.page;\n}\n\npub fn stringToPersistedFunction(\n    self: *Context,\n    function_body: []const u8,\n    comptime parameter_names: []const []const u8,\n    extensions: []const v8.Object,\n) !js.Function.Global {\n    var ls: js.Local.Scope = undefined;\n    self.localScope(&ls);\n    defer ls.deinit();\n\n    const js_function = try ls.local.compileFunction(function_body, parameter_names, extensions);\n    return js_function.persist();\n}\n\npub fn module(self: *Context, comptime want_result: bool, local: *const js.Local, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {\n    const mod, const owned_url = blk: {\n        const arena = self.arena;\n\n        // gop will _always_ initiated if cacheable == true\n        var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;\n        if (cacheable) {\n            gop = try self.module_cache.getOrPut(arena, url);\n            if (gop.found_existing) {\n                if (gop.value_ptr.module) |cache_mod| {\n                    if (gop.value_ptr.module_promise == null) {\n                        // This an usual case, but it can happen if a module is\n                        // first asynchronously requested and then synchronously\n                        // requested as a child of some root import. In that case,\n                        // the module may not be instantiated yet (so we have to\n                        // do that). It might not be evaluated yet. So we have\n                        // to do that too. Evaluation is particularly important\n                        // as it sets up our cache entry's module_promise.\n                        // It appears that v8 handles potential double-instantiated\n                        // and double-evaluated modules safely. The 2nd instantiation\n                        // is a no-op, and the second evaluation returns the same\n                        // promise.\n                        const mod = local.toLocal(cache_mod);\n                        if (mod.getStatus() == .kUninstantiated and try mod.instantiate(resolveModuleCallback) == false) {\n                            return error.ModuleInstantiationError;\n                        }\n                        return self.evaluateModule(want_result, mod, url, true);\n                    }\n                    return if (comptime want_result) gop.value_ptr.* else {};\n                }\n            } else {\n                // first time seeing this\n                gop.value_ptr.* = .{};\n            }\n        }\n\n        const owned_url = try arena.dupeZ(u8, url);\n        if (cacheable and !gop.found_existing) {\n            gop.key_ptr.* = owned_url;\n        }\n        const m = try compileModule(local, src, owned_url);\n\n        if (cacheable) {\n            // compileModule is synchronous - nothing can modify the cache during compilation\n            lp.assert(gop.value_ptr.module == null, \"Context.module has module\", .{});\n            gop.value_ptr.module = try m.persist();\n        }\n\n        break :blk .{ m, owned_url };\n    };\n\n    try self.postCompileModule(mod, owned_url, local);\n\n    if (try mod.instantiate(resolveModuleCallback) == false) {\n        return error.ModuleInstantiationError;\n    }\n\n    return self.evaluateModule(want_result, mod, owned_url, cacheable);\n}\n\nfn evaluateModule(self: *Context, comptime want_result: bool, mod: js.Module, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {\n    const evaluated = mod.evaluate() catch {\n        if (comptime IS_DEBUG) {\n            std.debug.assert(mod.getStatus() == .kErrored);\n        }\n\n        // Some module-loading errors aren't handled by TryCatch. We need to\n        // get the error from the module itself.\n        const message = blk: {\n            const e = mod.getException().toString() catch break :blk \"???\";\n            break :blk e.toSlice() catch \"???\";\n        };\n        log.warn(.js, \"evaluate module\", .{\n            .message = message,\n            .specifier = url,\n        });\n        return error.EvaluationError;\n    };\n\n    // https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f\n    // Must be a promise that gets returned here.\n    lp.assert(evaluated.isPromise(), \"Context.module non-promise\", .{});\n\n    if (!cacheable) {\n        switch (comptime want_result) {\n            false => return,\n            true => unreachable,\n        }\n    }\n\n    // entry has to have been created atop this function\n    const entry = self.module_cache.getPtr(url).?;\n\n    // and the module must have been set after we compiled it\n    lp.assert(entry.module != null, \"Context.module with module\", .{});\n    if (entry.module_promise != null) {\n        // While loading this script, it's possible that it was dynamically\n        // included (either the module dynamically loaded itself (unlikely) or\n        // it included a script which dynamically imported it). If it was, then\n        // the module_promise would already be setup, and we don't need to do\n        // anything\n    } else {\n        // The *much* more likely case where the module we're trying to load\n        // didn't [directly or indirectly] dynamically load itself.\n        entry.module_promise = try evaluated.toPromise().persist();\n    }\n    return if (comptime want_result) entry.* else {};\n}\n\nfn compileModule(local: *const js.Local, src: []const u8, name: []const u8) !js.Module {\n    var origin_handle: v8.ScriptOrigin = undefined;\n    v8.v8__ScriptOrigin__CONSTRUCT2(\n        &origin_handle,\n        local.isolate.initStringHandle(name),\n        0, // resource_line_offset\n        0, // resource_column_offset\n        false, // resource_is_shared_cross_origin\n        -1, // script_id\n        null, // source_map_url\n        false, // resource_is_opaque\n        false, // is_wasm\n        true, // is_module\n        null, // host_defined_options\n    );\n\n    var source_handle: v8.ScriptCompilerSource = undefined;\n    v8.v8__ScriptCompiler__Source__CONSTRUCT2(\n        local.isolate.initStringHandle(src),\n        &origin_handle,\n        null, // cached data\n        &source_handle,\n    );\n\n    defer v8.v8__ScriptCompiler__Source__DESTRUCT(&source_handle);\n\n    const module_handle = v8.v8__ScriptCompiler__CompileModule(\n        local.isolate.handle,\n        &source_handle,\n        v8.kNoCompileOptions,\n        v8.kNoCacheNoReason,\n    ) orelse {\n        return error.JsException;\n    };\n\n    return .{\n        .local = local,\n        .handle = module_handle,\n    };\n}\n\n// After we compile a module, whether it's a top-level one, or a nested one,\n// we always want to track its identity (so that, if this module imports other\n// modules, we can resolve the full URL), and preload any dependent modules.\nfn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8, local: *const js.Local) !void {\n    try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);\n\n    // Non-async modules are blocking. We can download them in parallel, but\n    // they need to be processed serially. So we want to get the list of\n    // dependent modules this module has and start downloading them asap.\n    const requests = mod.getModuleRequests();\n    const request_len = requests.len();\n    const script_manager = self.script_manager.?;\n    for (0..request_len) |i| {\n        const specifier = requests.get(i).specifier(local);\n        const normalized_specifier = try script_manager.resolveSpecifier(\n            self.call_arena,\n            url,\n            try specifier.toSliceZ(),\n        );\n        const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);\n        if (!nested_gop.found_existing) {\n            const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);\n            nested_gop.key_ptr.* = owned_specifier;\n            nested_gop.value_ptr.* = .{};\n            try script_manager.preloadImport(owned_specifier, url);\n        } else if (nested_gop.value_ptr.module == null) {\n            // Entry exists but module failed to compile previously.\n            // The imported_modules entry may have been consumed, so\n            // re-preload to ensure waitForImport can find it.\n            // Key was stored via dupeZ so it has a sentinel in memory.\n            const key = nested_gop.key_ptr.*;\n            const key_z: [:0]const u8 = key.ptr[0..key.len :0];\n            try script_manager.preloadImport(key_z, url);\n        }\n    }\n}\n\nfn newFunctionWithData(local: *const js.Local, comptime callback: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, data: *anyopaque) js.Function {\n    const external = local.isolate.createExternal(data);\n    const handle = v8.v8__Function__New__DEFAULT2(local.handle, callback, @ptrCast(external)).?;\n    return .{\n        .local = local,\n        .handle = handle,\n    };\n}\n\n// == Callbacks ==\n// Callback from V8, asking us to load a module. The \"specifier\" is\n// the src of the module to load.\nfn resolveModuleCallback(\n    c_context: ?*const v8.Context,\n    c_specifier: ?*const v8.String,\n    import_attributes: ?*const v8.FixedArray,\n    c_referrer: ?*const v8.Module,\n) callconv(.c) ?*const v8.Module {\n    _ = import_attributes;\n\n    const self = fromC(c_context.?).?;\n    const local = js.Local{\n        .ctx = self,\n        .handle = c_context.?,\n        .isolate = self.isolate,\n        .call_arena = self.call_arena,\n    };\n\n    const specifier = js.String.toSliceZ(.{ .local = &local, .handle = c_specifier.? }) catch |err| {\n        log.err(.js, \"resolve module\", .{ .err = err });\n        return null;\n    };\n    const referrer = js.Module{ .local = &local, .handle = c_referrer.? };\n\n    return self._resolveModuleCallback(referrer, specifier, &local) catch |err| {\n        log.err(.js, \"resolve module\", .{\n            .err = err,\n            .specifier = specifier,\n        });\n        return null;\n    };\n}\n\npub fn dynamicModuleCallback(\n    c_context: ?*const v8.Context,\n    host_defined_options: ?*const v8.Data,\n    resource_name: ?*const v8.Value,\n    v8_specifier: ?*const v8.String,\n    import_attrs: ?*const v8.FixedArray,\n) callconv(.c) ?*v8.Promise {\n    _ = host_defined_options;\n    _ = import_attrs;\n\n    const self = fromC(c_context.?).?;\n    const local = js.Local{\n        .ctx = self,\n        .handle = c_context.?,\n        .call_arena = self.call_arena,\n        .isolate = self.isolate,\n    };\n\n    const resource = blk: {\n        const resource_value = js.Value{ .handle = resource_name.?, .local = &local };\n        if (resource_value.isNullOrUndefined()) {\n            // will only be null / undefined in extreme cases (e.g. WPT tests)\n            // where you're\n            break :blk self.page.base();\n        }\n\n        break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {\n            log.err(.app, \"OOM\", .{ .err = err, .src = \"dynamicModuleCallback1\" });\n            return @constCast((local.rejectPromise(\"Out of memory\") catch return null).handle);\n        };\n    };\n\n    const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {\n        log.err(.app, \"OOM\", .{ .err = err, .src = \"dynamicModuleCallback2\" });\n        return @constCast((local.rejectPromise(\"Out of memory\") catch return null).handle);\n    };\n\n    const normalized_specifier = self.script_manager.?.resolveSpecifier(\n        self.arena, // might need to survive until the module is loaded\n        resource,\n        specifier,\n    ) catch |err| {\n        log.err(.app, \"OOM\", .{ .err = err, .src = \"dynamicModuleCallback3\" });\n        return @constCast((local.rejectPromise(\"Out of memory\") catch return null).handle);\n    };\n\n    const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {\n        log.err(.js, \"dynamic module callback\", .{\n            .err = err,\n        });\n        break :blk local.rejectPromise(\"Failed to load module\") catch return null;\n    };\n    return @constCast(promise.handle);\n}\n\npub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void {\n    // @HandleScope  implement this without a fat context/local..\n    const self = fromC(c_context.?).?;\n    var local = js.Local{\n        .ctx = self,\n        .handle = c_context.?,\n        .isolate = self.isolate,\n        .call_arena = self.call_arena,\n    };\n\n    const m = js.Module{ .local = &local, .handle = c_module.? };\n    const meta = js.Object{ .local = &local, .handle = @ptrCast(c_meta.?) };\n\n    const url = self.module_identifier.get(m.getIdentityHash()) orelse {\n        // Shouldn't be possible.\n        log.err(.js, \"import meta\", .{ .err = error.UnknownModuleReferrer });\n        return;\n    };\n\n    const js_value = local.zigValueToJs(url, .{}) catch {\n        log.err(.js, \"import meta\", .{ .err = error.FailedToConvertUrl });\n        return;\n    };\n    const res = meta.defineOwnProperty(\"url\", js_value, 0) orelse false;\n    if (!res) {\n        log.err(.js, \"import meta\", .{ .err = error.FailedToSet });\n    }\n}\n\nfn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]const u8, local: *const js.Local) !?*const v8.Module {\n    const referrer_path = self.module_identifier.get(referrer.getIdentityHash()) orelse {\n        // Shouldn't be possible.\n        return error.UnknownModuleReferrer;\n    };\n\n    const normalized_specifier = try self.script_manager.?.resolveSpecifier(\n        self.arena,\n        referrer_path,\n        specifier,\n    );\n\n    const entry = self.module_cache.getPtr(normalized_specifier).?;\n    if (entry.module) |m| {\n        return local.toLocal(m).handle;\n    }\n\n    var source = self.script_manager.?.waitForImport(normalized_specifier) catch |err| switch (err) {\n        error.UnknownModule => blk: {\n            // Module is in cache but was consumed from imported_modules\n            // (e.g., by a previous failed resolution). Re-preload and retry.\n            try self.script_manager.?.preloadImport(normalized_specifier, referrer_path);\n            break :blk try self.script_manager.?.waitForImport(normalized_specifier);\n        },\n        else => return err,\n    };\n    defer source.deinit();\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(local);\n    defer try_catch.deinit();\n\n    const mod = try compileModule(local, source.src(), normalized_specifier);\n    try self.postCompileModule(mod, normalized_specifier, local);\n    entry.module = try mod.persist();\n    // Note: We don't instantiate/evaluate here - V8 will handle instantiation\n    // as part of the parent module's dependency chain. If there's a resolver\n    // waiting, it will be handled when the module is eventually evaluated\n    // (either as a top-level module or when accessed via dynamic import)\n    return mod.handle;\n}\n\n// Will get passed to ScriptManager and then passed back to us when\n// the src of the module is loaded\nconst DynamicModuleResolveState = struct {\n    // The module that we're resolving (we'll actually resolve its\n    // namespace)\n    module: ?js.Module.Global,\n    context_id: usize,\n    context: *Context,\n    specifier: [:0]const u8,\n    resolver: js.PromiseResolver.Global,\n};\n\nfn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []const u8, local: *const js.Local) !js.Promise {\n    const gop = try self.module_cache.getOrPut(self.arena, specifier);\n    if (gop.found_existing) {\n        if (gop.value_ptr.resolver_promise) |rp| {\n            return local.toLocal(rp);\n        }\n    }\n\n    const resolver = local.createPromiseResolver();\n    const state = try self.arena.create(DynamicModuleResolveState);\n\n    state.* = .{\n        .module = null,\n        .context = self,\n        .specifier = specifier,\n        .context_id = self.id,\n        .resolver = try resolver.persist(),\n    };\n\n    const promise = resolver.promise();\n\n    if (!gop.found_existing or gop.value_ptr.module == null) {\n        // Either this is a completely new module, or it's an entry that was\n        // created (e.g., in postCompileModule) but not yet loaded\n        // this module hasn't been seen before. This is the most\n        // complicated path.\n\n        // First, we'll setup a bare entry into our cache. This will\n        // prevent anyone one else from trying to asynchronously load\n        // it. Instead, they can just return our promise.\n        gop.value_ptr.* = ModuleEntry{\n            .module = null,\n            .module_promise = null,\n            .resolver_promise = try promise.persist(),\n        };\n\n        // Next, we need to actually load it.\n        self.script_manager.?.getAsyncImport(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {\n            const error_msg = local.newString(@errorName(err));\n            _ = resolver.reject(\"dynamic module get async\", error_msg);\n        };\n\n        // For now, we're done. but this will be continued in\n        // `dynamicModuleSourceCallback`, once the source for the module is loaded.\n        return promise;\n    }\n\n    // we might update the map, so we might need to re-fetch this.\n    var entry = gop.value_ptr;\n\n    // So we have a module, but no async resolver. This can only\n    // happen if the module was first synchronously loaded (Does that\n    // ever even happen?!) You'd think we can just return the module\n    // but no, we need to resolve the module namespace, and the\n    // module could still be loading!\n    // We need to do part of what the first case is going to do in\n    // `dynamicModuleSourceCallback`, but we can skip some steps\n    // since the module is already loaded,\n    lp.assert(gop.value_ptr.module != null, \"Context._dynamicModuleCallback has module\", .{});\n\n    // If the module hasn't been evaluated yet (it was only instantiated\n    // as a static import dependency), we need to evaluate it now.\n    if (entry.module_promise == null) {\n        const mod = local.toLocal(gop.value_ptr.module.?);\n        const status = mod.getStatus();\n        if (status == .kEvaluated or status == .kEvaluating) {\n            // Module was already evaluated (shouldn't normally happen, but handle it).\n            // Create a pre-resolved promise with the module namespace.\n            const module_resolver = local.createPromiseResolver();\n            module_resolver.resolve(\"resolve module\", mod.getModuleNamespace());\n            _ = try module_resolver.persist();\n            entry.module_promise = try module_resolver.promise().persist();\n        } else {\n            // the module was loaded, but not evaluated, we _have_ to evaluate it now\n            if (status == .kUninstantiated) {\n                if (try mod.instantiate(resolveModuleCallback) == false) {\n                    _ = resolver.reject(\"module instantiation\", local.newString(\"Module instantiation failed\"));\n                    return promise;\n                }\n            }\n\n            const evaluated = mod.evaluate() catch {\n                if (comptime IS_DEBUG) {\n                    std.debug.assert(mod.getStatus() == .kErrored);\n                }\n                _ = resolver.reject(\"module evaluation\", local.newString(\"Module evaluation failed\"));\n                return promise;\n            };\n            lp.assert(evaluated.isPromise(), \"Context._dynamicModuleCallback non-promise\", .{});\n            // mod.evaluate can invalidate or gop\n            entry = self.module_cache.getPtr(specifier).?;\n            entry.module_promise = try evaluated.toPromise().persist();\n        }\n    }\n\n    // like before, we want to set this up so that if anything else\n    // tries to load this module, it can just return our promise\n    // since we're going to be doing all the work.\n    entry.resolver_promise = try promise.persist();\n\n    // But we can skip direclty to `resolveDynamicModule` which is\n    // what the above callback will eventually do.\n    self.resolveDynamicModule(state, entry.*, local);\n    return promise;\n}\n\nfn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptManager.ModuleSource) void {\n    const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx));\n    var self = state.context;\n\n    var ls: js.Local.Scope = undefined;\n    self.localScope(&ls);\n    defer ls.deinit();\n\n    const local = &ls.local;\n\n    var ms = module_source_ catch |err| {\n        _ = local.toLocal(state.resolver).reject(\"dynamic module source\", local.newString(@errorName(err)));\n        return;\n    };\n\n    const module_entry = blk: {\n        defer ms.deinit();\n\n        var try_catch: js.TryCatch = undefined;\n        try_catch.init(local);\n        defer try_catch.deinit();\n\n        break :blk self.module(true, local, ms.src(), state.specifier, true) catch |err| {\n            const caught = try_catch.caughtOrError(self.call_arena, err);\n            log.err(.js, \"module compilation failed\", .{\n                .caught = caught,\n                .specifier = state.specifier,\n            });\n            _ = local.toLocal(state.resolver).reject(\"dynamic compilation failure\", local.newString(caught.exception orelse \"\"));\n            return;\n        };\n    };\n\n    self.resolveDynamicModule(state, module_entry, local);\n}\n\nfn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, module_entry: ModuleEntry, local: *const js.Local) void {\n    defer local.runMicrotasks();\n\n    // we can only be here if the module has been evaluated and if\n    // we have a resolve loading this asynchronously.\n    lp.assert(module_entry.module_promise != null, \"Context.resolveDynamicModule has module_promise\", .{});\n    lp.assert(module_entry.resolver_promise != null, \"Context.resolveDynamicModule has resolver_promise\", .{});\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.module_cache.contains(state.specifier));\n    }\n    state.module = module_entry.module.?;\n\n    // We've gotten the source for the module and are evaluating it.\n    // You might think we're done, but the module evaluation is\n    // itself asynchronous. We need to chain to the module's own\n    // promise. When the module is evaluated, it resolves to the\n    // last value of the module. But, for module loading, we need to\n    // resolve to the module's namespace.\n\n    const then_callback = newFunctionWithData(local, struct {\n        pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n            var c: Caller = undefined;\n            c.initFromHandle(callback_handle);\n            defer c.deinit();\n\n            const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? };\n            const s: *DynamicModuleResolveState = @ptrCast(@alignCast(info.getData() orelse return));\n\n            if (s.context_id != c.local.ctx.id) {\n                // The microtask is tied to the isolate, not the context\n                // it can be resolved while another context is active\n                // (Which seems crazy to me). If that happens, then\n                // another page was loaded and we MUST ignore this\n                // (most of the fields in state are not valid)\n                return;\n            }\n            const l = c.local;\n            defer l.runMicrotasks();\n            const namespace = l.toLocal(s.module.?).getModuleNamespace();\n            _ = l.toLocal(s.resolver).resolve(\"resolve namespace\", namespace);\n        }\n    }.callback, @ptrCast(state));\n\n    const catch_callback = newFunctionWithData(local, struct {\n        pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n            var c: Caller = undefined;\n            c.initFromHandle(callback_handle);\n            defer c.deinit();\n\n            const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? };\n            const s: *DynamicModuleResolveState = @ptrCast(@alignCast(info.getData() orelse return));\n\n            const l = &c.local;\n            if (s.context_id != l.ctx.id) {\n                return;\n            }\n\n            defer l.runMicrotasks();\n            _ = l.toLocal(s.resolver).reject(\"catch callback\", js.Value{\n                .local = l,\n                .handle = v8.v8__FunctionCallbackInfo__Data(callback_handle).?,\n            });\n        }\n    }.callback, @ptrCast(state));\n\n    _ = local.toLocal(module_entry.module_promise.?).thenAndCatch(then_callback, catch_callback) catch |err| {\n        log.err(.js, \"module evaluation is promise\", .{\n            .err = err,\n            .specifier = state.specifier,\n        });\n        _ = local.toLocal(state.resolver).reject(\"module promise\", local.newString(\"Failed to evaluate promise\"));\n    };\n}\n\n// Used to make temporarily enter and exit a context, updating and restoring\n// page.js:\n//    var hs: js.HandleScope = undefined;\n//    const entered = ctx.enter(&hs);\n//    defer entered.exit();\npub fn enter(self: *Context, hs: *js.HandleScope) Entered {\n    const isolate = self.isolate;\n    js.HandleScope.init(hs, isolate);\n\n    const page = self.page;\n    const original = page.js;\n    page.js = self;\n\n    const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));\n    v8.v8__Context__Enter(handle);\n    return .{ .original = original, .handle = handle, .handle_scope = hs };\n}\n\nconst Entered = struct {\n    // the context we should restore on the page\n    original: *Context,\n\n    // the handle of the entered context\n    handle: *const v8.Context,\n\n    handle_scope: *js.HandleScope,\n\n    pub fn exit(self: Entered) void {\n        self.original.page.js = self.original;\n        v8.v8__Context__Exit(self.handle);\n        self.handle_scope.deinit();\n    }\n};\n\npub fn queueMutationDelivery(self: *Context) !void {\n    self.enqueueMicrotask(struct {\n        fn run(ctx: *Context) void {\n            ctx.page.deliverMutations();\n        }\n    }.run);\n}\n\npub fn queueIntersectionChecks(self: *Context) !void {\n    self.enqueueMicrotask(struct {\n        fn run(ctx: *Context) void {\n            ctx.page.performScheduledIntersectionChecks();\n        }\n    }.run);\n}\n\npub fn queueIntersectionDelivery(self: *Context) !void {\n    self.enqueueMicrotask(struct {\n        fn run(ctx: *Context) void {\n            ctx.page.deliverIntersections();\n        }\n    }.run);\n}\n\npub fn queueSlotchangeDelivery(self: *Context) !void {\n    self.enqueueMicrotask(struct {\n        fn run(ctx: *Context) void {\n            ctx.page.deliverSlotchangeEvents();\n        }\n    }.run);\n}\n\n// Helper for executing a Microtask on this Context. In V8, microtasks aren't\n// associated to a Context - they are just functions to execute in an Isolate.\n// But for these Context microtasks, we want to (a) make sure the context isn't\n// being shut down and (b) that it's entered.\nfn enqueueMicrotask(self: *Context, callback: anytype) void {\n    // Use context-specific microtask queue instead of isolate queue\n    v8.v8__MicrotaskQueue__EnqueueMicrotask(self.microtask_queue, self.isolate.handle, struct {\n        fn run(data: ?*anyopaque) callconv(.c) void {\n            const ctx: *Context = @ptrCast(@alignCast(data.?));\n            var hs: js.HandleScope = undefined;\n            const entered = ctx.enter(&hs);\n            defer entered.exit();\n            callback(ctx);\n        }\n    }.run, self);\n}\n\n// There's an assumption here: the js.Function will be alive when microtasks are\n// run. If we're Env.runMicrotasks in all the places that we're supposed to, then\n// this should be safe (I think). In whatever HandleScope a microtask is enqueued,\n// PerformCheckpoint should be run. So the v8::Local<v8::Function> should remain\n// valid. If we have problems with this, a simple solution is to provide a Zig\n// wrapper for these callbacks which references a js.Function.Temp, on callback\n// it executes the function and then releases the global.\npub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {\n    // Use context-specific microtask queue instead of isolate queue\n    v8.v8__MicrotaskQueue__EnqueueMicrotaskFunc(self.microtask_queue, self.isolate.handle, cb.handle);\n}\n\n// == Profiler ==\npub fn startCpuProfiler(self: *Context) void {\n    if (comptime !IS_DEBUG) {\n        // Still testing this out, don't have it properly exposed, so add this\n        // guard for the time being to prevent any accidental/weird prod issues.\n        @compileError(\"CPU Profiling is only available in debug builds\");\n    }\n\n    var ls: js.Local.Scope = undefined;\n    self.localScope(&ls);\n    defer ls.deinit();\n\n    std.debug.assert(self.cpu_profiler == null);\n    v8.v8__CpuProfiler__UseDetailedSourcePositionsForProfiling(self.isolate.handle);\n\n    const cpu_profiler = v8.v8__CpuProfiler__Get(self.isolate.handle).?;\n    const title = self.isolate.initStringHandle(\"v8_cpu_profile\");\n    v8.v8__CpuProfiler__StartProfiling(cpu_profiler, title);\n    self.cpu_profiler = cpu_profiler;\n}\n\npub fn stopCpuProfiler(self: *Context) ![]const u8 {\n    var ls: js.Local.Scope = undefined;\n    self.localScope(&ls);\n    defer ls.deinit();\n\n    const title = self.isolate.initStringHandle(\"v8_cpu_profile\");\n    const handle = v8.v8__CpuProfiler__StopProfiling(self.cpu_profiler.?, title) orelse return error.NoProfiles;\n    const string_handle = v8.v8__CpuProfile__Serialize(handle, self.isolate.handle) orelse return error.NoProfile;\n    return (js.String{ .local = &ls.local, .handle = string_handle }).toSlice();\n}\n\npub fn startHeapProfiler(self: *Context) void {\n    if (comptime !IS_DEBUG) {\n        @compileError(\"Heap Profiling is only available in debug builds\");\n    }\n\n    var ls: js.Local.Scope = undefined;\n    self.localScope(&ls);\n    defer ls.deinit();\n\n    std.debug.assert(self.heap_profiler == null);\n    const heap_profiler = v8.v8__HeapProfiler__Get(self.isolate.handle).?;\n\n    // Sample every 32KB, stack depth 32\n    v8.v8__HeapProfiler__StartSamplingHeapProfiler(heap_profiler, 32 * 1024, 32);\n    v8.v8__HeapProfiler__StartTrackingHeapObjects(heap_profiler, true);\n\n    self.heap_profiler = heap_profiler;\n}\n\npub fn stopHeapProfiler(self: *Context) !struct { []const u8, []const u8 } {\n    var ls: js.Local.Scope = undefined;\n    self.localScope(&ls);\n    defer ls.deinit();\n\n    const allocating = blk: {\n        const profile = v8.v8__HeapProfiler__GetAllocationProfile(self.heap_profiler.?);\n        const string_handle = v8.v8__AllocationProfile__Serialize(profile, self.isolate.handle);\n        v8.v8__HeapProfiler__StopSamplingHeapProfiler(self.heap_profiler.?);\n        v8.v8__AllocationProfile__Delete(profile);\n        break :blk try (js.String{ .local = &ls.local, .handle = string_handle.? }).toSlice();\n    };\n\n    const snapshot = blk: {\n        const snapshot = v8.v8__HeapProfiler__TakeHeapSnapshot(self.heap_profiler.?, null) orelse return error.NoProfiles;\n        const string_handle = v8.v8__HeapSnapshot__Serialize(snapshot, self.isolate.handle);\n        v8.v8__HeapProfiler__StopTrackingHeapObjects(self.heap_profiler.?);\n        v8.v8__HeapSnapshot__Delete(snapshot);\n        break :blk try (js.String{ .local = &ls.local, .handle = string_handle.? }).toSlice();\n    };\n\n    return .{ allocating, snapshot };\n}\n\nconst UnknownPropertyStat = struct {\n    count: usize,\n    first_stack: []const u8,\n};\n"
  },
  {
    "path": "src/browser/js/Env.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst builtin = @import(\"builtin\");\n\nconst v8 = js.v8;\n\nconst App = @import(\"../../App.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst bridge = @import(\"bridge.zig\");\nconst Origin = @import(\"Origin.zig\");\nconst Context = @import(\"Context.zig\");\nconst Isolate = @import(\"Isolate.zig\");\nconst Platform = @import(\"Platform.zig\");\nconst Snapshot = @import(\"Snapshot.zig\");\nconst Inspector = @import(\"Inspector.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Window = @import(\"../webapi/Window.zig\");\n\nconst JsApis = bridge.JsApis;\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = builtin.mode == .Debug;\n\nfn initClassIds() void {\n    inline for (JsApis, 0..) |JsApi, i| {\n        JsApi.Meta.class_id = i;\n    }\n}\n\nvar class_id_once = std.once(initClassIds);\n\n// The Env maps to a V8 isolate, which represents a isolated sandbox for\n// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,\n// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.\n// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance\n// of S must be given. This instance is available to any Zig binding.\n// The `types` parameter is a tuple of Zig structures we want to bind to V8.\nconst Env = @This();\n\napp: *App,\n\nallocator: Allocator,\n\nplatform: *const Platform,\n\n// the global isolate\nisolate: js.Isolate,\n\ncontexts: [64]*Context,\ncontext_count: usize,\n\n// just kept around because we need to free it on deinit\nisolate_params: *v8.CreateParams,\n\ncontext_id: usize,\n\n// Maps origin -> shared Origin contains, for v8 values shared across\n// same-origin Contexts. There's a mismatch here between our JS model and our\n// Browser model. Origins only live as long as the root page of a session exists.\n// It would be wrong/dangerous to re-use an Origin across root page navigations.\n\n// Global handles that need to be freed on deinit\neternal_function_templates: []v8.Eternal,\n\n// Dynamic slice to avoid circular dependency on JsApis.len at comptime\ntemplates: []*const v8.FunctionTemplate,\n\n// Global template created once per isolate and reused across all contexts\nglobal_template: v8.Eternal,\n\n// Inspector associated with the Isolate. Exists when CDP is being used.\ninspector: ?*Inspector,\n\n// We can store data in a v8::Object's Private data bag. The keys are v8::Private\n// which an be created once per isolaet.\nprivate_symbols: PrivateSymbols,\n\nmicrotask_queues_are_running: bool,\n\npub const InitOpts = struct {\n    with_inspector: bool = false,\n};\n\npub fn init(app: *App, opts: InitOpts) !Env {\n    if (comptime IS_DEBUG) {\n        comptime {\n            // V8 requirement for any data using SetAlignedPointerInInternalField\n            const a = @alignOf(@import(\"TaggedOpaque.zig\"));\n            std.debug.assert(a >= 2 and a % 2 == 0);\n        }\n    }\n\n    // Initialize class IDs once before any V8 work\n    class_id_once.call();\n\n    const allocator = app.allocator;\n    const snapshot = &app.snapshot;\n\n    var params = try allocator.create(v8.CreateParams);\n    errdefer allocator.destroy(params);\n    v8.v8__Isolate__CreateParams__CONSTRUCT(params);\n    params.snapshot_blob = @ptrCast(&snapshot.startup_data);\n\n    params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;\n    errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);\n\n    params.external_references = &snapshot.external_references;\n\n    var isolate = js.Isolate.init(params);\n    errdefer isolate.deinit();\n    const isolate_handle = isolate.handle;\n\n    v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);\n    v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);\n    v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);\n    v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);\n    v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);\n\n    isolate.enter();\n    errdefer isolate.exit();\n\n    v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate_handle, Context.metaObjectCallback);\n\n    // Allocate arrays dynamically to avoid comptime dependency on JsApis.len\n    const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);\n    errdefer allocator.free(eternal_function_templates);\n\n    const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);\n    errdefer allocator.free(templates);\n\n    var global_eternal: v8.Eternal = undefined;\n    var private_symbols: PrivateSymbols = undefined;\n    {\n        var temp_scope: js.HandleScope = undefined;\n        temp_scope.init(isolate);\n        defer temp_scope.deinit();\n\n        inline for (JsApis, 0..) |_, i| {\n            const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);\n            const function_handle: *const v8.FunctionTemplate = @ptrCast(data);\n            // Make function template eternal\n            v8.v8__Eternal__New(isolate_handle, @ptrCast(function_handle), &eternal_function_templates[i]);\n\n            // Extract the local handle from the global for easy access\n            const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate_handle);\n            templates[i] = @ptrCast(@alignCast(eternal_ptr.?));\n        }\n\n        // Create global template once per isolate\n        const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);\n        const window_name = v8.v8__String__NewFromUtf8(isolate_handle, \"Window\", v8.kNormal, 6);\n        v8.v8__FunctionTemplate__SetClassName(js_global, window_name);\n\n        // Find Window in JsApis by name (avoids circular import)\n        const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);\n        v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);\n\n        const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;\n        v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{\n            .getter = bridge.unknownWindowPropertyCallback,\n            .setter = null,\n            .query = null,\n            .deleter = null,\n            .enumerator = null,\n            .definer = null,\n            .descriptor = null,\n            .data = null,\n            .flags = v8.kOnlyInterceptStrings | v8.kNonMasking,\n        });\n        // I don't 100% understand this. We actually set this up in the snapshot,\n        // but for the global instance, it doesn't work. SetIndexedHandler and\n        // SetNamedHandler are set on the Instance template, and that's the key\n        // difference. The context has its own global instance, so we need to set\n        // these back up directly on it. There might be a better way to do this.\n        v8.v8__ObjectTemplate__SetIndexedHandler(global_template_local, &.{\n            .getter = Window.JsApi.index.getter,\n            .setter = null,\n            .query = null,\n            .deleter = null,\n            .enumerator = null,\n            .definer = null,\n            .descriptor = null,\n            .data = null,\n            .flags = 0,\n        });\n        v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);\n        private_symbols = PrivateSymbols.init(isolate_handle);\n    }\n\n    var inspector: ?*js.Inspector = null;\n    if (opts.with_inspector) {\n        inspector = try Inspector.init(allocator, isolate_handle);\n    }\n\n    return .{\n        .app = app,\n        .context_id = 0,\n        .allocator = allocator,\n        .contexts = undefined,\n        .context_count = 0,\n        .isolate = isolate,\n        .platform = &app.platform,\n        .templates = templates,\n        .isolate_params = params,\n        .inspector = inspector,\n        .global_template = global_eternal,\n        .private_symbols = private_symbols,\n        .microtask_queues_are_running = false,\n        .eternal_function_templates = eternal_function_templates,\n    };\n}\n\npub fn deinit(self: *Env) void {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.context_count == 0);\n    }\n    for (self.contexts[0..self.context_count]) |ctx| {\n        ctx.deinit();\n    }\n\n    const app = self.app;\n    const allocator = app.allocator;\n\n    if (self.inspector) |i| {\n        i.deinit(allocator);\n    }\n\n    allocator.free(self.templates);\n    allocator.free(self.eternal_function_templates);\n    self.private_symbols.deinit();\n\n    self.isolate.exit();\n    self.isolate.deinit();\n    v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);\n    allocator.destroy(self.isolate_params);\n}\n\npub fn createContext(self: *Env, page: *Page) !*Context {\n    const context_arena = try self.app.arena_pool.acquire();\n    errdefer self.app.arena_pool.release(context_arena);\n\n    const isolate = self.isolate;\n    var hs: js.HandleScope = undefined;\n    hs.init(isolate);\n    defer hs.deinit();\n\n    // Create a per-context microtask queue for isolation\n    const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;\n    errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);\n\n    // Get the global template that was created once per isolate\n    const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));\n    v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));\n\n    const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{\n        .global_template = global_template,\n        .global_object = null,\n        .microtask_queue = microtask_queue,\n    }).?;\n\n    // Create the v8::Context and wrap it in a v8::Global\n    var context_global: v8.Global = undefined;\n    v8.v8__Global__New(isolate.handle, v8_context, &context_global);\n\n    // get the global object for the context, this maps to our Window\n    const global_obj = v8.v8__Context__Global(v8_context).?;\n\n    {\n        // Store our TAO inside the internal field of the global object. This\n        // maps the v8::Object -> Zig instance. Almost all objects have this, and\n        // it gets setup automatically as objects are created, but the Window\n        // object already exists in v8 (it's the global) so we manually create\n        // the mapping here.\n        const tao = try context_arena.create(@import(\"TaggedOpaque.zig\"));\n        tao.* = .{\n            .value = @ptrCast(page.window),\n            .prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,\n            .prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),\n            .subtype = .node, // this probably isn't right, but it's what we've been doing all along\n        };\n        v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);\n    }\n\n    // our window wrapped in a v8::Global\n    var global_global: v8.Global = undefined;\n    v8.v8__Global__New(isolate.handle, global_obj, &global_global);\n\n    const context_id = self.context_id;\n    self.context_id = context_id + 1;\n\n    const origin = try page._session.getOrCreateOrigin(null);\n    errdefer page._session.releaseOrigin(origin);\n\n    const context = try context_arena.create(Context);\n    context.* = .{\n        .env = self,\n        .page = page,\n        .session = page._session,\n        .origin = origin,\n        .id = context_id,\n        .isolate = isolate,\n        .arena = context_arena,\n        .handle = context_global,\n        .templates = self.templates,\n        .call_arena = page.call_arena,\n        .microtask_queue = microtask_queue,\n        .script_manager = &page._script_manager,\n        .scheduler = .init(context_arena),\n    };\n    try context.origin.identity_map.putNoClobber(origin.arena, @intFromPtr(page.window), global_global);\n\n    // Store a pointer to our context inside the v8 context so that, given\n    // a v8 context, we can get our context out\n    v8.v8__Context__SetAlignedPointerInEmbedderData(v8_context, 1, @ptrCast(context));\n\n    const count = self.context_count;\n    if (count >= self.contexts.len) {\n        return error.TooManyContexts;\n    }\n    self.contexts[count] = context;\n    self.context_count = count + 1;\n\n    return context;\n}\n\npub fn destroyContext(self: *Env, context: *Context) void {\n    for (self.contexts[0..self.context_count], 0..) |ctx, i| {\n        if (ctx == context) {\n            // Swap with last element and decrement count\n            self.context_count -= 1;\n            self.contexts[i] = self.contexts[self.context_count];\n            break;\n        }\n    } else {\n        if (comptime IS_DEBUG) {\n            @panic(\"Tried to remove unknown context\");\n        }\n    }\n\n    const isolate = self.isolate;\n    if (self.inspector) |inspector| {\n        var hs: js.HandleScope = undefined;\n        hs.init(isolate);\n        defer hs.deinit();\n        inspector.contextDestroyed(@ptrCast(v8.v8__Global__Get(&context.handle, isolate.handle)));\n    }\n\n    context.deinit();\n}\n\npub fn runMicrotasks(self: *Env) void {\n    if (self.microtask_queues_are_running == false) {\n        const v8_isolate = self.isolate.handle;\n\n        self.microtask_queues_are_running = true;\n        defer self.microtask_queues_are_running = false;\n\n        var i: usize = 0;\n        while (i < self.context_count) : (i += 1) {\n            const ctx = self.contexts[i];\n            v8.v8__MicrotaskQueue__PerformCheckpoint(ctx.microtask_queue, v8_isolate);\n        }\n    }\n}\n\npub fn runMacrotasks(self: *Env) !void {\n    for (self.contexts[0..self.context_count]) |ctx| {\n        if (comptime builtin.is_test == false) {\n            // I hate this comptime check as much as you do. But we have tests\n            // which rely on short execution before shutdown. In real world, it's\n            // underterministic whether a timer will or won't run before the\n            // page shutsdown. But for tests, we need to run them to their end.\n            if (ctx.scheduler.hasReadyTasks() == false) {\n                continue;\n            }\n        }\n\n        var hs: js.HandleScope = undefined;\n        const entered = ctx.enter(&hs);\n        defer entered.exit();\n        try ctx.scheduler.run();\n    }\n}\n\npub fn msToNextMacrotask(self: *Env) ?u64 {\n    var next_task: u64 = std.math.maxInt(u64);\n    for (self.contexts[0..self.context_count]) |ctx| {\n        const candidate = ctx.scheduler.msToNextHigh() orelse continue;\n        next_task = @min(candidate, next_task);\n    }\n    return if (next_task == std.math.maxInt(u64)) null else next_task;\n}\n\npub fn pumpMessageLoop(self: *const Env) void {\n    var hs: v8.HandleScope = undefined;\n    v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);\n    defer v8.v8__HandleScope__DESTRUCT(&hs);\n\n    const isolate = self.isolate.handle;\n    const platform = self.platform.handle;\n    while (v8.v8__Platform__PumpMessageLoop(platform, isolate, false)) {}\n}\n\npub fn hasBackgroundTasks(self: *const Env) bool {\n    return v8.v8__Isolate__HasPendingBackgroundTasks(self.isolate.handle);\n}\n\npub fn waitForBackgroundTasks(self: *Env) void {\n    var hs: v8.HandleScope = undefined;\n    v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);\n    defer v8.v8__HandleScope__DESTRUCT(&hs);\n\n    const isolate = self.isolate.handle;\n    const platform = self.platform.handle;\n    while (v8.v8__Isolate__HasPendingBackgroundTasks(isolate)) {\n        _ = v8.v8__Platform__PumpMessageLoop(platform, isolate, true);\n        self.runMicrotasks();\n    }\n}\n\npub fn runIdleTasks(self: *const Env) void {\n    v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);\n}\n\n// V8 doesn't immediately free memory associated with\n// a Context, it's managed by the garbage collector. We use the\n// `lowMemoryNotification` call on the isolate to encourage v8 to free\n// any contexts which have been freed.\n// This GC is very aggressive. Use memoryPressureNotification for less\n// aggressive GC passes.\npub fn lowMemoryNotification(self: *Env) void {\n    var handle_scope: js.HandleScope = undefined;\n    handle_scope.init(self.isolate);\n    defer handle_scope.deinit();\n    self.isolate.lowMemoryNotification();\n}\n\n// V8 doesn't immediately free memory associated with\n// a Context, it's managed by the garbage collector. We use the\n// `memoryPressureNotification` call on the isolate to encourage v8 to free\n// any contexts which have been freed.\n// The level indicates the aggressivity of the GC required:\n// moderate speeds up incremental GC\n// critical runs one full GC\n// For a more aggressive GC, use lowMemoryNotification.\npub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {\n    var handle_scope: js.HandleScope = undefined;\n    handle_scope.init(self.isolate);\n    defer handle_scope.deinit();\n    self.isolate.memoryPressureNotification(level);\n}\n\npub fn dumpMemoryStats(self: *Env) void {\n    const stats = self.isolate.getHeapStatistics();\n    std.debug.print(\n        \\\\ Total Heap Size: {d}\n        \\\\ Total Heap Size Executable: {d}\n        \\\\ Total Physical Size: {d}\n        \\\\ Total Available Size: {d}\n        \\\\ Used Heap Size: {d}\n        \\\\ Heap Size Limit: {d}\n        \\\\ Malloced Memory: {d}\n        \\\\ External Memory: {d}\n        \\\\ Peak Malloced Memory: {d}\n        \\\\ Number Of Native Contexts: {d}\n        \\\\ Number Of Detached Contexts: {d}\n        \\\\ Total Global Handles Size: {d}\n        \\\\ Used Global Handles Size: {d}\n        \\\\ Zap Garbage: {any}\n        \\\\\n    , .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });\n}\n\npub fn terminate(self: *const Env) void {\n    v8.v8__Isolate__TerminateExecution(self.isolate.handle);\n}\n\nfn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {\n    const promise_event = v8.v8__PromiseRejectMessage__GetEvent(&message_handle);\n    if (promise_event != v8.kPromiseRejectWithNoHandler and promise_event != v8.kPromiseHandlerAddedAfterReject) {\n        return;\n    }\n\n    const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;\n    const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;\n    const isolate = js.Isolate{ .handle = v8_isolate };\n    const ctx, const v8_context = Context.fromIsolate(isolate);\n\n    const local = js.Local{\n        .ctx = ctx,\n        .isolate = isolate,\n        .handle = v8_context,\n        .call_arena = ctx.call_arena,\n    };\n\n    const page = ctx.page;\n    page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{\n        .local = &local,\n        .handle = &message_handle,\n    }, page) catch |err| {\n        log.warn(.browser, \"unhandled rejection handler\", .{ .err = err });\n    };\n}\n\nfn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {\n    const location = std.mem.span(c_location);\n    const message = std.mem.span(c_message);\n    log.fatal(.app, \"V8 fatal callback\", .{ .location = location, .message = message });\n    @import(\"../../crash_handler.zig\").crash(\"Fatal V8 Error\", .{ .location = location, .message = message }, @returnAddress());\n}\n\nfn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {\n    const location = std.mem.span(c_location);\n    const detail = if (details) |d| std.mem.span(d.detail) else \"\";\n    log.fatal(.app, \"V8 OOM\", .{ .location = location, .detail = detail });\n    @import(\"../../crash_handler.zig\").crash(\"V8 OOM\", .{ .location = location, .detail = detail }, @returnAddress());\n}\n\nconst PrivateSymbols = struct {\n    const Private = @import(\"Private.zig\");\n\n    child_nodes: Private,\n\n    fn init(isolate: *v8.Isolate) PrivateSymbols {\n        return .{\n            .child_nodes = Private.init(isolate, \"child_nodes\"),\n        };\n    }\n\n    fn deinit(self: *PrivateSymbols) void {\n        self.child_nodes.deinit();\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Function.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst log = @import(\"../../log.zig\");\n\nconst Function = @This();\n\nlocal: *const js.Local,\nthis: ?*const v8.Object = null,\nhandle: *const v8.Function,\n\npub const Result = struct {\n    stack: ?[]const u8,\n    exception: []const u8,\n};\n\npub fn withThis(self: *const Function, value: anytype) !Function {\n    const local = self.local;\n    const this_obj = if (@TypeOf(value) == js.Object)\n        value.handle\n    else\n        (try local.zigValueToJs(value, .{})).handle;\n\n    return .{\n        .local = local,\n        .this = this_obj,\n        .handle = self.handle,\n    };\n}\n\npub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {\n    const local = self.local;\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(local);\n    defer try_catch.deinit();\n\n    // This creates a new instance using this Function as a constructor.\n    // const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));\n    const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {\n        caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);\n        return error.JsConstructorFailed;\n    };\n\n    return .{\n        .local = local,\n        .handle = handle,\n    };\n}\n\npub fn call(self: *const Function, comptime T: type, args: anytype) !T {\n    var caught: js.TryCatch.Caught = undefined;\n    return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {\n        log.warn(.js, \"call caught\", .{ .err = err, .caught = caught });\n        return err;\n    };\n}\n\npub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {\n    var caught: js.TryCatch.Caught = undefined;\n    return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| {\n        log.warn(.js, \"call caught\", .{ .err = err, .caught = caught });\n        return err;\n    };\n}\n\npub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {\n    var caught: js.TryCatch.Caught = undefined;\n    return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| {\n        log.warn(.js, \"callWithThis caught\", .{ .err = err, .caught = caught });\n        return err;\n    };\n}\n\npub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {\n    return self._tryCallWithThis(T, self.getThis(), args, caught, .{});\n}\n\npub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {\n    return self._tryCallWithThis(T, this, args, caught, .{});\n}\n\nconst CallOpts = struct {\n    rethrow: bool = false,\n};\nfn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T {\n    caught.* = .{};\n    const local = self.local;\n\n    // When we're calling a function from within JavaScript itself, this isn't\n    // necessary. We're within a Caller instantiation, which will already have\n    // incremented the call_depth and it won't decrement it until the Caller is\n    // done.\n    // But some JS functions are initiated from Zig code, and not v8. For\n    // example, Observers, some event and window callbacks. In those cases, we\n    // need to increase the call_depth so that the call_arena remains valid for\n    // the duration of the function call. If we don't do this, the call_arena\n    // will be reset after each statement of the function which executes Zig code.\n    const ctx = local.ctx;\n    const call_depth = ctx.call_depth;\n    ctx.call_depth = call_depth + 1;\n    defer ctx.call_depth = call_depth;\n\n    const js_this = blk: {\n        if (@TypeOf(this) == js.Object) {\n            break :blk this;\n        }\n        break :blk try local.zigValueToJs(this, .{});\n    };\n\n    const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;\n\n    const js_args: []const *const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {\n        .@\"struct\" => |s| blk: {\n            const fields = s.fields;\n            var js_args: [fields.len]*const v8.Value = undefined;\n            inline for (fields, 0..) |f, i| {\n                js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;\n            }\n            const cargs: [fields.len]*const v8.Value = js_args;\n            break :blk &cargs;\n        },\n        .pointer => blk: {\n            var values = try local.call_arena.alloc(*const v8.Value, args.len);\n            for (args, 0..) |a, i| {\n                values[i] = (try local.zigValueToJs(a, .{})).handle;\n            }\n            break :blk values;\n        },\n        else => @compileError(\"JS Function called with invalid paremter type\"),\n    };\n\n    const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(local);\n    defer try_catch.deinit();\n\n    const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {\n        if ((comptime opts.rethrow) and try_catch.hasCaught()) {\n            try_catch.rethrow();\n            return error.TryCatchRethrow;\n        }\n        caught.* = try_catch.caughtOrError(local.call_arena, error.JsException);\n        return error.JsException;\n    };\n\n    if (@typeInfo(T) == .void) {\n        return {};\n    }\n    return local.jsValueToZig(T, .{ .local = local, .handle = handle });\n}\n\nfn getThis(self: *const Function) js.Object {\n    const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;\n    return .{\n        .local = self.local,\n        .handle = handle,\n    };\n}\n\npub fn src(self: *const Function) ![]const u8 {\n    return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});\n}\n\npub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {\n    const local = self.local;\n    const key = local.isolate.initStringHandle(name);\n    const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {\n        return error.JsException;\n    };\n\n    return .{\n        .local = local,\n        .handle = handle,\n    };\n}\n\npub fn persist(self: *const Function) !Global {\n    return self._persist(true);\n}\n\npub fn temp(self: *const Function) !Temp {\n    return self._persist(false);\n}\n\nfn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) {\n    var ctx = self.local.ctx;\n\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n    if (comptime is_global) {\n        try ctx.trackGlobal(global);\n        return .{ .handle = global, .origin = {} };\n    }\n    try ctx.trackTemp(global);\n    return .{ .handle = global, .origin = ctx.origin };\n}\n\npub fn tempWithThis(self: *const Function, value: anytype) !Temp {\n    const with_this = try self.withThis(value);\n    return with_this.temp();\n}\n\npub fn persistWithThis(self: *const Function, value: anytype) !Global {\n    const with_this = try self.withThis(value);\n    return with_this.persist();\n}\n\npub const Temp = G(.temp);\npub const Global = G(.global);\n\nconst GlobalType = enum(u8) {\n    temp,\n    global,\n};\n\nfn G(comptime global_type: GlobalType) type {\n    return struct {\n        handle: v8.Global,\n        origin: if (global_type == .temp) *js.Origin else void,\n\n        const Self = @This();\n\n        pub fn deinit(self: *Self) void {\n            v8.v8__Global__Reset(&self.handle);\n        }\n\n        pub fn local(self: *const Self, l: *const js.Local) Function {\n            return .{\n                .local = l,\n                .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),\n            };\n        }\n\n        pub fn isEqual(self: *const Self, other: Function) bool {\n            return v8.v8__Global__IsEqual(&self.handle, other.handle);\n        }\n\n        pub fn release(self: *const Self) void {\n            self.origin.releaseTemp(self.handle);\n        }\n    };\n}\n"
  },
  {
    "path": "src/browser/js/HandleScope.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst HandleScope = @This();\n\nhandle: v8.HandleScope,\n\n// V8 takes an address of the value that's passed in, so it needs to be stable.\n// We can't create the v8.HandleScope here, pass it to v8 and then return the\n// value, as v8 will then have taken the address of the function-scopped (and no\n// longer valid) local.\npub fn init(self: *HandleScope, isolate: js.Isolate) void {\n    self.initWithIsolateHandle(isolate.handle);\n}\n\npub fn initWithIsolateHandle(self: *HandleScope, isolate: *v8.Isolate) void {\n    v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate);\n}\n\npub fn deinit(self: *HandleScope) void {\n    v8.v8__HandleScope__DESTRUCT(&self.handle);\n}\n"
  },
  {
    "path": "src/browser/js/Inspector.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst TaggedOpaque = @import(\"TaggedOpaque.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst CONTEXT_GROUP_ID = 1;\nconst CLIENT_TRUST_LEVEL = 1;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\n// Inspector exists for the lifetime of the Isolate/Env. 1 Isolate = 1 Inspector.\n// It combines the v8.Inspector and the v8.InspectorClientImpl. The v8.InspectorClientImpl\n// is our own implementation that fulfills the InspectorClient API, i.e. it's the\n// mechanism v8 provides to let us tweak how the inspector works. For example, it\n// Below, you'll find a few pub export fn v8_inspector__Client__IMPL__XYZ functions\n// which is our implementation of what the v8::Inspector requires of our Client\n// (not much at all)\nconst Inspector = @This();\n\nunique_id: i64,\nisolate: *v8.Isolate,\nhandle: *v8.Inspector,\nclient: *v8.InspectorClientImpl,\ndefault_context: ?v8.Global,\nsession: ?Session,\n\npub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {\n    const self = try allocator.create(Inspector);\n    errdefer allocator.destroy(self);\n\n    self.* = .{\n        .unique_id = 1,\n        .session = null,\n        .isolate = isolate,\n        .client = undefined,\n        .handle = undefined,\n        .default_context = null,\n    };\n\n    self.client = v8.v8_inspector__Client__IMPL__CREATE();\n    errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);\n    v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);\n\n    self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;\n    errdefer v8.v8_inspector__Inspector__DELETE(self.handle);\n\n    return self;\n}\n\npub fn deinit(self: *const Inspector, allocator: Allocator) void {\n    var hs: v8.HandleScope = undefined;\n    v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);\n    defer v8.v8__HandleScope__DESTRUCT(&hs);\n\n    if (self.session) |*s| {\n        s.deinit();\n    }\n    v8.v8_inspector__Client__IMPL__DELETE(self.client);\n    v8.v8_inspector__Inspector__DELETE(self.handle);\n    allocator.destroy(self);\n}\n\npub fn startSession(self: *Inspector, ctx: anytype) *Session {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.session == null);\n    }\n\n    self.session = @as(Session, undefined);\n    Session.init(&self.session.?, self, ctx);\n    return &self.session.?;\n}\n\npub fn stopSession(self: *Inspector) void {\n    self.session.?.deinit();\n    self.session = null;\n}\n\n// From CDP docs\n// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription\n// ----\n// - name: Human readable name describing given context.\n// - origin: Execution context origin (ie. URL who initialised the request)\n// - auxData: Embedder-specific auxiliary data likely matching\n// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}\n// - is_default_context: Whether the execution context is default, should match the auxData\npub fn contextCreated(\n    self: *Inspector,\n    local: *const js.Local,\n    name: []const u8,\n    origin: []const u8,\n    aux_data: []const u8,\n    is_default_context: bool,\n) void {\n    v8.v8_inspector__Inspector__ContextCreated(\n        self.handle,\n        name.ptr,\n        name.len,\n        origin.ptr,\n        origin.len,\n        aux_data.ptr,\n        aux_data.len,\n        CONTEXT_GROUP_ID,\n        local.handle,\n    );\n\n    if (is_default_context) {\n        self.default_context = local.ctx.handle;\n    }\n}\n\npub fn contextDestroyed(self: *Inspector, context: *const v8.Context) void {\n    v8.v8_inspector__Inspector__ContextDestroyed(self.handle, context);\n\n    if (self.default_context) |*dc| {\n        if (v8.v8__Global__IsEqual(dc, context)) {\n            self.default_context = null;\n        }\n    }\n}\n\npub fn resetContextGroup(self: *const Inspector) void {\n    var hs: v8.HandleScope = undefined;\n    v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);\n    defer v8.v8__HandleScope__DESTRUCT(&hs);\n\n    v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);\n}\n\npub const RemoteObject = struct {\n    handle: *v8.RemoteObject,\n\n    pub fn deinit(self: RemoteObject) void {\n        v8.v8_inspector__RemoteObject__DELETE(self.handle);\n    }\n\n    pub fn getType(self: RemoteObject, allocator: Allocator) ![]const u8 {\n        var ctype_: v8.CZigString = .{ .ptr = null, .len = 0 };\n        if (!v8.v8_inspector__RemoteObject__getType(self.handle, &allocator, &ctype_)) return error.V8AllocFailed;\n        return cZigStringToString(ctype_) orelse return error.InvalidType;\n    }\n\n    pub fn getSubtype(self: RemoteObject, allocator: Allocator) !?[]const u8 {\n        if (!v8.v8_inspector__RemoteObject__hasSubtype(self.handle)) return null;\n\n        var csubtype: v8.CZigString = .{ .ptr = null, .len = 0 };\n        if (!v8.v8_inspector__RemoteObject__getSubtype(self.handle, &allocator, &csubtype)) return error.V8AllocFailed;\n        return cZigStringToString(csubtype);\n    }\n\n    pub fn getClassName(self: RemoteObject, allocator: Allocator) !?[]const u8 {\n        if (!v8.v8_inspector__RemoteObject__hasClassName(self.handle)) return null;\n\n        var cclass_name: v8.CZigString = .{ .ptr = null, .len = 0 };\n        if (!v8.v8_inspector__RemoteObject__getClassName(self.handle, &allocator, &cclass_name)) return error.V8AllocFailed;\n        return cZigStringToString(cclass_name);\n    }\n\n    pub fn getDescription(self: RemoteObject, allocator: Allocator) !?[]const u8 {\n        if (!v8.v8_inspector__RemoteObject__hasDescription(self.handle)) return null;\n\n        var description: v8.CZigString = .{ .ptr = null, .len = 0 };\n        if (!v8.v8_inspector__RemoteObject__getDescription(self.handle, &allocator, &description)) return error.V8AllocFailed;\n        return cZigStringToString(description);\n    }\n\n    pub fn getObjectId(self: RemoteObject, allocator: Allocator) !?[]const u8 {\n        if (!v8.v8_inspector__RemoteObject__hasObjectId(self.handle)) return null;\n\n        var cobject_id: v8.CZigString = .{ .ptr = null, .len = 0 };\n        if (!v8.v8_inspector__RemoteObject__getObjectId(self.handle, &allocator, &cobject_id)) return error.V8AllocFailed;\n        return cZigStringToString(cobject_id);\n    }\n};\n\n// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The\n// InspectorSession is for zig -> v8 (sending messages to the inspector). The\n// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass\n// back ot some opaque context, i.e the CDP BrowserContext).\n// The channel callbacks are defined below, as:\n//   pub export fn v8_inspector__Channel__IMPL__XYZ\npub const Session = struct {\n    inspector: *Inspector,\n    handle: *v8.InspectorSession,\n    channel: *v8.InspectorChannelImpl,\n\n    // callbacks\n    ctx: *anyopaque,\n    onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,\n    onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,\n\n    fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {\n        const Container = @typeInfo(@TypeOf(ctx)).pointer.child;\n\n        const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);\n        const handle = v8.v8_inspector__Inspector__Connect(\n            inspector.handle,\n            CONTEXT_GROUP_ID,\n            channel,\n            CLIENT_TRUST_LEVEL,\n        ).?;\n        v8.v8_inspector__Channel__IMPL__SET_DATA(channel, self);\n\n        self.* = .{\n            .ctx = ctx,\n            .handle = handle,\n            .channel = channel,\n            .inspector = inspector,\n            .onResp = Container.onInspectorResponse,\n            .onNotif = Container.onInspectorEvent,\n        };\n    }\n\n    fn deinit(self: *const Session) void {\n        v8.v8_inspector__Session__DELETE(self.handle);\n        v8.v8_inspector__Channel__IMPL__DELETE(self.channel);\n    }\n\n    pub fn send(self: *const Session, msg: []const u8) void {\n        const isolate = self.inspector.isolate;\n        var hs: v8.HandleScope = undefined;\n        v8.v8__HandleScope__CONSTRUCT(&hs, isolate);\n        defer v8.v8__HandleScope__DESTRUCT(&hs);\n\n        v8.v8_inspector__Session__dispatchProtocolMessage(\n            self.handle,\n            isolate,\n            msg.ptr,\n            msg.len,\n        );\n    }\n\n    // Gets a value by object ID regardless of which context it is in.\n    // Our TaggedOpaque stores the \"resolved\" ptr value (the most specific _type,\n    // e.g. we store the ptr to the Div not the EventTarget). But, this is asking for\n    // the pointer to the Node, so we need to use the same resolution mechanism which\n    // is used when we're calling a function to turn the Div into a Node, which is\n    // what TaggedOpaque.fromJS does.\n    pub fn getNodePtr(self: *const Session, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {\n        // just to indicate that the caller is responsible for ensuring there's a local environment\n        _ = local;\n\n        const unwrapped = try self.unwrapObject(allocator, object_id);\n        // The values context and groupId are not used here\n        const js_val = unwrapped.value;\n        if (!v8.v8__Value__IsObject(js_val)) {\n            return error.ObjectIdIsNotANode;\n        }\n\n        const Node = @import(\"../webapi/Node.zig\");\n        // Cast to *const v8.Object for typeTaggedAnyOpaque\n        return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;\n    }\n\n    // Retrieves the RemoteObject for a given value.\n    // The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,\n    // just like a method return value. Therefore, if we've mapped this\n    // value before, we'll get the existing js.Global(js.Object) and if not\n    // we'll create it and track it for cleanup when the context ends.\n    pub fn getRemoteObject(\n        self: *const Session,\n        local: *const js.Local,\n        group: []const u8,\n        value: anytype,\n    ) !RemoteObject {\n        const js_val = try local.zigValueToJs(value, .{});\n\n        // We do not want to expose this as a parameter for now\n        const generate_preview = false;\n        return self.wrapObject(\n            local.isolate.handle,\n            local.handle,\n            js_val.handle,\n            group,\n            generate_preview,\n        );\n    }\n\n    fn wrapObject(\n        self: Session,\n        isolate: *v8.Isolate,\n        ctx: *const v8.Context,\n        val: *const v8.Value,\n        grpname: []const u8,\n        generatepreview: bool,\n    ) !RemoteObject {\n        const remote_object = v8.v8_inspector__Session__wrapObject(\n            self.handle,\n            isolate,\n            ctx,\n            val,\n            grpname.ptr,\n            grpname.len,\n            generatepreview,\n        ).?;\n        return .{ .handle = remote_object };\n    }\n\n    fn unwrapObject(\n        self: Session,\n        allocator: Allocator,\n        object_id: []const u8,\n    ) !UnwrappedObject {\n        const in_object_id = v8.CZigString{\n            .ptr = object_id.ptr,\n            .len = object_id.len,\n        };\n        var out_error: v8.CZigString = .{ .ptr = null, .len = 0 };\n        var out_value_handle: ?*v8.Value = null;\n        var out_context_handle: ?*v8.Context = null;\n        var out_object_group: v8.CZigString = .{ .ptr = null, .len = 0 };\n\n        const result = v8.v8_inspector__Session__unwrapObject(\n            self.handle,\n            &allocator,\n            &out_error,\n            in_object_id,\n            &out_value_handle,\n            &out_context_handle,\n            &out_object_group,\n        );\n\n        if (!result) {\n            const error_str = cZigStringToString(out_error) orelse return error.UnwrapFailed;\n            std.log.err(\"unwrapObject failed: {s}\", .{error_str});\n            return error.UnwrapFailed;\n        }\n\n        return .{\n            .value = out_value_handle.?,\n            .context = out_context_handle.?,\n            .object_group = cZigStringToString(out_object_group),\n        };\n    }\n};\n\nconst UnwrappedObject = struct {\n    value: *const v8.Value,\n    context: *const v8.Context,\n    object_group: ?[]const u8,\n};\n\npub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {\n    if (!v8.v8__Value__IsObject(value)) {\n        return null;\n    }\n    const internal_field_count = v8.v8__Object__InternalFieldCount(value);\n    if (internal_field_count == 0) {\n        return null;\n    }\n\n    const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(value, 0).?;\n    return @ptrCast(@alignCast(tao_ptr));\n}\n\nfn cZigStringToString(s: v8.CZigString) ?[]const u8 {\n    if (s.ptr == null) return null;\n    return s.ptr[0..s.len];\n}\n\n// C export functions for Inspector callbacks\npub export fn v8_inspector__Client__IMPL__generateUniqueId(\n    _: *v8.InspectorClientImpl,\n    data: *anyopaque,\n) callconv(.c) i64 {\n    const inspector: *Inspector = @ptrCast(@alignCast(data));\n    const unique_id = inspector.unique_id + 1;\n    inspector.unique_id = unique_id;\n    return unique_id;\n}\n\npub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(\n    _: *v8.InspectorClientImpl,\n    data: *anyopaque,\n    context_group_id: c_int,\n) callconv(.c) void {\n    _ = data;\n    _ = context_group_id;\n}\n\npub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(\n    _: *v8.InspectorClientImpl,\n    data: *anyopaque,\n) callconv(.c) void {\n    _ = data;\n}\n\npub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(\n    _: *v8.InspectorClientImpl,\n    _: *anyopaque,\n    _: c_int,\n) callconv(.c) void {\n    // TODO\n}\n\npub export fn v8_inspector__Client__IMPL__consoleAPIMessage(\n    _: *v8.InspectorClientImpl,\n    _: *anyopaque,\n    _: c_int,\n    _: v8.MessageErrorLevel,\n    _: *v8.StringView,\n    _: *v8.StringView,\n    _: c_uint,\n    _: c_uint,\n    _: *v8.StackTrace,\n) callconv(.c) void {}\n\npub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(\n    _: *v8.InspectorClientImpl,\n    data: *anyopaque,\n) callconv(.c) ?*const v8.Context {\n    const inspector: *Inspector = @ptrCast(@alignCast(data));\n    const global_handle = inspector.default_context orelse return null;\n    return v8.v8__Global__Get(&global_handle, inspector.isolate);\n}\n\npub export fn v8_inspector__Channel__IMPL__sendResponse(\n    _: *v8.InspectorChannelImpl,\n    data: *anyopaque,\n    call_id: c_int,\n    msg: [*c]u8,\n    length: usize,\n) callconv(.c) void {\n    const session: *Session = @ptrCast(@alignCast(data));\n    session.onResp(session.ctx, @intCast(call_id), msg[0..length]);\n}\n\npub export fn v8_inspector__Channel__IMPL__sendNotification(\n    _: *v8.InspectorChannelImpl,\n    data: *anyopaque,\n    msg: [*c]u8,\n    length: usize,\n) callconv(.c) void {\n    const session: *Session = @ptrCast(@alignCast(data));\n    session.onNotif(session.ctx, msg[0..length]);\n}\n\npub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(\n    _: *v8.InspectorChannelImpl,\n    _: *anyopaque,\n) callconv(.c) void {\n    // TODO\n}\n"
  },
  {
    "path": "src/browser/js/Integer.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\n\nconst v8 = js.v8;\n\nconst Integer = @This();\n\nhandle: *const v8.Integer,\n\npub fn init(isolate: *v8.Isolate, value: anytype) Integer {\n    const handle = switch (@TypeOf(value)) {\n        i8, i16, i32 => v8.v8__Integer__New(isolate, value).?,\n        u8, u16, u32 => v8.v8__Integer__NewFromUnsigned(isolate, value).?,\n        else => |T| @compileError(\"cannot create v8::Integer from: \" ++ @typeName(T)),\n    };\n    return .{ .handle = handle };\n}\n"
  },
  {
    "path": "src/browser/js/Isolate.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst Isolate = @This();\n\nhandle: *v8.Isolate,\n\npub fn init(params: *v8.CreateParams) Isolate {\n    return .{\n        .handle = v8.v8__Isolate__New(params).?,\n    };\n}\n\npub fn deinit(self: Isolate) void {\n    v8.v8__Isolate__Dispose(self.handle);\n}\n\npub fn enter(self: Isolate) void {\n    v8.v8__Isolate__Enter(self.handle);\n}\n\npub fn exit(self: Isolate) void {\n    v8.v8__Isolate__Exit(self.handle);\n}\n\npub fn lowMemoryNotification(self: Isolate) void {\n    v8.v8__Isolate__LowMemoryNotification(self.handle);\n}\n\npub const MemoryPressureLevel = enum(u32) {\n    none = v8.kNone,\n    moderate = v8.kModerate,\n    critical = v8.kCritical,\n};\n\npub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void {\n    v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level));\n}\n\npub fn notifyContextDisposed(self: Isolate) void {\n    _ = v8.v8__Isolate__ContextDisposedNotification(self.handle);\n}\n\npub fn getHeapStatistics(self: Isolate) v8.HeapStatistics {\n    var res: v8.HeapStatistics = undefined;\n    v8.v8__Isolate__GetHeapStatistics(self.handle, &res);\n    return res;\n}\n\npub fn throwException(self: Isolate, value: *const v8.Value) *const v8.Value {\n    return v8.v8__Isolate__ThrowException(self.handle, value).?;\n}\n\npub fn initStringHandle(self: Isolate, str: []const u8) *const v8.String {\n    return v8.v8__String__NewFromUtf8(self.handle, str.ptr, v8.kNormal, @as(c_int, @intCast(str.len))).?;\n}\n\npub fn createError(self: Isolate, msg: []const u8) *const v8.Value {\n    const message = self.initStringHandle(msg);\n    return v8.v8__Exception__Error(message).?;\n}\n\npub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {\n    const message = self.initStringHandle(msg);\n    return v8.v8__Exception__TypeError(message).?;\n}\n\npub fn initNull(self: Isolate) *const v8.Value {\n    return v8.v8__Null(self.handle).?;\n}\n\npub fn initUndefined(self: Isolate) *const v8.Value {\n    return v8.v8__Undefined(self.handle).?;\n}\n\npub fn initFalse(self: Isolate) *const v8.Value {\n    return v8.v8__False(self.handle).?;\n}\n\npub fn initTrue(self: Isolate) *const v8.Value {\n    return v8.v8__True(self.handle).?;\n}\n\npub fn initInteger(self: Isolate, val: anytype) js.Integer {\n    return js.Integer.init(self.handle, val);\n}\n\npub fn initBigInt(self: Isolate, val: anytype) js.BigInt {\n    return js.BigInt.init(self.handle, val);\n}\n\npub fn initNumber(self: Isolate, val: anytype) js.Number {\n    return js.Number.init(self.handle, val);\n}\n\npub fn createExternal(self: Isolate, val: *anyopaque) *const v8.External {\n    return v8.v8__External__New(self.handle, val).?;\n}\n"
  },
  {
    "path": "src/browser/js/Local.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\nconst log = @import(\"../../log.zig\");\nconst string = @import(\"../../string.zig\");\n\nconst js = @import(\"js.zig\");\nconst bridge = @import(\"bridge.zig\");\nconst Caller = @import(\"Caller.zig\");\nconst Context = @import(\"Context.zig\");\nconst Isolate = @import(\"Isolate.zig\");\nconst TaggedOpaque = @import(\"TaggedOpaque.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst v8 = js.v8;\nconst CallOpts = Caller.CallOpts;\nconst Allocator = std.mem.Allocator;\n\n// Where js.Context has a lifetime tied to the page, and holds the\n// v8::Global<v8::Context>, this has a much shorter lifetime and holds a\n// v8::Local<v8::Context>. In V8, you need a Local<v8::Context> or get anything\n// done, but the local only exists for the lifetime of the HandleScope it was\n// created on. When V8 calls into Zig, things are pretty straightforward, since\n// that callback gives us the currenty-entered V8::Local<Context>. But when Zig\n// has to call into V8, it's a bit more messy.\n// As a general rule, think of it this way:\n// 1 - Caller.zig is for V8 -> Zig\n// 2 - Context.zig is for Zig -> V8\n// The Local is encapsulates the data and logic they both need. It just happens\n// that it's easier to use Local from Caller than from Context.\nconst Local = @This();\n\nctx: *Context,\nhandle: *const v8.Context,\n\n// available on ctx, but accessed often, so pushed into the Local\nisolate: Isolate,\ncall_arena: std.mem.Allocator,\n\npub fn newString(self: *const Local, str: []const u8) js.String {\n    return .{\n        .local = self,\n        .handle = self.isolate.initStringHandle(str),\n    };\n}\n\npub fn newObject(self: *const Local) js.Object {\n    return .{\n        .local = self,\n        .handle = v8.v8__Object__New(self.isolate.handle).?,\n    };\n}\n\npub fn newArray(self: *const Local, len: u32) js.Array {\n    return .{\n        .local = self,\n        .handle = v8.v8__Array__New(self.isolate.handle, @intCast(len)).?,\n    };\n}\n\n/// Creates a new typed array. Memory is owned by JS context.\n/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays\npub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, size: usize) js.ArrayBufferRef(array_type) {\n    return .init(self, size);\n}\n\npub fn newCallback(\n    self: *const Local,\n    callback: anytype,\n    data: anytype,\n) js.Function {\n    const external = self.isolate.createExternal(data);\n    const handle = v8.v8__Function__New__DEFAULT2(self.handle, struct {\n        fn wrap(info_handle: ?*const js.v8.FunctionCallbackInfo) callconv(.c) void {\n            Caller.Function.call(@TypeOf(data), info_handle.?, callback, .{ .embedded_receiver = true });\n        }\n    }.wrap, @ptrCast(external)).?;\n    return .{ .local = self, .handle = handle };\n}\n\npub fn runMacrotasks(self: *const Local) void {\n    const env = self.ctx.env;\n    env.pumpMessageLoop();\n    env.runMicrotasks(); // macrotasks can cause microtasks to queue\n}\n\npub fn runMicrotasks(self: *const Local) void {\n    self.ctx.env.runMicrotasks();\n}\n\n// == Executors ==\npub fn eval(self: *const Local, src: []const u8, name: ?[]const u8) !void {\n    _ = try self.exec(src, name);\n}\n\npub fn exec(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value {\n    return self.compileAndRun(src, name);\n}\n\n/// Compiles a function body as function.\n///\n/// https://v8.github.io/api/head/classv8_1_1ScriptCompiler.html#a3a15bb5a7dfc3f998e6ac789e6b4646a\npub fn compileFunction(\n    self: *const Local,\n    function_body: []const u8,\n    /// We tend to know how many params we'll pass; can remove the comptime if necessary.\n    comptime parameter_names: []const []const u8,\n    extensions: []const v8.Object,\n) !js.Function {\n    // TODO: Make configurable.\n    const script_name = self.isolate.initStringHandle(\"anonymous\");\n    const script_source = self.isolate.initStringHandle(function_body);\n\n    var parameter_list: [parameter_names.len]*const v8.String = undefined;\n    inline for (0..parameter_names.len) |i| {\n        parameter_list[i] = self.isolate.initStringHandle(parameter_names[i]);\n    }\n\n    // Create `ScriptOrigin`.\n    var origin: v8.ScriptOrigin = undefined;\n    v8.v8__ScriptOrigin__CONSTRUCT(&origin, script_name);\n\n    // Create `ScriptCompilerSource`.\n    var script_compiler_source: v8.ScriptCompilerSource = undefined;\n    v8.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_compiler_source);\n    defer v8.v8__ScriptCompiler__Source__DESTRUCT(&script_compiler_source);\n\n    // Compile the function.\n    const result = v8.v8__ScriptCompiler__CompileFunction(\n        self.handle,\n        &script_compiler_source,\n        parameter_list.len,\n        &parameter_list,\n        extensions.len,\n        @ptrCast(&extensions),\n        v8.kNoCompileOptions,\n        v8.kNoCacheNoReason,\n    ) orelse return error.CompilationError;\n\n    return .{ .local = self, .handle = result };\n}\n\npub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js.Value {\n    const script_name = self.isolate.initStringHandle(name orelse \"anonymous\");\n    const script_source = self.isolate.initStringHandle(src);\n\n    // Create ScriptOrigin\n    var origin: v8.ScriptOrigin = undefined;\n    v8.v8__ScriptOrigin__CONSTRUCT(&origin, @ptrCast(script_name));\n\n    // Create ScriptCompilerSource\n    var script_comp_source: v8.ScriptCompilerSource = undefined;\n    v8.v8__ScriptCompiler__Source__CONSTRUCT2(script_source, &origin, null, &script_comp_source);\n    defer v8.v8__ScriptCompiler__Source__DESTRUCT(&script_comp_source);\n\n    // Compile the script\n    const v8_script = v8.v8__ScriptCompiler__Compile(\n        self.handle,\n        &script_comp_source,\n        v8.kNoCompileOptions,\n        v8.kNoCacheNoReason,\n    ) orelse return error.CompilationError;\n\n    // Run the script\n    const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.JsException;\n    return .{ .local = self, .handle = result };\n}\n\n// == Zig -> JS ==\n\n// To turn a Zig instance into a v8 object, we need to do a number of things.\n// First, if it's a struct, we need to put it on the heap.\n// Second, if we've already returned this instance, we should return\n// the same object. Hence, our executor maintains a map of Zig objects\n// to v8.Global(js.Object) (the \"identity_map\").\n// Finally, if this is the first time we've seen this instance, we need to:\n//  1 - get the FunctionTemplate (from our templates slice)\n//  2 - Create the TaggedAnyOpaque so that, if needed, we can do the reverse\n//      (i.e. js -> zig)\n//  3 - Create a v8.Global(js.Object) (because Zig owns this object, not v8)\n//  4 - Store our TaggedAnyOpaque into the persistent object\n//  5 - Update our identity_map (so that, if we return this same instance again,\n//      we can just grab it from the identity_map)\npub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {\n    const ctx = self.ctx;\n    const origin_arena = ctx.origin.arena;\n\n    const T = @TypeOf(value);\n    switch (@typeInfo(T)) {\n        .@\"struct\" => {\n            // Struct, has to be placed on the heap\n            const heap = try origin_arena.create(T);\n            heap.* = value;\n            return self.mapZigInstanceToJs(js_obj_handle, heap);\n        },\n        .pointer => |ptr| {\n            const resolved = resolveValue(value);\n\n            const gop = try ctx.origin.addIdentity(@intFromPtr(resolved.ptr));\n            if (gop.found_existing) {\n                // we've seen this instance before, return the same object\n                return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);\n            }\n\n            const isolate = self.isolate;\n            const JsApi = bridge.Struct(ptr.child).JsApi;\n\n            // Sometimes we're creating a new Object, like when\n            // we're returning a value from a function. In those cases\n            // we have to get the object template, and we can get an object\n            // by calling initInstance its InstanceTemplate.\n            // Sometimes though we already have the Object to bind to\n            // for example, when we're executing a constructor, v8 has\n            // already created the \"this\" object.\n            const js_obj = js.Object{\n                .local = self,\n                .handle = js_obj_handle orelse blk: {\n                    const function_template_handle = ctx.templates[resolved.class_id];\n                    const object_template_handle = v8.v8__FunctionTemplate__InstanceTemplate(function_template_handle).?;\n                    break :blk v8.v8__ObjectTemplate__NewInstance(object_template_handle, self.handle).?;\n                },\n            };\n\n            if (!@hasDecl(JsApi.Meta, \"empty_with_no_proto\")) {\n                // The TAO contains the pointer to our Zig instance as\n                // well as any meta data we'll need to use it later.\n                // See the TaggedOpaque struct for more details.\n                const tao = try origin_arena.create(TaggedOpaque);\n                tao.* = .{\n                    .value = resolved.ptr,\n                    .prototype_chain = resolved.prototype_chain.ptr,\n                    .prototype_len = @intCast(resolved.prototype_chain.len),\n                    .subtype = if (@hasDecl(JsApi.Meta, \"subtype\")) JsApi.Meta.subype else .node,\n                };\n\n                v8.v8__Object__SetAlignedPointerInInternalField(js_obj.handle, 0, tao);\n            } else {\n                // If the struct is empty, we don't need to do all\n                // the TOA stuff and setting the internal data.\n                // When we try to map this from JS->Zig, in\n                // TaggedOpaque, we'll also know there that\n                // the type is empty and can create an empty instance.\n            }\n\n            // dont' use js_obj.persist(), because we don't want to track this in\n            // context.global_objects, we want to track it in context.identity_map.\n            v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);\n            if (@hasDecl(JsApi.Meta, \"finalizer\")) {\n                // It would be great if resolved knew the resolved type, but I\n                // can't figure out how to make that work, since it depends on\n                // the [runtime] `value`.\n                // We need the resolved finalizer, which we have in resolved.\n                //\n                // The above if statement would be more clear as:\n                //    if (resolved.finalizer_from_v8) |finalizer| {\n                // But that's a runtime check.\n                // Instead, we check if the base has finalizer. The assumption\n                // here is that if a resolve type has a finalizer, then the base\n                // should have a finalizer too.\n                const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);\n                {\n                    errdefer fc.deinit();\n                    try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc);\n                }\n\n                conditionallyReference(value);\n                if (@hasDecl(JsApi.Meta, \"weak\")) {\n                    if (comptime IS_DEBUG) {\n                        std.debug.assert(JsApi.Meta.weak == true);\n                    }\n                    v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter);\n                }\n            }\n            return js_obj;\n        },\n        else => @compileError(\"Expected a struct or pointer, got \" ++ @typeName(T) ++ \" (constructors must return struct or pointers)\"),\n    }\n}\n\npub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts) !js.Value {\n    const isolate = self.isolate;\n\n    // Check if it's a \"simple\" type. This is extracted so that it can be\n    // reused by other parts of the code. \"simple\" types only require an\n    // isolate to create (specifically, they don't our templates array)\n    if (js.simpleZigValueToJs(isolate, value, false, opts.null_as_undefined)) |js_value_handle| {\n        return .{ .local = self, .handle = js_value_handle };\n    }\n\n    const T = @TypeOf(value);\n    switch (@typeInfo(T)) {\n        .void, .bool, .int, .comptime_int, .float, .comptime_float, .@\"enum\", .null => {\n            // Need to do this to keep the compiler happy\n            // simpleZigValueToJs handles all of these cases.\n            unreachable;\n        },\n        .array => {\n            var js_arr = self.newArray(value.len);\n            for (value, 0..) |v, i| {\n                if (try js_arr.set(@intCast(i), v, opts) == false) {\n                    return error.FailedToCreateArray;\n                }\n            }\n            return js_arr.toValue();\n        },\n        .pointer => |ptr| switch (ptr.size) {\n            .one => {\n                if (@typeInfo(ptr.child) == .@\"struct\" and @hasDecl(ptr.child, \"JsApi\")) {\n                    if (bridge.JsApiLookup.has(ptr.child.JsApi)) {\n                        const js_obj = try self.mapZigInstanceToJs(null, value);\n                        return js_obj.toValue();\n                    }\n                }\n\n                if (@typeInfo(ptr.child) == .@\"struct\" and @hasDecl(ptr.child, \"runtimeGenericWrap\")) {\n                    const wrap = try value.runtimeGenericWrap(self.ctx.page);\n                    return self.zigValueToJs(wrap, opts);\n                }\n\n                const one_info = @typeInfo(ptr.child);\n                if (one_info == .array and one_info.array.child == u8) {\n                    // Need to do this to keep the compiler happy\n                    // If this was the case, simpleZigValueToJs would\n                    // have handled it\n                    unreachable;\n                }\n            },\n            .slice => {\n                if (ptr.child == u8) {\n                    // Need to do this to keep the compiler happy\n                    // If this was the case, simpleZigValueToJs would\n                    // have handled it\n                    unreachable;\n                }\n                var js_arr = self.newArray(@intCast(value.len));\n                for (value, 0..) |v, i| {\n                    if (try js_arr.set(@intCast(i), v, opts) == false) {\n                        return error.FailedToCreateArray;\n                    }\n                }\n                return js_arr.toValue();\n            },\n            else => {},\n        },\n        .@\"struct\" => |s| {\n            if (@hasDecl(T, \"JsApi\")) {\n                if (bridge.JsApiLookup.has(T.JsApi)) {\n                    const js_obj = try self.mapZigInstanceToJs(null, value);\n                    return js_obj.toValue();\n                }\n            }\n            if (T == string.String or T == string.Global) {\n                // would have been handled by simpleZigValueToJs\n                unreachable;\n            }\n\n            // zig fmt: off\n            switch (T) {\n                js.Value => return value,\n                js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },\n\n                js.ArrayBufferRef(.int8).Global, js.ArrayBufferRef(.uint8).Global,\n                js.ArrayBufferRef(.uint8_clamped).Global, js.ArrayBufferRef(.int16).Global,\n                js.ArrayBufferRef(.uint16).Global, js.ArrayBufferRef(.int32).Global,\n                js.ArrayBufferRef(.uint32).Global, js.ArrayBufferRef(.float16).Global,\n                js.ArrayBufferRef(.float32).Global, js.ArrayBufferRef(.float64).Global,\n                => {\n                    return .{ .local = self, .handle = value.local(self).handle };\n                },\n\n                inline\n                js.Array,\n                js.Function,\n                js.Object,\n                js.Promise,\n                js.String => return .{ .local = self, .handle = @ptrCast(value.handle) },\n\n                inline\n                js.Function.Global,\n                js.Function.Temp,\n                js.Value.Global,\n                js.Value.Temp,\n                js.Object.Global,\n                js.Promise.Global,\n                js.Promise.Temp,\n                js.PromiseResolver.Global,\n                js.Module.Global => return .{ .local = self, .handle = @ptrCast(value.local(self).handle) },\n                else => {}\n            }\n            // zig fmt: on\n\n            if (@hasDecl(T, \"runtimeGenericWrap\")) {\n                const wrap = try value.runtimeGenericWrap(self.ctx.page);\n                return self.zigValueToJs(wrap, opts);\n            }\n\n            if (s.is_tuple) {\n                // return the tuple struct as an array\n                var js_arr = self.newArray(@intCast(s.fields.len));\n                inline for (s.fields, 0..) |f, i| {\n                    if (try js_arr.set(@intCast(i), @field(value, f.name), opts) == false) {\n                        return error.FailedToCreateArray;\n                    }\n                }\n                return js_arr.toValue();\n            }\n\n            const js_obj = self.newObject();\n            inline for (s.fields) |f| {\n                if (try js_obj.set(f.name, @field(value, f.name), opts) == false) {\n                    return error.CreateObjectFailure;\n                }\n            }\n            return js_obj.toValue();\n        },\n        .@\"union\" => |un| {\n            if (T == std.json.Value) {\n                return self.zigJsonToJs(value);\n            }\n            if (un.tag_type) |UnionTagType| {\n                inline for (un.fields) |field| {\n                    if (value == @field(UnionTagType, field.name)) {\n                        return self.zigValueToJs(@field(value, field.name), opts);\n                    }\n                }\n                unreachable;\n            }\n            @compileError(\"Cannot use untagged union: \" ++ @typeName(T));\n        },\n        .optional => {\n            if (value) |v| {\n                return self.zigValueToJs(v, opts);\n            }\n            // would be handled by simpleZigValueToJs\n            unreachable;\n        },\n        .error_union => return self.zigValueToJs(try value, opts),\n        else => {},\n    }\n\n    @compileError(\"A function returns an unsupported type: \" ++ @typeName(T));\n}\n\nfn zigJsonToJs(self: *const Local, value: std.json.Value) !js.Value {\n    const isolate = self.isolate;\n\n    switch (value) {\n        .bool => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },\n        .float => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },\n        .integer => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },\n        .string => |v| return .{ .local = self, .handle = js.simpleZigValueToJs(isolate, v, true, false) },\n        .null => return .{ .local = self, .handle = isolate.initNull() },\n\n        // TODO handle number_string.\n        // It is used to represent too big numbers.\n        .number_string => return error.TODO,\n\n        .array => |v| {\n            const js_arr = self.newArray(@intCast(v.items.len));\n            for (v.items, 0..) |array_value, i| {\n                if (try js_arr.set(@intCast(i), array_value, .{}) == false) {\n                    return error.JSObjectSetValue;\n                }\n            }\n            return js_arr.toArray();\n        },\n        .object => |v| {\n            var js_obj = self.newObject();\n            var it = v.iterator();\n            while (it.next()) |kv| {\n                if (try js_obj.set(kv.key_ptr.*, kv.value_ptr.*, .{}) == false) {\n                    return error.JSObjectSetValue;\n                }\n            }\n            return .{ .local = self, .handle = @ptrCast(js_obj.handle) };\n        },\n    }\n}\n\n// == JS -> Zig ==\n\npub fn jsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !T {\n    switch (@typeInfo(T)) {\n        .optional => |o| {\n            // If type type is a ?js.Value or a ?js.Object, then we want to pass\n            // a js.Object, not null. Consider a function,\n            //    _doSomething(arg: ?Env.JsObjet) void { ... }\n            //\n            // And then these two calls:\n            //   doSomething();\n            //   doSomething(null);\n            //\n            // In the first case, we'll pass `null`. But in the\n            // second, we'll pass a js.Object which represents\n            // null.\n            // If we don't have this code, both cases will\n            // pass in `null` and the the doSomething won't\n            // be able to tell if `null` was explicitly passed\n            // or whether no parameter was passed.\n            if (comptime o.child == js.Value) {\n                return js_val;\n            }\n\n            if (comptime o.child == js.NullableString) {\n                if (js_val.isUndefined()) {\n                    return null;\n                }\n                return .{ .value = try js_val.toStringSlice() };\n            }\n\n            if (comptime o.child == js.Object) {\n                return js.Object{\n                    .local = self,\n                    .handle = @ptrCast(js_val.handle),\n                };\n            }\n\n            if (js_val.isNullOrUndefined()) {\n                return null;\n            }\n            return try self.jsValueToZig(o.child, js_val);\n        },\n        .float => |f| switch (f.bits) {\n            0...32 => return js_val.toF32(),\n            33...64 => return js_val.toF64(),\n            else => {},\n        },\n        .int => return jsIntToZig(T, js_val),\n        .bool => return js_val.toBool(),\n        .pointer => |ptr| switch (ptr.size) {\n            .one => {\n                if (!js_val.isObject()) {\n                    return error.InvalidArgument;\n                }\n                if (@hasDecl(ptr.child, \"JsApi\")) {\n                    std.debug.assert(bridge.JsApiLookup.has(ptr.child.JsApi));\n                    return TaggedOpaque.fromJS(*ptr.child, @ptrCast(js_val.handle));\n                }\n            },\n            .slice => {\n                if (ptr.sentinel() == null) {\n                    if (try jsValueToTypedArray(ptr.child, js_val)) |value| {\n                        return value;\n                    }\n                }\n\n                if (ptr.child == u8) {\n                    if (ptr.sentinel()) |s| {\n                        if (comptime s == 0) {\n                            return try js_val.toStringSliceZ();\n                        }\n                    } else {\n                        return try js_val.toStringSlice();\n                    }\n                }\n\n                if (!js_val.isArray()) {\n                    return error.InvalidArgument;\n                }\n                const js_arr = js_val.toArray();\n                const arr = try self.call_arena.alloc(ptr.child, js_arr.len());\n                for (arr, 0..) |*a, i| {\n                    const item_value = try js_arr.get(@intCast(i));\n                    a.* = try self.jsValueToZig(ptr.child, item_value);\n                }\n                return arr;\n            },\n            else => {},\n        },\n        .array => |arr| {\n            // Retrieve fixed-size array as slice\n            const slice_type = []arr.child;\n            const slice_value = try self.jsValueToZig(slice_type, js_val);\n            if (slice_value.len != arr.len) {\n                // Exact length match, we could allow smaller arrays, but we would not be able to communicate how many were written\n                return error.InvalidArgument;\n            }\n            return @as(*T, @ptrCast(slice_value.ptr)).*;\n        },\n        .@\"struct\" => {\n            return try (self.jsValueToStruct(T, js_val)) orelse {\n                return error.InvalidArgument;\n            };\n        },\n        .@\"union\" => |u| {\n            // see probeJsValueToZig for some explanation of what we're\n            // trying to do\n\n            // the first field that we find which the js_val could be\n            // coerced to.\n            var coerce_index: ?usize = null;\n\n            // the first field that we find which the js_val is\n            // compatible with. A compatible field has higher precedence\n            // than a coercible, but still isn't a perfect match.\n            var compatible_index: ?usize = null;\n            inline for (u.fields, 0..) |field, i| {\n                switch (try self.probeJsValueToZig(field.type, js_val)) {\n                    .value => |v| return @unionInit(T, field.name, v),\n                    .ok => {\n                        // a perfect match like above case, except the probing\n                        // didn't get the value for us.\n                        return @unionInit(T, field.name, try self.jsValueToZig(field.type, js_val));\n                    },\n                    .coerce => if (coerce_index == null) {\n                        coerce_index = i;\n                    },\n                    .compatible => if (compatible_index == null) {\n                        compatible_index = i;\n                    },\n                    .invalid => {},\n                }\n            }\n\n            // We didn't find a perfect match.\n            const closest = compatible_index orelse coerce_index orelse return error.InvalidArgument;\n            inline for (u.fields, 0..) |field, i| {\n                if (i == closest) {\n                    return @unionInit(T, field.name, try self.jsValueToZig(field.type, js_val));\n                }\n            }\n            unreachable;\n        },\n        .@\"enum\" => |e| {\n            if (@hasDecl(T, \"js_enum_from_string\")) {\n                const js_str = js_val.isString() orelse return error.InvalidArgument;\n                return std.meta.stringToEnum(T, try js_str.toSlice()) orelse return error.InvalidArgument;\n            }\n            switch (@typeInfo(e.tag_type)) {\n                .int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_val)),\n                else => @compileError(\"unsupported enum parameter type: \" ++ @typeName(T)),\n            }\n        },\n        else => {},\n    }\n\n    @compileError(\"has an unsupported parameter type: \" ++ @typeName(T));\n}\n\n// Extracted so that it can be used in both jsValueToZig and in\n// probeJsValueToZig. Avoids having to duplicate this logic when probing.\nfn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {\n    return switch (T) {\n        js.Function, js.Function.Global, js.Function.Temp => {\n            if (!js_val.isFunction()) {\n                return null;\n            }\n            const js_func = js.Function{ .local = self, .handle = @ptrCast(js_val.handle) };\n            return switch (T) {\n                js.Function => js_func,\n                js.Function.Temp => try js_func.temp(),\n                js.Function.Global => try js_func.persist(),\n                else => unreachable,\n            };\n        },\n        // zig fmt: off\n        js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64),\n        js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64),\n        js.TypedArray(f32), js.TypedArray(f64),\n        // zig fmt: on\n        => {\n            const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;\n            const arr = (try jsValueToTypedArray(ValueType, js_val)) orelse return null;\n            return .{ .values = arr };\n        },\n        js.Value => js_val,\n        js.Value.Global => return try js_val.persist(),\n        js.Value.Temp => return try js_val.temp(),\n        js.Object => {\n            if (!js_val.isObject()) {\n                return null;\n            }\n            return js.Object{\n                .local = self,\n                .handle = @ptrCast(js_val.handle),\n            };\n        },\n        js.Object.Global => {\n            if (!js_val.isObject()) {\n                return null;\n            }\n            const obj = js.Object{\n                .local = self,\n                .handle = @ptrCast(js_val.handle),\n            };\n            return try obj.persist();\n        },\n\n        js.Promise.Global, js.Promise.Temp => {\n            if (!js_val.isPromise()) {\n                return null;\n            }\n            const js_promise = js.Promise{\n                .local = self,\n                .handle = @ptrCast(js_val.handle),\n            };\n            return switch (T) {\n                js.Promise.Temp => try js_promise.temp(),\n                js.Promise.Global => try js_promise.persist(),\n                else => unreachable,\n            };\n        },\n        string.String => {\n            const js_str = js_val.isString() orelse return null;\n            return try js_str.toSSO(false);\n        },\n        string.Global => {\n            const js_str = js_val.isString() orelse return null;\n            return try js_str.toSSO(true);\n        },\n        else => {\n            if (!js_val.isObject()) {\n                return null;\n            }\n\n            const isolate = self.isolate;\n            const js_obj = js_val.toObject();\n\n            var value: T = undefined;\n            inline for (@typeInfo(T).@\"struct\".fields) |field| {\n                const name = field.name;\n                const key = isolate.initStringHandle(name);\n                if (js_obj.has(key)) {\n                    @field(value, name) = try self.jsValueToZig(field.type, try js_obj.get(key));\n                } else if (@typeInfo(field.type) == .optional) {\n                    @field(value, name) = null;\n                } else {\n                    const dflt = field.defaultValue() orelse return null;\n                    @field(value, name) = dflt;\n                }\n            }\n\n            return value;\n        },\n    };\n}\n\nfn jsValueToTypedArray(comptime T: type, js_val: js.Value) !?[]T {\n    var force_u8 = false;\n    var array_buffer: ?*const v8.ArrayBuffer = null;\n    var byte_len: usize = undefined;\n    var byte_offset: usize = undefined;\n\n    if (js_val.isTypedArray()) {\n        const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_val.handle);\n        byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle);\n        byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle);\n        array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?;\n    } else if (js_val.isArrayBufferView()) {\n        force_u8 = true;\n        const buffer_handle: *const v8.ArrayBufferView = @ptrCast(js_val.handle);\n        byte_len = v8.v8__ArrayBufferView__ByteLength(buffer_handle);\n        byte_offset = v8.v8__ArrayBufferView__ByteOffset(buffer_handle);\n        array_buffer = v8.v8__ArrayBufferView__Buffer(buffer_handle).?;\n    } else if (js_val.isArrayBuffer()) {\n        force_u8 = true;\n        array_buffer = @ptrCast(js_val.handle);\n        byte_len = v8.v8__ArrayBuffer__ByteLength(array_buffer);\n        byte_offset = 0;\n    }\n\n    const backing_store_ptr = v8.v8__ArrayBuffer__GetBackingStore(array_buffer orelse return null);\n    const backing_store_handle = v8.std__shared_ptr__v8__BackingStore__get(&backing_store_ptr).?;\n    const data = v8.v8__BackingStore__Data(backing_store_handle);\n\n    switch (T) {\n        u8 => {\n            if (force_u8 or js_val.isUint8Array() or js_val.isUint8ClampedArray()) {\n                if (byte_len == 0) return &[_]u8{};\n                const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len];\n            }\n        },\n        i8 => {\n            if (js_val.isInt8Array()) {\n                if (byte_len == 0) return &[_]i8{};\n                const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len];\n            }\n        },\n        u16 => {\n            if (js_val.isUint16Array()) {\n                if (byte_len == 0) return &[_]u16{};\n                const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len / 2];\n            }\n        },\n        i16 => {\n            if (js_val.isInt16Array()) {\n                if (byte_len == 0) return &[_]i16{};\n                const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len / 2];\n            }\n        },\n        u32 => {\n            if (js_val.isUint32Array()) {\n                if (byte_len == 0) return &[_]u32{};\n                const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len / 4];\n            }\n        },\n        i32 => {\n            if (js_val.isInt32Array()) {\n                if (byte_len == 0) return &[_]i32{};\n                const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len / 4];\n            }\n        },\n        u64 => {\n            if (js_val.isBigUint64Array()) {\n                if (byte_len == 0) return &[_]u64{};\n                const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len / 8];\n            }\n        },\n        i64 => {\n            if (js_val.isBigInt64Array()) {\n                if (byte_len == 0) return &[_]i64{};\n                const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));\n                return arr_ptr[byte_offset .. byte_offset + byte_len / 8];\n            }\n        },\n        else => {},\n    }\n    return error.InvalidArgument;\n}\n\n// Probing is part of trying to map a JS value to a Zig union. There's\n// a lot of ambiguity in this process, in part because some JS values\n// can almost always be coerced. For example, anything can be coerced\n// into an integer (it just becomes 0), or a float (becomes NaN) or a\n// string.\n//\n// The way we'll do this is that, if there's a direct match, we'll use it\n// If there's a potential match, we'll keep looking for a direct match\n// and only use the (first) potential match as a fallback.\n//\n// Finally, I considered adding this probing directly into jsValueToZig\n// but I decided doing this separately was better. However, the goal is\n// obviously that probing is consistent with jsValueToZig.\nfn ProbeResult(comptime T: type) type {\n    return union(enum) {\n        // The js_value maps directly to T\n        value: T,\n\n        // The value is a T. This is almost the same as returning value: T,\n        // but the caller still has to get T by calling jsValueToZig.\n        // We prefer returning .{.ok => {}}, to avoid reducing duplication\n        // with jsValueToZig, but in some cases where probing has a cost\n        // AND yields the value anyways, we'll use .{.value = T}.\n        ok: void,\n\n        // the js_value is compatible with T (i.e. a int -> float),\n        compatible: void,\n\n        // the js_value can be coerced to T (this is a lower precedence\n        // than compatible)\n        coerce: void,\n\n        // the js_value cannot be turned into T\n        invalid: void,\n    };\n}\nfn probeJsValueToZig(self: *const Local, comptime T: type, js_val: js.Value) !ProbeResult(T) {\n    switch (@typeInfo(T)) {\n        .optional => |o| {\n            if (js_val.isNullOrUndefined()) {\n                return .{ .value = null };\n            }\n            return self.probeJsValueToZig(o.child, js_val);\n        },\n        .float => {\n            if (js_val.isNumber() or js_val.isNumberObject()) {\n                if (js_val.isInt32() or js_val.isUint32() or js_val.isBigInt() or js_val.isBigIntObject()) {\n                    // int => float is a reasonable match\n                    return .{ .compatible = {} };\n                }\n                return .{ .ok = {} };\n            }\n            // anything can be coerced into a float, it becomes NaN\n            return .{ .coerce = {} };\n        },\n        .int => {\n            if (js_val.isNumber() or js_val.isNumberObject()) {\n                if (js_val.isInt32() or js_val.isUint32() or js_val.isBigInt() or js_val.isBigIntObject()) {\n                    return .{ .ok = {} };\n                }\n                // float => int is kind of reasonable, I guess\n                return .{ .compatible = {} };\n            }\n            // anything can be coerced into a int, it becomes 0\n            return .{ .coerce = {} };\n        },\n        .bool => {\n            if (js_val.isBoolean() or js_val.isBooleanObject()) {\n                return .{ .ok = {} };\n            }\n            // anything can be coerced into a boolean, it will become\n            // true or false based on..some complex rules I don't know.\n            return .{ .coerce = {} };\n        },\n        .pointer => |ptr| switch (ptr.size) {\n            .one => {\n                if (!js_val.isObject()) {\n                    return .{ .invalid = {} };\n                }\n                if (bridge.JsApiLookup.has(ptr.child.JsApi)) {\n                    // There's a bit of overhead in doing this, so instead\n                    // of having a version of TaggedOpaque which\n                    // returns a boolean or an optional, we rely on the\n                    // main implementation and just handle the error.\n                    const attempt = TaggedOpaque.fromJS(*ptr.child, @ptrCast(js_val.handle));\n                    if (attempt) |value| {\n                        return .{ .value = value };\n                    } else |_| {\n                        return .{ .invalid = {} };\n                    }\n                }\n                // probably an error, but not for us to deal with\n                return .{ .invalid = {} };\n            },\n            .slice => {\n                if (js_val.isTypedArray()) {\n                    switch (ptr.child) {\n                        u8 => if (ptr.sentinel() == null) {\n                            if (js_val.isUint8Array() or js_val.isUint8ClampedArray()) {\n                                return .{ .ok = {} };\n                            }\n                        },\n                        i8 => if (js_val.isInt8Array()) {\n                            return .{ .ok = {} };\n                        },\n                        u16 => if (js_val.isUint16Array()) {\n                            return .{ .ok = {} };\n                        },\n                        i16 => if (js_val.isInt16Array()) {\n                            return .{ .ok = {} };\n                        },\n                        u32 => if (js_val.isUint32Array()) {\n                            return .{ .ok = {} };\n                        },\n                        i32 => if (js_val.isInt32Array()) {\n                            return .{ .ok = {} };\n                        },\n                        u64 => if (js_val.isBigUint64Array()) {\n                            return .{ .ok = {} };\n                        },\n                        i64 => if (js_val.isBigInt64Array()) {\n                            return .{ .ok = {} };\n                        },\n                        else => {},\n                    }\n                    return .{ .invalid = {} };\n                }\n\n                if (ptr.child == u8) {\n                    if (v8.v8__Value__IsString(js_val.handle)) {\n                        return .{ .ok = {} };\n                    }\n                    // anything can be coerced into a string\n                    return .{ .coerce = {} };\n                }\n\n                if (!js_val.isArray()) {\n                    return .{ .invalid = {} };\n                }\n\n                // This can get tricky.\n                const js_arr = js_val.toArray();\n\n                if (js_arr.len() == 0) {\n                    // not so tricky in this case.\n                    return .{ .value = &.{} };\n                }\n\n                // We settle for just probing the first value. Ok, actually\n                // not tricky in this case either.\n                const first_val = try js_arr.get(0);\n                switch (try self.probeJsValueToZig(ptr.child, first_val)) {\n                    .value, .ok => return .{ .ok = {} },\n                    .compatible => return .{ .compatible = {} },\n                    .coerce => return .{ .coerce = {} },\n                    .invalid => return .{ .invalid = {} },\n                }\n            },\n            else => {},\n        },\n        .array => |arr| {\n            // Retrieve fixed-size array as slice then probe\n            const slice_type = []arr.child;\n            switch (try self.probeJsValueToZig(slice_type, js_val)) {\n                .value => |slice_value| {\n                    if (slice_value.len == arr.len) {\n                        return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* };\n                    }\n                    return .{ .invalid = {} };\n                },\n                .ok => {\n                    // Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written\n                    if (js_val.isArray()) {\n                        const js_arr = js_val.toArray();\n                        if (js_arr.len() == arr.len) {\n                            return .{ .ok = {} };\n                        }\n                    } else if (arr.child == u8) {\n                        if (js_val.isString()) |js_str| {\n                            if (js_str.lenUtf8(self.isolate) == arr.len) {\n                                return .{ .ok = {} };\n                            }\n                        }\n                    }\n                    return .{ .invalid = {} };\n                },\n                .compatible => return .{ .compatible = {} },\n                .coerce => return .{ .coerce = {} },\n                .invalid => return .{ .invalid = {} },\n            }\n        },\n        .@\"struct\" => {\n            // Handle string.String and string.Global specially\n            if (T == string.String or T == string.Global) {\n                if (v8.v8__Value__IsString(js_val.handle)) {\n                    return .{ .ok = {} };\n                }\n                // Anything can be coerced to a string\n                return .{ .coerce = {} };\n            }\n\n            // We don't want to duplicate the code for this, so we call\n            // the actual conversion function.\n            const value = (try self.jsValueToStruct(T, js_val)) orelse {\n                return .{ .invalid = {} };\n            };\n            return .{ .value = value };\n        },\n        else => {},\n    }\n\n    return .{ .invalid = {} };\n}\n\nfn jsIntToZig(comptime T: type, js_value: js.Value) !T {\n    const n = @typeInfo(T).int;\n    switch (n.signedness) {\n        .signed => switch (n.bits) {\n            8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32()),\n            16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32()),\n            32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32()),\n            64 => {\n                if (js_value.isBigInt()) {\n                    const v = js_value.toBigInt();\n                    return v.getInt64();\n                }\n                return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32());\n            },\n            else => {},\n        },\n        .unsigned => switch (n.bits) {\n            8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32()),\n            16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32()),\n            32 => {\n                if (js_value.isBigInt()) {\n                    const v = js_value.toBigInt();\n                    const large = v.getUint64();\n                    if (large <= 4_294_967_295) {\n                        return @intCast(large);\n                    }\n                    return error.InvalidArgument;\n                }\n                return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32());\n            },\n            64 => {\n                if (js_value.isBigInt()) {\n                    const v = js_value.toBigInt();\n                    return v.getUint64();\n                }\n                return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32());\n            },\n            else => {},\n        },\n    }\n    @compileError(\"Only i8, i16, i32, i64, u8, u16, u32 and u64 are supported\");\n}\n\nfn jsSignedIntToZig(comptime T: type, comptime min: comptime_int, max: comptime_int, maybe: i32) !T {\n    if (maybe >= min and maybe <= max) {\n        return @intCast(maybe);\n    }\n    return error.InvalidArgument;\n}\n\nfn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {\n    if (maybe <= max) {\n        return @intCast(maybe);\n    }\n    return error.InvalidArgument;\n}\n\n// Every WebApi type has a class_id as T.JsApi.Meta.class_id. We use this to create\n// a JSValue class of the correct type. However, given a Node, we don't want\n// to create a Node class, we want to create a class of the most specific type.\n// In other words, given a Node{._type = .{.document .{}}}, we want to create\n// a Document, not a Node.\n// This function recursively walks the _type union field (if there is one) to\n// get the most specific class_id possible.\nconst Resolved = struct {\n    weak: bool,\n    ptr: *anyopaque,\n    class_id: u16,\n    prototype_chain: []const @import(\"TaggedOpaque.zig\").PrototypeChainEntry,\n    finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,\n    finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null,\n};\npub fn resolveValue(value: anytype) Resolved {\n    const T = bridge.Struct(@TypeOf(value));\n    if (!@hasField(T, \"_type\") or @typeInfo(@TypeOf(value._type)) != .@\"union\") {\n        return resolveT(T, value);\n    }\n\n    const U = @typeInfo(@TypeOf(value._type)).@\"union\";\n    inline for (U.fields) |field| {\n        if (value._type == @field(U.tag_type.?, field.name)) {\n            const child = switch (@typeInfo(field.type)) {\n                .pointer => @field(value._type, field.name),\n                .@\"struct\" => &@field(value._type, field.name),\n                .void => {\n                    // Unusual case, but the Event (and maybe others) can be\n                    // returned as-is. In that case, it has a dummy void type.\n                    return resolveT(T, value);\n                },\n                else => @compileError(@typeName(field.type) ++ \" has an unsupported _type field\"),\n            };\n            return resolveValue(child);\n        }\n    }\n    unreachable;\n}\n\nfn resolveT(comptime T: type, value: *anyopaque) Resolved {\n    const Meta = T.JsApi.Meta;\n    return .{\n        .ptr = value,\n        .class_id = Meta.class_id,\n        .prototype_chain = &Meta.prototype_chain,\n        .weak = if (@hasDecl(Meta, \"weak\")) Meta.weak else false,\n        .finalizer_from_v8 = if (@hasDecl(Meta, \"finalizer\")) Meta.finalizer.from_v8 else null,\n        .finalizer_from_zig = if (@hasDecl(Meta, \"finalizer\")) Meta.finalizer.from_zig else null,\n    };\n}\n\nfn conditionallyReference(value: anytype) void {\n    const T = bridge.Struct(@TypeOf(value));\n    if (@hasDecl(T, \"acquireRef\")) {\n        value.acquireRef();\n        return;\n    }\n    if (@hasField(T, \"_proto\")) {\n        conditionallyReference(value._proto);\n    }\n}\n\npub fn stackTrace(self: *const Local) !?[]const u8 {\n    const isolate = self.isolate;\n    const separator = log.separator();\n\n    var buf: std.ArrayList(u8) = .empty;\n    var writer = buf.writer(self.call_arena);\n\n    const stack_trace_handle = v8.v8__StackTrace__CurrentStackTrace__STATIC(isolate.handle, 30).?;\n    const frame_count = v8.v8__StackTrace__GetFrameCount(stack_trace_handle);\n\n    if (v8.v8__StackTrace__CurrentScriptNameOrSourceURL__STATIC(isolate.handle)) |script| {\n        const stack = js.String{ .local = self, .handle = script };\n        try writer.print(\"{s}<{f}>\", .{ separator, stack });\n    }\n\n    for (0..@intCast(frame_count)) |i| {\n        const frame_handle = v8.v8__StackTrace__GetFrame(stack_trace_handle, isolate.handle, @intCast(i)).?;\n        if (v8.v8__StackFrame__GetFunctionName(frame_handle)) |name| {\n            const script = js.String{ .local = self, .handle = name };\n            try writer.print(\"{s}{f}:{d}\", .{ separator, script, v8.v8__StackFrame__GetLineNumber(frame_handle) });\n        } else {\n            try writer.print(\"{s}<anonymous>:{d}\", .{ separator, v8.v8__StackFrame__GetLineNumber(frame_handle) });\n        }\n    }\n    return buf.items;\n}\n\n// == Promise Helpers ==\npub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {\n    var resolver = js.PromiseResolver.init(self);\n    resolver.reject(\"Local.rejectPromise\", value);\n    return resolver.promise();\n}\n\npub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise {\n    var resolver = js.PromiseResolver.init(self);\n    resolver.rejectError(\"Local.rejectPromise\", value);\n    return resolver.promise();\n}\n\npub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {\n    var resolver = js.PromiseResolver.init(self);\n    resolver.resolve(\"Local.resolvePromise\", value);\n    return resolver.promise();\n}\n\npub fn createPromiseResolver(self: *const Local) js.PromiseResolver {\n    return js.PromiseResolver.init(self);\n}\n\npub fn debugValue(self: *const Local, js_val: js.Value, writer: *std.Io.Writer) !void {\n    var seen: std.AutoHashMapUnmanaged(u32, void) = .empty;\n    return self._debugValue(js_val, &seen, 0, writer) catch error.WriteFailed;\n}\n\nfn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void {\n    if (js_val.isNull()) {\n        // I think null can sometimes appear as an object, so check this and\n        // handle it first.\n        return writer.writeAll(\"null\");\n    }\n\n    if (!js_val.isObject()) {\n        // handle these explicitly, so we don't include the type (we only want to include\n        // it when there's some ambiguity, e.g. the string \"true\")\n        if (js_val.isUndefined()) {\n            return writer.writeAll(\"undefined\");\n        }\n        if (js_val.isTrue()) {\n            return writer.writeAll(\"true\");\n        }\n        if (js_val.isFalse()) {\n            return writer.writeAll(\"false\");\n        }\n\n        if (js_val.isSymbol()) {\n            const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;\n            if (v8.v8__Value__IsUndefined(symbol_handle)) {\n                return writer.writeAll(\"undefined (symbol)\");\n            }\n            return writer.print(\"{f} (symbol)\", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});\n        }\n        const js_val_str = try js_val.toStringSlice();\n        if (js_val_str.len > 2000) {\n            try writer.writeAll(js_val_str[0..2000]);\n            try writer.writeAll(\" ... (truncated)\");\n        } else {\n            try writer.writeAll(js_val_str);\n        }\n        return writer.print(\" ({f})\", .{js_val.typeOf()});\n    }\n\n    const js_obj = js_val.toObject();\n    {\n        // explicit scope because gop will become invalid in recursive call\n        const obj_id: u32 = @bitCast(v8.v8__Object__GetIdentityHash(js_obj.handle));\n        const gop = try seen.getOrPut(self.call_arena, obj_id);\n        if (gop.found_existing) {\n            return writer.writeAll(\"<circular>\\n\");\n        }\n        gop.value_ptr.* = {};\n    }\n\n    if (depth > 20) {\n        return writer.writeAll(\"...deeply nested object...\");\n    }\n\n    const names_arr = js_obj.getOwnPropertyNames() catch {\n        return writer.writeAll(\"...invalid object...\");\n    };\n    const len = names_arr.len();\n\n    const own_len = blk: {\n        const own_names = js_obj.getOwnPropertyNames() catch break :blk 0;\n        break :blk own_names.len();\n    };\n\n    if (own_len == 0) {\n        const js_val_str = try js_val.toStringSlice();\n        if (js_val_str.len > 2000) {\n            try writer.writeAll(js_val_str[0..2000]);\n            return writer.writeAll(\" ... (truncated)\");\n        }\n        return writer.writeAll(js_val_str);\n    }\n\n    const all_len = js_obj.getPropertyNames().len();\n    try writer.print(\"({d}/{d})\", .{ own_len, all_len });\n    for (0..len) |i| {\n        if (i == 0) {\n            try writer.writeByte('\\n');\n        }\n        const field_name = try names_arr.get(@intCast(i));\n        const name = try field_name.toStringSlice();\n        try writer.splatByteAll(' ', depth);\n        try writer.writeAll(name);\n        try writer.writeAll(\": \");\n\n        const field_val = try js_obj.get(name);\n        try self._debugValue(field_val, seen, depth + 1, writer);\n        if (i != len - 1) {\n            try writer.writeByte('\\n');\n        }\n    }\n}\n\n// == Misc ==\npub fn parseJSON(self: *const Local, json: []const u8) !js.Value {\n    const string_handle = self.isolate.initStringHandle(json);\n    const value_handle = v8.v8__JSON__Parse(self.handle, string_handle) orelse return error.JsException;\n    return .{\n        .local = self,\n        .handle = value_handle,\n    };\n}\n\npub fn throw(self: *const Local, err: []const u8) js.Exception {\n    const handle = self.isolate.createError(err);\n    return .{\n        .local = self,\n        .handle = handle,\n    };\n}\n\n// Convert a Global (or optional Global) to a Local (or optional Local).\n// Meant to be used from either page.js.toLocal, where the context must have an\n// non-null local (orelse panic), or from a LocalScope\npub fn toLocal(self: *const Local, global: anytype) ToLocalReturnType(@TypeOf(global)) {\n    const T = @TypeOf(global);\n    if (@typeInfo(T) == .optional) {\n        const unwrapped = global orelse return null;\n        return unwrapped.local(self);\n    }\n    return global.local(self);\n}\n\npub fn ToLocalReturnType(comptime T: type) type {\n    if (@typeInfo(T) == .optional) {\n        const GlobalType = @typeInfo(T).optional.child;\n        const struct_info = @typeInfo(GlobalType).@\"struct\";\n        inline for (struct_info.decls) |decl| {\n            if (std.mem.eql(u8, decl.name, \"local\")) {\n                const Fn = @TypeOf(@field(GlobalType, \"local\"));\n                const fn_info = @typeInfo(Fn).@\"fn\";\n                return ?fn_info.return_type.?;\n            }\n        }\n        @compileError(\"Type does not have local method\");\n    } else {\n        const struct_info = @typeInfo(T).@\"struct\";\n        inline for (struct_info.decls) |decl| {\n            if (std.mem.eql(u8, decl.name, \"local\")) {\n                const Fn = @TypeOf(@field(T, \"local\"));\n                const fn_info = @typeInfo(Fn).@\"fn\";\n                return fn_info.return_type.?;\n            }\n        }\n        @compileError(\"Type does not have local method\");\n    }\n}\n\npub fn debugContextId(self: *const Local) i32 {\n    return v8.v8__Context__DebugContextId(self.handle);\n}\n\n// Encapsulates a Local and a HandleScope. When we're going from V8->Zig\n// we easily get both a Local and a HandleScope via Caller.init.\n// But when we're going from Zig -> V8, things are more complicated.\n\n// 1 - In some cases, we're going from Zig -> V8, but the origin is actually V8,\n// so it's really V8 -> Zig -> V8. For example, when element.click() is called,\n// V8 will call the Element.click method, which could then call back into V8 for\n// a click handler.\n//\n// 2 - In other cases, it's always initiated from Zig, e.g. window.setTimeout or\n// window.onload.\n//\n// 3 - Yet in other cases, it might could be either. Event dispatching can both be\n// initiated from Zig and from V8.\n//\n// When JS execution is Zig initiated (or if we aren't sure whether it's Zig\n// initiated or not), we need to create a Local.Scope:\n//\n//   var ls: js.Local.Scope = udnefined;\n//   page.js.localScope(&ls);\n//   defer ls.deinit();\n//   // can use ls.local as needed.\n//\n// Note: Zig code that is 100% guaranteed to be v8-initiated can get a local via:\n//   page.js.local.?\npub const Scope = struct {\n    local: Local,\n    handle_scope: js.HandleScope,\n\n    pub fn deinit(self: *Scope) void {\n        v8.v8__Context__Exit(self.local.handle);\n        self.handle_scope.deinit();\n    }\n\n    pub fn toLocal(self: *Scope, global: anytype) ToLocalReturnType(@TypeOf(global)) {\n        return self.local.toLocal(global);\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Module.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst Module = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.Module,\n\npub const Status = enum(u32) {\n    kUninstantiated = v8.kUninstantiated,\n    kInstantiating = v8.kInstantiating,\n    kInstantiated = v8.kInstantiated,\n    kEvaluating = v8.kEvaluating,\n    kEvaluated = v8.kEvaluated,\n    kErrored = v8.kErrored,\n};\n\npub fn getStatus(self: Module) Status {\n    return @enumFromInt(v8.v8__Module__GetStatus(self.handle));\n}\n\npub fn getException(self: Module) js.Value {\n    return .{\n        .local = self.local,\n        .handle = v8.v8__Module__GetException(self.handle).?,\n    };\n}\n\npub fn getModuleRequests(self: Module) Requests {\n    return .{\n        .context_handle = self.local.handle,\n        .handle = v8.v8__Module__GetModuleRequests(self.handle).?,\n    };\n}\n\npub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out);\n    if (out.has_value) {\n        return out.value;\n    }\n    return error.JsException;\n}\n\npub fn evaluate(self: Module) !js.Value {\n    const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;\n\n    if (self.getStatus() == .kErrored) {\n        return error.JsException;\n    }\n\n    return .{\n        .local = self.local,\n        .handle = res,\n    };\n}\n\npub fn getIdentityHash(self: Module) u32 {\n    return @bitCast(v8.v8__Module__GetIdentityHash(self.handle));\n}\n\npub fn getModuleNamespace(self: Module) js.Value {\n    return .{\n        .local = self.local,\n        .handle = v8.v8__Module__GetModuleNamespace(self.handle).?,\n    };\n}\n\npub fn getScriptId(self: Module) u32 {\n    return @intCast(v8.v8__Module__ScriptId(self.handle));\n}\n\npub fn persist(self: Module) !Global {\n    var ctx = self.local.ctx;\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n    try ctx.global_modules.append(ctx.arena, global);\n    return .{ .handle = global };\n}\n\npub const Global = struct {\n    handle: v8.Global,\n\n    pub fn deinit(self: *Global) void {\n        v8.v8__Global__Reset(&self.handle);\n    }\n\n    pub fn local(self: *const Global, l: *const js.Local) Module {\n        return .{\n            .local = l,\n            .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),\n        };\n    }\n\n    pub fn isEqual(self: *const Global, other: Module) bool {\n        return v8.v8__Global__IsEqual(&self.handle, other.handle);\n    }\n};\n\nconst Requests = struct {\n    handle: *const v8.FixedArray,\n    context_handle: *const v8.Context,\n\n    pub fn len(self: Requests) usize {\n        return @intCast(v8.v8__FixedArray__Length(self.handle));\n    }\n\n    pub fn get(self: Requests, idx: usize) Request {\n        return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };\n    }\n};\n\nconst Request = struct {\n    handle: *const v8.ModuleRequest,\n\n    pub fn specifier(self: Request, local: *const js.Local) js.String {\n        return .{ .local = local, .handle = v8.v8__ModuleRequest__GetSpecifier(self.handle).? };\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Number.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\n\nconst v8 = js.v8;\n\nconst Number = @This();\n\nhandle: *const v8.Number,\n\npub fn init(isolate: *v8.Isolate, value: anytype) Number {\n    const handle = v8.v8__Number__New(isolate, value).?;\n    return .{ .handle = handle };\n}\n"
  },
  {
    "path": "src/browser/js/Object.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Object = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.Object,\n\npub fn has(self: Object, key: anytype) bool {\n    const ctx = self.local.ctx;\n    const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);\n\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out);\n    if (out.has_value) {\n        return out.value;\n    }\n    return false;\n}\n\npub fn get(self: Object, key: anytype) !js.Value {\n    const ctx = self.local.ctx;\n\n    const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);\n    const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException;\n\n    return .{\n        .local = self.local,\n        .handle = js_val_handle,\n    };\n}\n\npub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {\n    const ctx = self.local.ctx;\n\n    const js_value = try self.local.zigValueToJs(value, opts);\n    const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);\n\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out);\n    return out.has_value;\n}\n\npub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {\n    const ctx = self.local.ctx;\n    const name_handle = ctx.isolate.initStringHandle(name);\n\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out);\n\n    if (out.has_value) {\n        return out.value;\n    } else {\n        return null;\n    }\n}\n\npub fn toValue(self: Object) js.Value {\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn format(self: Object, writer: *std.Io.Writer) !void {\n    if (comptime IS_DEBUG) {\n        return self.local.ctx.debugValue(self.toValue(), writer);\n    }\n    const str = self.toString() catch return error.WriteFailed;\n    return writer.writeAll(str);\n}\n\npub fn persist(self: Object) !Global {\n    var ctx = self.local.ctx;\n\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n\n    try ctx.trackGlobal(global);\n\n    return .{ .handle = global };\n}\n\npub fn getFunction(self: Object, name: []const u8) !?js.Function {\n    if (self.isNullOrUndefined()) {\n        return null;\n    }\n    const local = self.local;\n\n    const js_name = local.isolate.initStringHandle(name);\n    const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException;\n\n    if (v8.v8__Value__IsFunction(js_val_handle) == false) {\n        return null;\n    }\n    return .{\n        .local = local,\n        .handle = @ptrCast(js_val_handle),\n    };\n}\n\npub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {\n    const func = try self.getFunction(method_name) orelse return error.MethodNotFound;\n    return func.callWithThis(T, self, args);\n}\n\npub fn isNullOrUndefined(self: Object) bool {\n    return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));\n}\n\npub fn getOwnPropertyNames(self: Object) !js.Array {\n    const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle) orelse {\n        // This is almost always a fatal error case. Either we're in some exception\n        // and things are messy, or we're shutting down, or someone has messed up\n        // the object (like some WPT tests do).\n        return error.TypeError;\n    };\n\n    return .{\n        .local = self.local,\n        .handle = handle,\n    };\n}\n\npub fn getPropertyNames(self: Object) js.Array {\n    const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;\n    return .{\n        .local = self.local,\n        .handle = handle,\n    };\n}\n\npub fn nameIterator(self: Object) !NameIterator {\n    const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle) orelse {\n        // see getOwnPropertyNames above\n        return error.TypeError;\n    };\n    const count = v8.v8__Array__Length(handle);\n\n    return .{\n        .local = self.local,\n        .handle = handle,\n        .count = count,\n    };\n}\n\npub fn toZig(self: Object, comptime T: type) !T {\n    const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };\n    return self.local.jsValueToZig(T, js_value);\n}\n\npub const Global = struct {\n    handle: v8.Global,\n\n    pub fn deinit(self: *Global) void {\n        v8.v8__Global__Reset(&self.handle);\n    }\n\n    pub fn local(self: *const Global, l: *const js.Local) Object {\n        return .{\n            .local = l,\n            .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),\n        };\n    }\n\n    pub fn isEqual(self: *const Global, other: Object) bool {\n        return v8.v8__Global__IsEqual(&self.handle, other.handle);\n    }\n};\n\npub const NameIterator = struct {\n    count: u32,\n    idx: u32 = 0,\n    local: *const js.Local,\n    handle: *const v8.Array,\n\n    pub fn next(self: *NameIterator) !?[]const u8 {\n        const idx = self.idx;\n        if (idx == self.count) {\n            return null;\n        }\n        self.idx += 1;\n\n        const local = self.local;\n        const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;\n        return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Origin.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n// Origin represents the shared Zig<->JS bridge state for all contexts within\n// the same origin. Multiple contexts (frames) from the same origin share a\n// single Origin, ensuring that JS objects maintain their identity across frames.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\n\nconst App = @import(\"../../App.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst v8 = js.v8;\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Origin = @This();\n\nrc: usize = 1,\narena: Allocator,\n\n// The key, e.g. lightpanda.io:443\nkey: []const u8,\n\n// Security token - all contexts in this realm must use the same v8::Value instance\n// as their security token for V8 to allow cross-context access\nsecurity_token: v8.Global,\n\n// Serves two purposes. Like `global_objects`, this is used to free\n// every Global(Object) we've created during the lifetime of the realm.\n// More importantly, it serves as an identity map - for a given Zig\n// instance, we map it to the same Global(Object).\n// The key is the @intFromPtr of the Zig value\nidentity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,\n\n// Some web APIs have to manage opaque values. Ideally, they use an\n// js.Object, but the js.Object has no lifetime guarantee beyond the\n// current call. They can call .persist() on their js.Object to get\n// a `Global(Object)`. We need to track these to free them.\n// This used to be a map and acted like identity_map; the key was\n// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without\n// a reliable way to know if an object has already been persisted,\n// we now simply persist every time persist() is called.\nglobals: std.ArrayList(v8.Global) = .empty,\n\n// Temp variants stored in HashMaps for O(1) early cleanup.\n// Key is global.data_ptr.\ntemps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,\n\n// Any type that is stored in the identity_map which has a finalizer declared\n// will have its finalizer stored here. This is only used when shutting down\n// if v8 hasn't called the finalizer directly itself.\nfinalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,\n\ntaken_over: std.ArrayList(*Origin),\n\npub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {\n    const arena = try app.arena_pool.acquire();\n    errdefer app.arena_pool.release(arena);\n\n    var hs: js.HandleScope = undefined;\n    hs.init(isolate);\n    defer hs.deinit();\n\n    const owned_key = try arena.dupe(u8, key);\n    const token_local = isolate.initStringHandle(owned_key);\n    var token_global: v8.Global = undefined;\n    v8.v8__Global__New(isolate.handle, token_local, &token_global);\n\n    const self = try arena.create(Origin);\n    self.* = .{\n        .rc = 1,\n        .arena = arena,\n        .key = owned_key,\n        .temps = .empty,\n        .globals = .empty,\n        .taken_over = .empty,\n        .security_token = token_global,\n    };\n    return self;\n}\n\npub fn deinit(self: *Origin, app: *App) void {\n    for (self.taken_over.items) |o| {\n        o.deinit(app);\n    }\n\n    // Call finalizers before releasing anything\n    {\n        var it = self.finalizer_callbacks.valueIterator();\n        while (it.next()) |finalizer| {\n            finalizer.*.deinit();\n        }\n    }\n\n    v8.v8__Global__Reset(&self.security_token);\n\n    {\n        var it = self.identity_map.valueIterator();\n        while (it.next()) |global| {\n            v8.v8__Global__Reset(global);\n        }\n    }\n\n    for (self.globals.items) |*global| {\n        v8.v8__Global__Reset(global);\n    }\n\n    {\n        var it = self.temps.valueIterator();\n        while (it.next()) |global| {\n            v8.v8__Global__Reset(global);\n        }\n    }\n\n    app.arena_pool.release(self.arena);\n}\n\npub fn trackGlobal(self: *Origin, global: v8.Global) !void {\n    return self.globals.append(self.arena, global);\n}\n\npub const IdentityResult = struct {\n    value_ptr: *v8.Global,\n    found_existing: bool,\n};\n\npub fn addIdentity(self: *Origin, ptr: usize) !IdentityResult {\n    const gop = try self.identity_map.getOrPut(self.arena, ptr);\n    return .{\n        .value_ptr = gop.value_ptr,\n        .found_existing = gop.found_existing,\n    };\n}\n\npub fn trackTemp(self: *Origin, global: v8.Global) !void {\n    return self.temps.put(self.arena, global.data_ptr, global);\n}\n\npub fn releaseTemp(self: *Origin, global: v8.Global) void {\n    if (self.temps.fetchRemove(global.data_ptr)) |kv| {\n        var g = kv.value;\n        v8.v8__Global__Reset(&g);\n    }\n}\n\n/// Release an item from the identity_map (called after finalizer runs from V8)\npub fn release(self: *Origin, item: *anyopaque) void {\n    var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {\n        if (comptime IS_DEBUG) {\n            std.debug.assert(false);\n        }\n        return;\n    };\n    v8.v8__Global__Reset(&global.value);\n\n    // The item has been finalized, remove it from the finalizer callback so that\n    // we don't try to call it again on shutdown.\n    const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {\n        if (comptime IS_DEBUG) {\n            std.debug.assert(false);\n        }\n        return;\n    };\n    const fc = kv.value;\n    fc.session.releaseArena(fc.arena);\n}\n\npub fn createFinalizerCallback(\n    self: *Origin,\n    session: *Session,\n    global: v8.Global,\n    ptr: *anyopaque,\n    zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,\n) !*FinalizerCallback {\n    const arena = try session.getArena(.{ .debug = \"FinalizerCallback\" });\n    errdefer session.releaseArena(arena);\n    const fc = try arena.create(FinalizerCallback);\n    fc.* = .{\n        .arena = arena,\n        .origin = self,\n        .session = session,\n        .ptr = ptr,\n        .global = global,\n        .zig_finalizer = zig_finalizer,\n    };\n    return fc;\n}\n\npub fn takeover(self: *Origin, original: *Origin) !void {\n    const arena = self.arena;\n\n    try self.globals.ensureUnusedCapacity(arena, original.globals.items.len);\n    for (original.globals.items) |obj| {\n        self.globals.appendAssumeCapacity(obj);\n    }\n    original.globals.clearRetainingCapacity();\n\n    {\n        try self.temps.ensureUnusedCapacity(arena, original.temps.count());\n        var it = original.temps.iterator();\n        while (it.next()) |kv| {\n            try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);\n        }\n        original.temps.clearRetainingCapacity();\n    }\n\n    {\n        try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count());\n        var it = original.finalizer_callbacks.iterator();\n        while (it.next()) |kv| {\n            kv.value_ptr.*.origin = self;\n            try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);\n        }\n        original.finalizer_callbacks.clearRetainingCapacity();\n    }\n\n    {\n        try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count());\n        var it = original.identity_map.iterator();\n        while (it.next()) |kv| {\n            try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);\n        }\n        original.identity_map.clearRetainingCapacity();\n    }\n\n    try self.taken_over.append(self.arena, original);\n}\n\n// A type that has a finalizer can have its finalizer called one of two ways.\n// The first is from V8 via the WeakCallback we give to weakRef. But that isn't\n// guaranteed to fire, so we track this in finalizer_callbacks and call them on\n// origin shutdown.\npub const FinalizerCallback = struct {\n    arena: Allocator,\n    origin: *Origin,\n    session: *Session,\n    ptr: *anyopaque,\n    global: v8.Global,\n    zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,\n\n    pub fn deinit(self: *FinalizerCallback) void {\n        self.zig_finalizer(self.ptr, self.session);\n        self.session.releaseArena(self.arena);\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Platform.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst Platform = @This();\nhandle: *v8.Platform,\n\npub fn init() !Platform {\n    if (v8.v8__V8__InitializeICU() == false) {\n        return error.FailedToInitializeICU;\n    }\n    // 0 - threadpool size, 0 == let v8 decide\n    // 1 - idle_task_support, 1 == enabled\n    const handle = v8.v8__Platform__NewDefaultPlatform(0, 1).?;\n    v8.v8__V8__InitializePlatform(handle);\n    v8.v8__V8__Initialize();\n    return .{ .handle = handle };\n}\n\npub fn deinit(self: Platform) void {\n    _ = v8.v8__V8__Dispose();\n    v8.v8__V8__DisposePlatform();\n    v8.v8__Platform__DELETE(self.handle);\n}\n"
  },
  {
    "path": "src/browser/js/Private.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst Private = @This();\n\n// Unlike most types, we always store the Private as a Global. It makes more\n// sense for this type given how it's used.\nhandle: v8.Global,\n\npub fn init(isolate: *v8.Isolate, name: []const u8) Private {\n    const v8_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));\n    const private_handle = v8.v8__Private__New(isolate, v8_name);\n\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(isolate, private_handle, &global);\n\n    return .{\n        .handle = global,\n    };\n}\n\npub fn deinit(self: *Private) void {\n    v8.v8__Global__Reset(&self.handle);\n}\n"
  },
  {
    "path": "src/browser/js/Promise.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst Promise = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.Promise,\n\npub fn toObject(self: Promise) js.Object {\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn toValue(self: Promise) js.Value {\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {\n    if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {\n        return .{\n            .local = self.local,\n            .handle = handle,\n        };\n    }\n    return error.PromiseChainFailed;\n}\n\npub fn persist(self: Promise) !Global {\n    return self._persist(true);\n}\n\npub fn temp(self: Promise) !Temp {\n    return self._persist(false);\n}\n\nfn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {\n    var ctx = self.local.ctx;\n\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n    if (comptime is_global) {\n        try ctx.trackGlobal(global);\n        return .{ .handle = global, .origin = {} };\n    }\n    try ctx.trackTemp(global);\n    return .{ .handle = global, .origin = ctx.origin };\n}\n\npub const Temp = G(.temp);\npub const Global = G(.global);\n\nconst GlobalType = enum(u8) {\n    temp,\n    global,\n};\n\nfn G(comptime global_type: GlobalType) type {\n    return struct {\n        handle: v8.Global,\n        origin: if (global_type == .temp) *js.Origin else void,\n\n        const Self = @This();\n\n        pub fn deinit(self: *Self) void {\n            v8.v8__Global__Reset(&self.handle);\n        }\n\n        pub fn local(self: *const Self, l: *const js.Local) Promise {\n            return .{\n                .local = l,\n                .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),\n            };\n        }\n\n        pub fn release(self: *const Self) void {\n            self.origin.releaseTemp(self.handle);\n        }\n    };\n}\n"
  },
  {
    "path": "src/browser/js/PromiseRejection.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst PromiseRejection = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.PromiseRejectMessage,\n\npub fn promise(self: PromiseRejection) js.Promise {\n    return .{\n        .local = self.local,\n        .handle = v8.v8__PromiseRejectMessage__GetPromise(self.handle).?,\n    };\n}\n\npub fn reason(self: PromiseRejection) ?js.Value {\n    const value_handle = v8.v8__PromiseRejectMessage__GetValue(self.handle) orelse return null;\n\n    return .{\n        .local = self.local,\n        .handle = value_handle,\n    };\n}\n"
  },
  {
    "path": "src/browser/js/PromiseResolver.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst log = @import(\"../../log.zig\");\nconst DOMException = @import(\"../webapi/DOMException.zig\");\n\nconst PromiseResolver = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.PromiseResolver,\n\npub fn init(local: *const js.Local) PromiseResolver {\n    return .{\n        .local = local,\n        .handle = v8.v8__Promise__Resolver__New(local.handle).?,\n    };\n}\n\npub fn promise(self: PromiseResolver) js.Promise {\n    return .{\n        .local = self.local,\n        .handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,\n    };\n}\n\npub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {\n    self._resolve(value) catch |err| {\n        log.err(.bug, \"resolve\", .{ .source = source, .err = err, .persistent = false });\n    };\n}\n\nfn _resolve(self: PromiseResolver, value: anytype) !void {\n    const local = self.local;\n    const js_val = try local.zigValueToJs(value, .{});\n\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out);\n    if (!out.has_value or !out.value) {\n        return error.FailedToResolvePromise;\n    }\n    local.runMicrotasks();\n}\n\npub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {\n    self._reject(value) catch |err| {\n        log.err(.bug, \"reject\", .{ .source = source, .err = err, .persistent = false });\n    };\n}\n\npub const RejectError = union(enum) {\n    generic: []const u8,\n    type_error: []const u8,\n    dom_exception: anyerror,\n};\npub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {\n    const handle = switch (err) {\n        .type_error => |str| self.local.isolate.createTypeError(str),\n        .generic => |str| self.local.isolate.createError(str),\n        .dom_exception => |exception| {\n            self.reject(source, DOMException.fromError(exception));\n            return;\n        },\n    };\n    self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {\n        log.err(.bug, \"rejectError\", .{ .source = source, .err = reject_err, .persistent = false });\n    };\n}\n\nfn _reject(self: PromiseResolver, value: anytype) !void {\n    const local = self.local;\n    const js_val = try local.zigValueToJs(value, .{});\n\n    var out: v8.MaybeBool = undefined;\n    v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);\n    if (!out.has_value or !out.value) {\n        return error.FailedToRejectPromise;\n    }\n    local.runMicrotasks();\n}\n\npub fn persist(self: PromiseResolver) !Global {\n    var ctx = self.local.ctx;\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n    try ctx.trackGlobal(global);\n    return .{ .handle = global };\n}\n\npub const Global = struct {\n    handle: v8.Global,\n\n    pub fn deinit(self: *Global) void {\n        v8.v8__Global__Reset(&self.handle);\n    }\n\n    pub fn local(self: *const Global, l: *const js.Local) PromiseResolver {\n        return .{\n            .local = l,\n            .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),\n        };\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Scheduler.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst log = @import(\"../../log.zig\");\nconst milliTimestamp = @import(\"../../datetime.zig\").milliTimestamp;\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\nconst Queue = std.PriorityQueue(Task, void, struct {\n    fn compare(_: void, a: Task, b: Task) std.math.Order {\n        const time_order = std.math.order(a.run_at, b.run_at);\n        if (time_order != .eq) return time_order;\n        // Break ties with sequence number to maintain FIFO order\n        return std.math.order(a.sequence, b.sequence);\n    }\n}.compare);\n\nconst Scheduler = @This();\n\n_sequence: u64,\nlow_priority: Queue,\nhigh_priority: Queue,\n\npub fn init(allocator: std.mem.Allocator) Scheduler {\n    return .{\n        ._sequence = 0,\n        .low_priority = Queue.init(allocator, {}),\n        .high_priority = Queue.init(allocator, {}),\n    };\n}\n\npub fn deinit(self: *Scheduler) void {\n    finalizeTasks(&self.low_priority);\n    finalizeTasks(&self.high_priority);\n}\n\nconst AddOpts = struct {\n    name: []const u8 = \"\",\n    low_priority: bool = false,\n    finalizer: ?Finalizer = null,\n};\npub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {\n    if (comptime IS_DEBUG) {\n        log.debug(.scheduler, \"scheduler.add\", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });\n    }\n    var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;\n    const seq = self._sequence + 1;\n    self._sequence = seq;\n    return queue.add(.{\n        .ctx = ctx,\n        .callback = cb,\n        .sequence = seq,\n        .name = opts.name,\n        .finalizer = opts.finalizer,\n        .run_at = milliTimestamp(.monotonic) + run_in_ms,\n    });\n}\n\npub fn run(self: *Scheduler) !void {\n    const now = milliTimestamp(.monotonic);\n    try self.runQueue(&self.low_priority, now);\n    try self.runQueue(&self.high_priority, now);\n}\n\npub fn hasReadyTasks(self: *Scheduler) bool {\n    const now = milliTimestamp(.monotonic);\n    return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);\n}\n\npub fn msToNextHigh(self: *Scheduler) ?u64 {\n    const task = self.high_priority.peek() orelse return null;\n    const now = milliTimestamp(.monotonic);\n    if (task.run_at <= now) {\n        return 0;\n    }\n    return @intCast(task.run_at - now);\n}\n\nfn runQueue(self: *Scheduler, queue: *Queue, now: u64) !void {\n    if (queue.count() == 0) {\n        return;\n    }\n\n    while (queue.peek()) |*task_| {\n        if (task_.run_at > now) {\n            return;\n        }\n        var task = queue.remove();\n        if (comptime IS_DEBUG) {\n            log.debug(.scheduler, \"scheduler.runTask\", .{ .name = task.name });\n        }\n\n        const repeat_in_ms = task.callback(task.ctx) catch |err| {\n            log.warn(.scheduler, \"task.callback\", .{ .name = task.name, .err = err });\n            continue;\n        };\n\n        if (repeat_in_ms) |ms| {\n            // Task cannot be repeated immediately, and they should know that\n            if (comptime IS_DEBUG) {\n                std.debug.assert(ms != 0);\n            }\n            task.run_at = now + ms;\n            try self.low_priority.add(task);\n        }\n    }\n    return;\n}\n\nfn queueuHasReadyTask(queue: *Queue, now: u64) bool {\n    const task = queue.peek() orelse return false;\n    return task.run_at <= now;\n}\n\nfn finalizeTasks(queue: *Queue) void {\n    var it = queue.iterator();\n    while (it.next()) |t| {\n        if (t.finalizer) |func| {\n            func(t.ctx);\n        }\n    }\n}\n\nconst Task = struct {\n    run_at: u64,\n    sequence: u64,\n    ctx: *anyopaque,\n    name: []const u8,\n    callback: Callback,\n    finalizer: ?Finalizer,\n};\n\nconst Callback = *const fn (ctx: *anyopaque) anyerror!?u32;\nconst Finalizer = *const fn (ctx: *anyopaque) void;\n"
  },
  {
    "path": "src/browser/js/Snapshot.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst bridge = @import(\"bridge.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst v8 = js.v8;\nconst JsApis = bridge.JsApis;\nconst Allocator = std.mem.Allocator;\n\nconst Snapshot = @This();\n\nconst embedded_snapshot_blob = if (@import(\"build_config\").snapshot_path) |path| @embedFile(path) else \"\";\n\n// When creating our Snapshot, we use local function templates for every Zig type.\n// You cannot, from what I can tell, create persisted FunctionTemplates at\n// snapshot creation time. But you can embedd those templates (or any other v8\n// Data) so that it's available to contexts created from the snapshot. This is\n// the starting index of those function templates, which we can extract. At\n// creation time, in debug, we assert that this is actually a consecutive integer\n// sequence\ndata_start: usize,\n\n// The snapshot data (v8.StartupData is a ptr to the data and len).\nstartup_data: v8.StartupData,\n\n// V8 doesn't know how to serialize external references, and pretty much any hook\n// into Zig is an external reference (e.g. every accessor and function callback).\n// When we create the snapshot, we give it an array with the address of every\n// external reference. When we load the snapshot, we need to give it the same\n// array with the exact same number of entries in the same order (but, of course\n// cross-process, the value (address) might be different).\nexternal_references: [countExternalReferences()]isize,\n\n// Track whether this snapshot owns its data (was created in-process)\n// If false, the data points into embedded_snapshot_blob and will not be freed\nowns_data: bool = false,\n\npub fn load() !Snapshot {\n    if (loadEmbedded()) |snapshot| {\n        return snapshot;\n    }\n    return create();\n}\n\nfn loadEmbedded() ?Snapshot {\n    // Binary format: [data_start: usize][blob data]\n    const min_size = @sizeOf(usize) + 1000;\n    if (embedded_snapshot_blob.len < min_size) {\n        // our blob should be in the MB, this is just a quick sanity check\n        return null;\n    }\n\n    const data_start = std.mem.readInt(usize, embedded_snapshot_blob[0..@sizeOf(usize)], .little);\n    const blob = embedded_snapshot_blob[@sizeOf(usize)..];\n\n    const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };\n    if (!v8.v8__StartupData__IsValid(startup_data)) {\n        return null;\n    }\n\n    return .{\n        .owns_data = false,\n        .data_start = data_start,\n        .startup_data = startup_data,\n        .external_references = collectExternalReferences(),\n    };\n}\n\npub fn deinit(self: Snapshot) void {\n    // Only free if we own the data (was created in-process)\n    if (self.owns_data) {\n        // V8 allocated this with `new char[]`, so we need to use the C++ delete[] operator\n        v8.v8__StartupData__DELETE(self.startup_data.data);\n    }\n}\n\npub fn write(self: Snapshot, writer: *std.Io.Writer) !void {\n    if (!self.isValid()) {\n        return error.InvalidSnapshot;\n    }\n\n    try writer.writeInt(usize, self.data_start, .little);\n    try writer.writeAll(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);\n}\n\npub fn fromEmbedded(self: Snapshot) bool {\n    // if the snapshot comes from the embedFile, then it'll be flagged as not\n    // owning (aka, not needing to free) the data.\n    return self.owns_data == false;\n}\n\nfn isValid(self: Snapshot) bool {\n    return v8.v8__StartupData__IsValid(self.startup_data);\n}\n\npub fn create() !Snapshot {\n    var external_references = collectExternalReferences();\n\n    var params: v8.CreateParams = undefined;\n    v8.v8__Isolate__CreateParams__CONSTRUCT(&params);\n    params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator();\n    defer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);\n    params.external_references = @ptrCast(&external_references);\n\n    const snapshot_creator = v8.v8__SnapshotCreator__CREATE(&params);\n    defer v8.v8__SnapshotCreator__DESTRUCT(snapshot_creator);\n\n    var data_start: usize = 0;\n    const isolate = v8.v8__SnapshotCreator__getIsolate(snapshot_creator).?;\n\n    {\n        // CreateBlob, which we'll call once everything is setup, MUST NOT\n        // be called from an active HandleScope. Hence we have this scope to\n        // clean it up before we call CreateBlob\n        var handle_scope: v8.HandleScope = undefined;\n        v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);\n        defer v8.v8__HandleScope__DESTRUCT(&handle_scope);\n\n        // Create templates (constructors only) FIRST\n        var templates: [JsApis.len]*v8.FunctionTemplate = undefined;\n        inline for (JsApis, 0..) |JsApi, i| {\n            @setEvalBranchQuota(10_000);\n            templates[i] = generateConstructor(JsApi, isolate);\n            attachClass(JsApi, isolate, templates[i]);\n        }\n\n        // Set up prototype chains BEFORE attaching properties\n        // This must come before attachClass so inheritance is set up first\n        inline for (JsApis, 0..) |JsApi, i| {\n            if (comptime protoIndexLookup(JsApi)) |proto_index| {\n                v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);\n            }\n        }\n\n        // Set up the global template to inherit from Window's template\n        // This way the global object gets all Window properties through inheritance\n        const context = v8.v8__Context__New(isolate, null, null);\n        v8.v8__Context__Enter(context);\n        defer v8.v8__Context__Exit(context);\n\n        // Add templates to context snapshot\n        var last_data_index: usize = 0;\n        inline for (JsApis, 0..) |_, i| {\n            @setEvalBranchQuota(10_000);\n            const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));\n            if (i == 0) {\n                data_start = data_index;\n                last_data_index = data_index;\n            } else {\n                // This isn't strictly required, but it means we only need to keep\n                // the first data_index. This is based on the assumption that\n                // addDataWithContext always increases by 1. If we ever hit this\n                // error, then that assumption is wrong and we should capture\n                // all the indexes explicitly in an array.\n                if (data_index != last_data_index + 1) {\n                    return error.InvalidDataIndex;\n                }\n                last_data_index = data_index;\n            }\n        }\n\n        // Realize all templates by getting their functions and attaching to global\n        const global_obj = v8.v8__Context__Global(context);\n\n        inline for (JsApis, 0..) |JsApi, i| {\n            const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);\n\n            // Attach to global if it has a name\n            if (@hasDecl(JsApi.Meta, \"name\")) {\n                if (@hasDecl(JsApi.Meta, \"constructor_alias\")) {\n                    const alias = JsApi.Meta.constructor_alias;\n                    const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));\n                    var maybe_result: v8.MaybeBool = undefined;\n                    v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);\n\n                    // @TODO: This is wrong. This name should be registered with the\n                    // illegalConstructorCallback. I.e. new Image() is OK, but\n                    // new HTMLImageElement() isn't.\n                    // But we _have_ to register the name, i.e. HTMLImageElement\n                    // has to be registered so, for now, instead of creating another\n                    // template, we just hook it into the constructor.\n                    const name = JsApi.Meta.name;\n                    const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));\n                    var maybe_result2: v8.MaybeBool = undefined;\n                    v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);\n                } else {\n                    const name = JsApi.Meta.name;\n                    const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));\n                    var maybe_result: v8.MaybeBool = undefined;\n                    var properties: v8.PropertyAttribute = v8.None;\n                    if (@hasDecl(JsApi.Meta, \"enumerable\") and JsApi.Meta.enumerable == false) {\n                        properties |= v8.DontEnum;\n                    }\n                    v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);\n                }\n            }\n        }\n\n        {\n            // If we want to overwrite the built-in console, we have to\n            // delete the built-in one.\n            const console_key = v8.v8__String__NewFromUtf8(isolate, \"console\", v8.kNormal, 7);\n            var maybe_deleted: v8.MaybeBool = undefined;\n            v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);\n            if (maybe_deleted.value == false) {\n                return error.ConsoleDeleteError;\n            }\n        }\n\n        // This shouldn't be necessary, but it is:\n        // https://groups.google.com/g/v8-users/c/qAQQBmbi--8\n        // TODO: see if newer V8 engines have a way around this.\n        inline for (JsApis, 0..) |JsApi, i| {\n            if (comptime protoIndexLookup(JsApi)) |proto_index| {\n                const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);\n                const proto_obj: *const v8.Object = @ptrCast(proto_func);\n\n                const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);\n                const self_obj: *const v8.Object = @ptrCast(self_func);\n\n                var maybe_result: v8.MaybeBool = undefined;\n                v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);\n            }\n        }\n\n        {\n            // Custom exception\n            // TODO: this is an horrible hack, I can't figure out how to do this cleanly.\n            const code_str = \"DOMException.prototype.__proto__ = Error.prototype\";\n            const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));\n            const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;\n            _ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;\n        }\n\n        v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);\n    }\n\n    const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);\n\n    return .{\n        .owns_data = true,\n        .data_start = data_start,\n        .external_references = external_references,\n        .startup_data = blob,\n    };\n}\n\n// Helper to check if a JsApi has a NamedIndexed handler\nfn hasNamedIndexedGetter(comptime JsApi: type) bool {\n    const declarations = @typeInfo(JsApi).@\"struct\".decls;\n    inline for (declarations) |d| {\n        const value = @field(JsApi, d.name);\n        const T = @TypeOf(value);\n        if (T == bridge.NamedIndexed) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// Count total callbacks needed for external_references array\nfn countExternalReferences() comptime_int {\n    @setEvalBranchQuota(100_000);\n\n    var count: comptime_int = 0;\n\n    // +1 for the illegal constructor callback shared by various types\n    count += 1;\n\n    // +1 for the noop function shared by various types\n    count += 1;\n\n    inline for (JsApis) |JsApi| {\n        // Constructor (only if explicit)\n        if (@hasDecl(JsApi, \"constructor\")) {\n            count += 1;\n        }\n\n        // Callable (htmldda)\n        if (@hasDecl(JsApi, \"callable\")) {\n            count += 1;\n        }\n\n        // All other callbacks\n        const declarations = @typeInfo(JsApi).@\"struct\".decls;\n        inline for (declarations) |d| {\n            const value = @field(JsApi, d.name);\n            const T = @TypeOf(value);\n            if (T == bridge.Accessor) {\n                count += 1; // getter\n                if (value.setter != null) {\n                    count += 1;\n                }\n            } else if (T == bridge.Function) {\n                count += 1;\n            } else if (T == bridge.Iterator) {\n                count += 1;\n            } else if (T == bridge.Indexed) {\n                count += 1;\n                if (value.enumerator != null) {\n                    count += 1;\n                }\n            } else if (T == bridge.NamedIndexed) {\n                count += 1; // getter\n                if (value.setter != null) count += 1;\n                if (value.deleter != null) count += 1;\n            }\n        }\n    }\n\n    // In debug mode, add unknown property callbacks for types without NamedIndexed\n    if (comptime IS_DEBUG) {\n        inline for (JsApis) |JsApi| {\n            if (!hasNamedIndexedGetter(JsApi)) {\n                count += 1;\n            }\n        }\n    }\n\n    return count + 1; // +1 for null terminator\n}\n\nfn collectExternalReferences() [countExternalReferences()]isize {\n    var idx: usize = 0;\n    var references = std.mem.zeroes([countExternalReferences()]isize);\n\n    references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));\n    idx += 1;\n\n    references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));\n    idx += 1;\n\n    inline for (JsApis) |JsApi| {\n        if (@hasDecl(JsApi, \"constructor\")) {\n            references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));\n            idx += 1;\n        }\n\n        if (@hasDecl(JsApi, \"callable\")) {\n            references[idx] = @bitCast(@intFromPtr(JsApi.callable.func));\n            idx += 1;\n        }\n\n        const declarations = @typeInfo(JsApi).@\"struct\".decls;\n        inline for (declarations) |d| {\n            const value = @field(JsApi, d.name);\n            const T = @TypeOf(value);\n            if (T == bridge.Accessor) {\n                references[idx] = @bitCast(@intFromPtr(value.getter));\n                idx += 1;\n                if (value.setter) |setter| {\n                    references[idx] = @bitCast(@intFromPtr(setter));\n                    idx += 1;\n                }\n            } else if (T == bridge.Function) {\n                references[idx] = @bitCast(@intFromPtr(value.func));\n                idx += 1;\n            } else if (T == bridge.Iterator) {\n                references[idx] = @bitCast(@intFromPtr(value.func));\n                idx += 1;\n            } else if (T == bridge.Indexed) {\n                references[idx] = @bitCast(@intFromPtr(value.getter));\n                idx += 1;\n                if (value.enumerator) |enumerator| {\n                    references[idx] = @bitCast(@intFromPtr(enumerator));\n                    idx += 1;\n                }\n            } else if (T == bridge.NamedIndexed) {\n                references[idx] = @bitCast(@intFromPtr(value.getter));\n                idx += 1;\n                if (value.setter) |setter| {\n                    references[idx] = @bitCast(@intFromPtr(setter));\n                    idx += 1;\n                }\n                if (value.deleter) |deleter| {\n                    references[idx] = @bitCast(@intFromPtr(deleter));\n                    idx += 1;\n                }\n            }\n        }\n    }\n\n    // In debug mode, collect unknown property callbacks for types without NamedIndexed\n    if (comptime IS_DEBUG) {\n        inline for (JsApis) |JsApi| {\n            if (!hasNamedIndexedGetter(JsApi)) {\n                references[idx] = @bitCast(@intFromPtr(bridge.unknownObjectPropertyCallback(JsApi)));\n                idx += 1;\n            }\n        }\n    }\n\n    return references;\n}\n\n// Even if a struct doesn't have a `constructor` function, we still\n// `generateConstructor`, because this is how we create our\n// FunctionTemplate. Such classes exist, but they can't be instantiated\n// via `new ClassName()` - but they could, for example, be created in\n// Zig and returned from a function call, which is why we need the\n// FunctionTemplate.\nfn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {\n    const callback = blk: {\n        if (@hasDecl(JsApi, \"constructor\")) {\n            break :blk JsApi.constructor.func;\n        }\n\n        // Use shared illegal constructor callback\n        break :blk illegalConstructorCallback;\n    };\n\n    const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);\n    {\n        const internal_field_count = comptime countInternalFields(JsApi);\n        if (internal_field_count > 0) {\n            const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);\n            v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);\n        }\n    }\n    const name_str = if (@hasDecl(JsApi.Meta, \"name\")) JsApi.Meta.name else @typeName(JsApi);\n    const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));\n    v8.v8__FunctionTemplate__SetClassName(template, class_name);\n    return template;\n}\n\npub fn countInternalFields(comptime JsApi: type) u8 {\n    var last_used_id = 0;\n    var cache_count: u8 = 0;\n\n    inline for (@typeInfo(JsApi).@\"struct\".decls) |d| {\n        const name: [:0]const u8 = d.name;\n        const value = @field(JsApi, name);\n        const definition = @TypeOf(value);\n\n        switch (definition) {\n            inline bridge.Accessor, bridge.Function => {\n                const cache = value.cache orelse continue;\n                if (cache != .internal) {\n                    continue;\n                }\n                // We assert that they are declared in-order. This isn't necessary\n                // but I don't want to do anything fancy to look for gaps or\n                // duplicates.\n                const internal_id = cache.internal;\n                if (internal_id != last_used_id + 1) {\n                    @compileError(@typeName(JsApi) ++ \".\" ++ name ++ \" has a non-monotonic cache index\");\n                }\n                last_used_id = internal_id;\n                cache_count += 1; // this is just last_used, but it's more explicit this way\n            },\n            else => {},\n        }\n    }\n\n    if (@hasDecl(JsApi.Meta, \"empty_with_no_proto\")) {\n        return cache_count;\n    }\n\n    // we need cache_count internal fields, + 1 for the TAO pointer (the v8 -> Zig)\n    // mapping) itself.\n    return cache_count + 1;\n}\n\n// Attaches JsApi members to the prototype template (normal case)\nfn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {\n    const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);\n    const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);\n\n    const declarations = @typeInfo(JsApi).@\"struct\".decls;\n    var has_named_index_getter = false;\n\n    inline for (declarations) |d| {\n        const name: [:0]const u8 = d.name;\n        const value = @field(JsApi, name);\n        const definition = @TypeOf(value);\n\n        switch (definition) {\n            bridge.Accessor => {\n                const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));\n                const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.getter }).?);\n                if (value.setter == null) {\n                    if (value.static) {\n                        v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);\n                    } else {\n                        v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);\n                    }\n                } else {\n                    if (comptime IS_DEBUG) {\n                        std.debug.assert(value.static == false);\n                    }\n                    const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);\n                    v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);\n                }\n            },\n            bridge.Function => {\n                const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?);\n                const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));\n                if (value.static) {\n                    v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);\n                } else {\n                    v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);\n                }\n            },\n            bridge.Indexed => {\n                var configuration: v8.IndexedPropertyHandlerConfiguration = .{\n                    .getter = value.getter,\n                    .enumerator = value.enumerator,\n                    .setter = null,\n                    .query = null,\n                    .deleter = null,\n                    .definer = null,\n                    .descriptor = null,\n                    .data = null,\n                    .flags = 0,\n                };\n                v8.v8__ObjectTemplate__SetIndexedHandler(instance, &configuration);\n            },\n            bridge.NamedIndexed => {\n                var configuration: v8.NamedPropertyHandlerConfiguration = .{\n                    .getter = value.getter,\n                    .setter = value.setter,\n                    .query = null,\n                    .deleter = value.deleter,\n                    .enumerator = null,\n                    .definer = null,\n                    .descriptor = null,\n                    .data = null,\n                    .flags = v8.kOnlyInterceptStrings | v8.kNonMasking,\n                };\n                v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);\n                has_named_index_getter = true;\n            },\n            bridge.Iterator => {\n                const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?);\n                const js_name = if (value.async)\n                    v8.v8__Symbol__GetAsyncIterator(isolate)\n                else\n                    v8.v8__Symbol__GetIterator(isolate);\n                v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);\n            },\n            bridge.Property => {\n                const js_value = switch (value.value) {\n                    .null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),\n                    inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),\n                };\n                const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));\n\n                {\n                    const flags = if (value.readonly) v8.ReadOnly + v8.DontDelete else 0;\n                    v8.v8__Template__Set(@ptrCast(prototype), js_name, js_value, flags);\n                }\n\n                if (value.template) {\n                    // apply it both to the type itself (e.g. Node.Elem)\n                    v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);\n                }\n            },\n            bridge.Constructor => {}, // already handled in generateConstructor\n            else => {},\n        }\n    }\n\n    if (@hasDecl(JsApi.Meta, \"htmldda\")) {\n        v8.v8__ObjectTemplate__MarkAsUndetectable(instance);\n        v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);\n    }\n\n    if (@hasDecl(JsApi.Meta, \"name\")) {\n        const js_name = v8.v8__Symbol__GetToStringTag(isolate);\n        const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));\n        v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);\n    }\n\n    if (comptime IS_DEBUG) {\n        if (!has_named_index_getter) {\n            var configuration: v8.NamedPropertyHandlerConfiguration = .{\n                .getter = bridge.unknownObjectPropertyCallback(JsApi),\n                .setter = null,\n                .query = null,\n                .deleter = null,\n                .enumerator = null,\n                .definer = null,\n                .descriptor = null,\n                .data = null,\n                .flags = v8.kOnlyInterceptStrings | v8.kNonMasking,\n            };\n            v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);\n        }\n    }\n}\n\nfn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {\n    @setEvalBranchQuota(2000);\n    comptime {\n        const T = JsApi.bridge.type;\n        if (!@hasField(T, \"_proto\")) {\n            return null;\n        }\n        const Ptr = std.meta.fieldInfo(T, ._proto).type;\n        const F = @typeInfo(Ptr).pointer.child;\n        return bridge.JsApiLookup.getId(F.JsApi);\n    }\n}\n\n// Shared illegal constructor callback for types without explicit constructors\nfn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n    const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);\n    log.warn(.js, \"Illegal constructor call\", .{});\n\n    const message = v8.v8__String__NewFromUtf8(isolate, \"Illegal Constructor\", v8.kNormal, 19);\n    const js_exception = v8.v8__Exception__TypeError(message);\n\n    _ = v8.v8__Isolate__ThrowException(isolate, js_exception);\n    var return_value: v8.ReturnValue = undefined;\n    v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);\n    v8.v8__ReturnValue__Set(return_value, js_exception);\n}\n"
  },
  {
    "path": "src/browser/js/String.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst SSO = @import(\"../../string.zig\").String;\n\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst v8 = js.v8;\n\nconst String = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.String,\n\npub fn toSlice(self: String) ![]u8 {\n    return self._toSlice(false, self.local.call_arena);\n}\npub fn toSliceZ(self: String) ![:0]u8 {\n    return self._toSlice(true, self.local.call_arena);\n}\npub fn toSliceWithAlloc(self: String, allocator: Allocator) ![]u8 {\n    return self._toSlice(false, allocator);\n}\nfn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !(if (null_terminate) [:0]u8 else []u8) {\n    const local = self.local;\n    const handle = self.handle;\n    const isolate = local.isolate.handle;\n\n    const len = v8.v8__String__Utf8Length(handle, isolate);\n    const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));\n    const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);\n    if (comptime IS_DEBUG) {\n        std.debug.assert(n == len);\n    }\n\n    return buf;\n}\n\npub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {\n    if (comptime global) {\n        return .{ .str = try self.toSSOWithAlloc(self.local.ctx.origin.arena) };\n    }\n    return self.toSSOWithAlloc(self.local.call_arena);\n}\npub fn toSSOWithAlloc(self: String, allocator: Allocator) !SSO {\n    const handle = self.handle;\n    const isolate = self.local.isolate.handle;\n\n    const len: usize = @intCast(v8.v8__String__Utf8Length(handle, isolate));\n\n    if (len <= 12) {\n        var content: [12]u8 = undefined;\n        const n = v8.v8__String__WriteUtf8(handle, isolate, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);\n        if (comptime IS_DEBUG) {\n            std.debug.assert(n == len);\n        }\n        // Weird that we do this _after_, but we have to..I've seen weird issues\n        // in ReleaseMode where v8 won't write to content if it starts off zero\n        // initiated\n        @memset(content[len..], 0);\n        return .{ .len = @intCast(len), .payload = .{ .content = content } };\n    }\n\n    const buf = try allocator.alloc(u8, len);\n    const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);\n    if (comptime IS_DEBUG) {\n        std.debug.assert(n == len);\n    }\n\n    var prefix: [4]u8 = @splat(0);\n    @memcpy(&prefix, buf[0..4]);\n\n    return .{\n        .len = @intCast(len),\n        .payload = .{ .heap = .{\n            .prefix = prefix,\n            .ptr = buf.ptr,\n        } },\n    };\n}\n\npub fn format(self: String, writer: *std.Io.Writer) !void {\n    const local = self.local;\n    const handle = self.handle;\n    const isolate = local.isolate.handle;\n\n    var small: [1024]u8 = undefined;\n    const len = v8.v8__String__Utf8Length(handle, isolate);\n    var buf = if (len < 1024) &small else local.call_arena.alloc(u8, @intCast(len)) catch return error.WriteFailed;\n\n    const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);\n    return writer.writeAll(buf[0..n]);\n}\n"
  },
  {
    "path": "src/browser/js/TaggedOpaque.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\nconst bridge = js.bridge;\n\n// When we return a Zig object to V8, we put it on the heap and pass it into\n// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a\n// function parameter, we know what type it _should_ be.\n//\n// In a simple/perfect world, we could use this knowledge to cast the *anyopaque\n// to the parameter type:\n//   const arg: @typeInfo(@TypeOf(function)).@\"fn\".params[0] = @ptrCast(v8_data);\n//\n// But there are 2 reasons we can't do that.\n//\n// == Reason 1 ==\n// The JS code might pass the wrong type:\n//\n//   var cat = new Cat();\n//   cat.setOwner(new Cat());\n//\n// The zig_setOwner method expects the 2nd parameter to be an *Owner, but\n// the JS code passed a *Cat.\n//\n// To solve this issue, we tag every returned value so that we can check what\n// type it is. In the above case, we'd expect an *Owner, but the tag would tell\n// us that we got a *Cat. We use the type index in our Types lookup as the tag.\n//\n// == Reason 2 ==\n// Because of prototype inheritance, even \"correct\" code can be a challenge. For\n// example, say the above JavaScript is fixed:\n//\n//   var cat = new Cat();\n//   cat.setOwner(new Owner(\"Leto\"));\n//\n// The issue is that setOwner might not expect an *Owner, but rather a\n// *Person, which is the prototype for Owner. Now our Zig code is expecting\n// a *Person, but it was (correctly) given an *Owner.\n// For this reason, we also store the prototype chain.\nconst TaggedOpaque = @This();\n\nprototype_len: u16,\nprototype_chain: [*]const PrototypeChainEntry,\n\n// Ptr to the Zig instance. Between the context where it's called (i.e.\n// we have the comptime parameter info for all functions), and the index field\n// we can figure out what type this is.\nvalue: *anyopaque,\n\n// When we're asked to describe an object via the Inspector, we _must_ include\n// the proper subtype (and description) fields in the returned JSON.\n// V8 will give us a Value and ask us for the subtype. From the js.Value we\n// can get a js.Object, and from the js.Object, we can get out TaggedOpaque\n// which is where we store the subtype.\nsubtype: ?bridge.SubType,\n\npub const PrototypeChainEntry = struct {\n    index: bridge.JsApiLookup.BackingInt,\n    offset: u16, // offset to the _proto field\n};\n\n// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque\n// contains a ptr to the correct type.\npub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {\n    const ti = @typeInfo(R);\n    if (ti != .pointer) {\n        @compileError(\"non-pointer Zig parameter type: \" ++ @typeName(R));\n    }\n\n    const T = ti.pointer.child;\n    const JsApi = bridge.Struct(T).JsApi;\n\n    if (@hasDecl(JsApi.Meta, \"empty_with_no_proto\")) {\n        // Empty structs aren't stored as TOAs and there's no data\n        // stored in the JSObject's IntenrnalField. Why bother when\n        // we can just return an empty struct here?\n        return @constCast(@as(*const T, &.{}));\n    }\n\n    const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);\n    // if it isn't an empty struct, then the v8.Object should have an\n    // InternalFieldCount > 0, since our toa pointer should be embedded\n    // at index 0 of the internal field count.\n    if (internal_field_count == 0) {\n        return error.InvalidArgument;\n    }\n\n    if (!bridge.JsApiLookup.has(JsApi)) {\n        @compileError(\"unknown Zig type: \" ++ @typeName(R));\n    }\n\n    const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0).?;\n    const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));\n    const expected_type_index = bridge.JsApiLookup.getId(JsApi);\n\n    const prototype_chain = tao.prototype_chain[0..tao.prototype_len];\n    if (prototype_chain[0].index == expected_type_index) {\n        return @ptrCast(@alignCast(tao.value));\n    }\n\n    // Ok, let's walk up the chain\n    var ptr = @intFromPtr(tao.value);\n    for (prototype_chain[1..]) |proto| {\n        ptr += proto.offset; // the offset to the _proto field\n        const proto_ptr: **anyopaque = @ptrFromInt(ptr);\n        if (proto.index == expected_type_index) {\n            return @ptrCast(@alignCast(proto_ptr.*));\n        }\n        ptr = @intFromPtr(proto_ptr.*);\n    }\n    return error.InvalidArgument;\n}\n"
  },
  {
    "path": "src/browser/js/TryCatch.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst v8 = js.v8;\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Allocator = std.mem.Allocator;\n\nconst TryCatch = @This();\n\nhandle: v8.TryCatch,\nlocal: *const js.Local,\n\npub fn init(self: *TryCatch, l: *const js.Local) void {\n    self.local = l;\n    v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);\n}\n\npub fn hasCaught(self: TryCatch) bool {\n    return v8.v8__TryCatch__HasCaught(&self.handle);\n}\n\npub fn rethrow(self: *TryCatch) void {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.hasCaught());\n    }\n    _ = v8.v8__TryCatch__ReThrow(&self.handle);\n}\n\npub fn caught(self: TryCatch, allocator: Allocator) ?Caught {\n    if (self.hasCaught() == false) {\n        return null;\n    }\n\n    const l = self.local;\n    const line: ?u32 = blk: {\n        const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;\n        const line = v8.v8__Message__GetLineNumber(handle, l.handle);\n        break :blk if (line < 0) null else @intCast(line);\n    };\n\n    const exception: ?[]const u8 = blk: {\n        const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;\n        var js_val = js.Value{ .local = l, .handle = handle };\n\n        // If it's an Error object, try to get the message property\n        if (js_val.isObject()) {\n            const js_obj = js_val.toObject();\n            if (js_obj.has(\"message\")) {\n                js_val = js_obj.get(\"message\") catch break :blk null;\n            }\n        }\n\n        if (js_val.isString()) |js_str| {\n            break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);\n        }\n        break :blk null;\n    };\n\n    const stack: ?[]const u8 = blk: {\n        const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;\n        var js_val = js.Value{ .local = l, .handle = handle };\n\n        // If it's an Error object, try to get the stack property\n        if (js_val.isObject()) {\n            const js_obj = js_val.toObject();\n            if (js_obj.has(\"stack\")) {\n                js_val = js_obj.get(\"stack\") catch break :blk null;\n            }\n        }\n\n        if (js_val.isString()) |js_str| {\n            break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);\n        }\n        break :blk null;\n    };\n\n    return .{\n        .line = line,\n        .stack = stack,\n        .caught = true,\n        .exception = exception,\n    };\n}\n\npub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {\n    return self.caught(allocator) orelse .{\n        .caught = false,\n        .line = null,\n        .stack = null,\n        .exception = @errorName(err),\n    };\n}\n\npub fn deinit(self: *TryCatch) void {\n    v8.v8__TryCatch__DESTRUCT(&self.handle);\n}\n\npub const Caught = struct {\n    line: ?u32 = null,\n    caught: bool = false,\n    stack: ?[]const u8 = null,\n    exception: ?[]const u8 = null,\n\n    pub fn format(self: Caught, writer: *std.Io.Writer) !void {\n        const separator = @import(\"../../log.zig\").separator();\n        try writer.print(\"{s}exception: {?s}\", .{ separator, self.exception });\n        try writer.print(\"{s}stack: {?s}\", .{ separator, self.stack });\n        try writer.print(\"{s}line: {?d}\", .{ separator, self.line });\n        try writer.print(\"{s}caught: {any}\", .{ separator, self.caught });\n    }\n\n    pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {\n        try writer.write(prefix ++ \".exception\", self.exception orelse \"???\");\n        try writer.write(prefix ++ \".stack\", self.stack orelse \"na\");\n        try writer.write(prefix ++ \".line\", self.line);\n        try writer.write(prefix ++ \".caught\", self.caught);\n    }\n\n    pub fn jsonStringify(self: Caught, jw: anytype) !void {\n        try jw.beginObject();\n        try jw.objectField(\"exception\");\n        try jw.write(self.exception);\n        try jw.objectField(\"stack\");\n        try jw.write(self.stack);\n        try jw.objectField(\"line\");\n        try jw.write(self.line);\n        try jw.objectField(\"caught\");\n        try jw.write(self.caught);\n        try jw.endObject();\n    }\n};\n"
  },
  {
    "path": "src/browser/js/Value.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst SSO = @import(\"../../string.zig\").String;\n\nconst v8 = js.v8;\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Allocator = std.mem.Allocator;\n\nconst Value = @This();\n\nlocal: *const js.Local,\nhandle: *const v8.Value,\n\npub fn isObject(self: Value) bool {\n    return v8.v8__Value__IsObject(self.handle);\n}\n\npub fn isString(self: Value) ?js.String {\n    const handle = self.handle;\n    if (!v8.v8__Value__IsString(handle)) {\n        return null;\n    }\n    return .{ .local = self.local, .handle = @ptrCast(handle) };\n}\n\npub fn isArray(self: Value) bool {\n    return v8.v8__Value__IsArray(self.handle);\n}\n\npub fn isSymbol(self: Value) bool {\n    return v8.v8__Value__IsSymbol(self.handle);\n}\n\npub fn isFunction(self: Value) bool {\n    return v8.v8__Value__IsFunction(self.handle);\n}\n\npub fn isNull(self: Value) bool {\n    return v8.v8__Value__IsNull(self.handle);\n}\n\npub fn isUndefined(self: Value) bool {\n    return v8.v8__Value__IsUndefined(self.handle);\n}\n\npub fn isNullOrUndefined(self: Value) bool {\n    return v8.v8__Value__IsNullOrUndefined(self.handle);\n}\n\npub fn isNumber(self: Value) bool {\n    return v8.v8__Value__IsNumber(self.handle);\n}\n\npub fn isNumberObject(self: Value) bool {\n    return v8.v8__Value__IsNumberObject(self.handle);\n}\n\npub fn isInt32(self: Value) bool {\n    return v8.v8__Value__IsInt32(self.handle);\n}\n\npub fn isUint32(self: Value) bool {\n    return v8.v8__Value__IsUint32(self.handle);\n}\n\npub fn isBigInt(self: Value) bool {\n    return v8.v8__Value__IsBigInt(self.handle);\n}\n\npub fn isBigIntObject(self: Value) bool {\n    return v8.v8__Value__IsBigIntObject(self.handle);\n}\n\npub fn isBoolean(self: Value) bool {\n    return v8.v8__Value__IsBoolean(self.handle);\n}\n\npub fn isBooleanObject(self: Value) bool {\n    return v8.v8__Value__IsBooleanObject(self.handle);\n}\n\npub fn isTrue(self: Value) bool {\n    return v8.v8__Value__IsTrue(self.handle);\n}\n\npub fn isFalse(self: Value) bool {\n    return v8.v8__Value__IsFalse(self.handle);\n}\n\npub fn isTypedArray(self: Value) bool {\n    return v8.v8__Value__IsTypedArray(self.handle);\n}\n\npub fn isArrayBufferView(self: Value) bool {\n    return v8.v8__Value__IsArrayBufferView(self.handle);\n}\n\npub fn isArrayBuffer(self: Value) bool {\n    return v8.v8__Value__IsArrayBuffer(self.handle);\n}\n\npub fn isUint8Array(self: Value) bool {\n    return v8.v8__Value__IsUint8Array(self.handle);\n}\n\npub fn isUint8ClampedArray(self: Value) bool {\n    return v8.v8__Value__IsUint8ClampedArray(self.handle);\n}\n\npub fn isInt8Array(self: Value) bool {\n    return v8.v8__Value__IsInt8Array(self.handle);\n}\n\npub fn isUint16Array(self: Value) bool {\n    return v8.v8__Value__IsUint16Array(self.handle);\n}\n\npub fn isInt16Array(self: Value) bool {\n    return v8.v8__Value__IsInt16Array(self.handle);\n}\n\npub fn isUint32Array(self: Value) bool {\n    return v8.v8__Value__IsUint32Array(self.handle);\n}\n\npub fn isInt32Array(self: Value) bool {\n    return v8.v8__Value__IsInt32Array(self.handle);\n}\n\npub fn isBigUint64Array(self: Value) bool {\n    return v8.v8__Value__IsBigUint64Array(self.handle);\n}\n\npub fn isBigInt64Array(self: Value) bool {\n    return v8.v8__Value__IsBigInt64Array(self.handle);\n}\n\npub fn isPromise(self: Value) bool {\n    return v8.v8__Value__IsPromise(self.handle);\n}\n\npub fn toBool(self: Value) bool {\n    return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);\n}\n\npub fn typeOf(self: Value) js.String {\n    const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;\n    return js.String{ .local = self.local, .handle = str_handle };\n}\n\npub fn toF32(self: Value) !f32 {\n    return @floatCast(try self.toF64());\n}\n\npub fn toF64(self: Value) !f64 {\n    var maybe: v8.MaybeF64 = undefined;\n    v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);\n    if (!maybe.has_value) {\n        return error.JsException;\n    }\n    return maybe.value;\n}\n\npub fn toI32(self: Value) !i32 {\n    var maybe: v8.MaybeI32 = undefined;\n    v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);\n    if (!maybe.has_value) {\n        return error.JsException;\n    }\n    return maybe.value;\n}\n\npub fn toU32(self: Value) !u32 {\n    var maybe: v8.MaybeU32 = undefined;\n    v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);\n    if (!maybe.has_value) {\n        return error.JsException;\n    }\n    return maybe.value;\n}\n\npub fn toPromise(self: Value) js.Promise {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.isPromise());\n    }\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn toString(self: Value) !js.String {\n    const l = self.local;\n    const value_handle: *const v8.Value = blk: {\n        if (self.isSymbol()) {\n            break :blk @ptrCast(v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?);\n        }\n        break :blk self.handle;\n    };\n\n    const str_handle = v8.v8__Value__ToString(value_handle, l.handle) orelse return error.JsException;\n    return .{ .local = self.local, .handle = str_handle };\n}\n\npub fn toSSO(self: Value, comptime global: bool) !(if (global) SSO.Global else SSO) {\n    return (try self.toString()).toSSO(global);\n}\npub fn toSSOWithAlloc(self: Value, allocator: Allocator) !SSO {\n    return (try self.toString()).toSSOWithAlloc(allocator);\n}\n\npub fn toStringSlice(self: Value) ![]u8 {\n    return (try self.toString()).toSlice();\n}\npub fn toStringSliceZ(self: Value) ![:0]u8 {\n    return (try self.toString()).toSliceZ();\n}\npub fn toStringSliceWithAlloc(self: Value, allocator: Allocator) ![]u8 {\n    return (try self.toString()).toSliceWithAlloc(allocator);\n}\n\npub fn toJson(self: Value, allocator: Allocator) ![]u8 {\n    const local = self.local;\n    const str_handle = v8.v8__JSON__Stringify(local.handle, self.handle, null) orelse return error.JsException;\n    return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);\n}\n\n// Currently does not support host objects (Blob, File, etc.) or transferables\n// which require delegate callbacks to be implemented.\npub fn structuredClone(self: Value) !Value {\n    const local = self.local;\n    const v8_context = local.handle;\n    const v8_isolate = local.isolate.handle;\n\n    const size, const data = blk: {\n        const serializer = v8.v8__ValueSerializer__New(v8_isolate, null) orelse return error.JsException;\n        defer v8.v8__ValueSerializer__DELETE(serializer);\n\n        var write_result: v8.MaybeBool = undefined;\n        v8.v8__ValueSerializer__WriteHeader(serializer);\n        v8.v8__ValueSerializer__WriteValue(serializer, v8_context, self.handle, &write_result);\n        if (!write_result.has_value or !write_result.value) {\n            return error.JsException;\n        }\n\n        var size: usize = undefined;\n        const data = v8.v8__ValueSerializer__Release(serializer, &size) orelse return error.JsException;\n        break :blk .{ size, data };\n    };\n\n    defer v8.v8__ValueSerializer__FreeBuffer(data);\n\n    const cloned_handle = blk: {\n        const deserializer = v8.v8__ValueDeserializer__New(v8_isolate, data, size, null) orelse return error.JsException;\n        defer v8.v8__ValueDeserializer__DELETE(deserializer);\n\n        var read_header_result: v8.MaybeBool = undefined;\n        v8.v8__ValueDeserializer__ReadHeader(deserializer, v8_context, &read_header_result);\n        if (!read_header_result.has_value or !read_header_result.value) {\n            return error.JsException;\n        }\n        break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, v8_context) orelse return error.JsException;\n    };\n\n    return .{ .local = local, .handle = cloned_handle };\n}\n\npub fn persist(self: Value) !Global {\n    return self._persist(true);\n}\n\npub fn temp(self: Value) !Temp {\n    return self._persist(false);\n}\n\nfn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Global else Temp) {\n    var ctx = self.local.ctx;\n\n    var global: v8.Global = undefined;\n    v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n    if (comptime is_global) {\n        try ctx.trackGlobal(global);\n        return .{ .handle = global, .origin = {} };\n    }\n    try ctx.trackTemp(global);\n    return .{ .handle = global, .origin = ctx.origin };\n}\n\npub fn toZig(self: Value, comptime T: type) !T {\n    return self.local.jsValueToZig(T, self);\n}\n\npub fn toObject(self: Value) js.Object {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.isObject());\n    }\n\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn toArray(self: Value) js.Array {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.isArray());\n    }\n\n    return .{\n        .local = self.local,\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn toBigInt(self: Value) js.BigInt {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(self.isBigInt());\n    }\n\n    return .{\n        .handle = @ptrCast(self.handle),\n    };\n}\n\npub fn format(self: Value, writer: *std.Io.Writer) !void {\n    if (comptime IS_DEBUG) {\n        return self.local.debugValue(self, writer);\n    }\n    const js_str = self.toString() catch return error.WriteFailed;\n    return js_str.format(writer);\n}\n\npub const Temp = G(.temp);\npub const Global = G(.global);\n\nconst GlobalType = enum(u8) {\n    temp,\n    global,\n};\n\nfn G(comptime global_type: GlobalType) type {\n    return struct {\n        handle: v8.Global,\n        origin: if (global_type == .temp) *js.Origin else void,\n\n        const Self = @This();\n\n        pub fn deinit(self: *Self) void {\n            v8.v8__Global__Reset(&self.handle);\n        }\n\n        pub fn local(self: *const Self, l: *const js.Local) Value {\n            return .{\n                .local = l,\n                .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),\n            };\n        }\n\n        pub fn isEqual(self: *const Self, other: Value) bool {\n            return v8.v8__Global__IsEqual(&self.handle, other.handle);\n        }\n\n        pub fn release(self: *const Self) void {\n            self.origin.releaseTemp(self.handle);\n        }\n    };\n}\n"
  },
  {
    "path": "src/browser/js/bridge.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"js.zig\");\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"../../log.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst v8 = js.v8;\n\nconst Caller = @import(\"Caller.zig\");\nconst Context = @import(\"Context.zig\");\nconst Origin = @import(\"Origin.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub fn Builder(comptime T: type) type {\n    return struct {\n        pub const @\"type\" = T;\n        pub const ClassId = u16;\n\n        pub fn constructor(comptime func: anytype, comptime opts: Constructor.Opts) Constructor {\n            return Constructor.init(T, func, opts);\n        }\n\n        pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {\n            return Accessor.init(T, getter, setter, opts);\n        }\n\n        pub fn function(comptime func: anytype, comptime opts: Caller.Function.Opts) Function {\n            return Function.init(T, func, opts);\n        }\n\n        pub fn indexed(comptime getter_func: anytype, comptime enumerator_func: anytype, comptime opts: Indexed.Opts) Indexed {\n            return Indexed.init(T, getter_func, enumerator_func, opts);\n        }\n\n        pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {\n            return NamedIndexed.init(T, getter_func, setter_func, deleter_func, opts);\n        }\n\n        pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator {\n            return Iterator.init(T, func, opts);\n        }\n\n        pub fn callable(comptime func: anytype, comptime opts: Callable.Opts) Callable {\n            return Callable.init(T, func, opts);\n        }\n\n        pub fn property(value: anytype, opts: Property.Opts) Property {\n            switch (@typeInfo(@TypeOf(value))) {\n                .bool => return Property.init(.{ .bool = value }, opts),\n                .null => return Property.init(.null, opts),\n                .comptime_int, .int => return Property.init(.{ .int = value }, opts),\n                .comptime_float, .float => return Property.init(.{ .float = value }, opts),\n                .pointer => |ptr| switch (ptr.size) {\n                    .one => {\n                        const one_info = @typeInfo(ptr.child);\n                        if (one_info == .array and one_info.array.child == u8) {\n                            return Property.init(.{ .string = value }, opts);\n                        }\n                    },\n                    else => {},\n                },\n                else => {},\n            }\n            @compileError(\"Property for \" ++ @typeName(@TypeOf(value)) ++ \" hasn't been defined yet\");\n        }\n\n        const PrototypeChainEntry = @import(\"TaggedOpaque.zig\").PrototypeChainEntry;\n        pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {\n            var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;\n\n            entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };\n\n            if (entries.len == 1) {\n                return entries;\n            }\n\n            var Prototype = T;\n            inline for (entries[1..]) |*entry| {\n                const Next = PrototypeType(Prototype).?;\n                entry.* = .{\n                    .index = JsApiLookup.getId(Next.JsApi),\n                    .offset = @offsetOf(Prototype, \"_proto\"),\n                };\n                Prototype = Next;\n            }\n            return entries;\n        }\n\n        pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {\n            return .{\n                .from_zig = struct {\n                    fn wrap(ptr: *anyopaque, session: *Session) void {\n                        func(@ptrCast(@alignCast(ptr)), true, session);\n                    }\n                }.wrap,\n\n                .from_v8 = struct {\n                    fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {\n                        const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;\n                        const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr));\n\n                        const origin = fc.origin;\n                        const value_ptr = fc.ptr;\n                        if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {\n                            func(@ptrCast(@alignCast(value_ptr)), false, fc.session);\n                            origin.release(value_ptr);\n                        } else {\n                            // A bit weird, but v8 _requires_ that we release it\n                            // If we don't. We'll 100% crash.\n                            v8.v8__Global__Reset(&fc.global);\n                        }\n                    }\n                }.wrap,\n            };\n        }\n    };\n}\n\npub const Constructor = struct {\n    func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,\n\n    const Opts = struct {\n        dom_exception: bool = false,\n    };\n\n    fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {\n        return .{ .func = struct {\n            fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;\n                var caller: Caller = undefined;\n                caller.init(v8_isolate);\n                defer caller.deinit();\n\n                caller.constructor(T, func, handle.?, .{\n                    .dom_exception = opts.dom_exception,\n                });\n            }\n        }.wrap };\n    }\n};\n\npub const Function = struct {\n    static: bool,\n    arity: usize,\n    noop: bool = false,\n    cache: ?Caller.Function.Opts.Caching = null,\n    func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,\n\n    fn init(comptime T: type, comptime func: anytype, comptime opts: Caller.Function.Opts) Function {\n        return .{\n            .cache = opts.cache,\n            .static = opts.static,\n            .arity = getArity(@TypeOf(func)),\n            .func = if (opts.noop) noopFunction else struct {\n                fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                    Caller.Function.call(T, handle.?, func, opts);\n                }\n            }.wrap,\n        };\n    }\n\n    pub fn noopFunction(_: ?*const v8.FunctionCallbackInfo) callconv(.c) void {}\n\n    fn getArity(comptime T: type) usize {\n        var count: usize = 0;\n        var params = @typeInfo(T).@\"fn\".params;\n        for (params[1..]) |p| { // start at 1, skip self\n            const PT = p.type.?;\n            if (PT == *Page or PT == *const Page) {\n                break;\n            }\n            if (@typeInfo(PT) == .optional) {\n                break;\n            }\n            count += 1;\n        }\n        return count;\n    }\n};\n\npub const Accessor = struct {\n    static: bool = false,\n    cache: ?Caller.Function.Opts.Caching = null,\n    getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,\n    setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,\n\n    fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {\n        var accessor = Accessor{\n            .cache = opts.cache,\n            .static = opts.static,\n        };\n\n        if (@typeInfo(@TypeOf(getter)) != .null) {\n            accessor.getter = struct {\n                fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                    Caller.Function.call(T, handle.?, getter, opts);\n                }\n            }.wrap;\n        }\n\n        if (@typeInfo(@TypeOf(setter)) != .null) {\n            accessor.setter = struct {\n                fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                    Caller.Function.call(T, handle.?, setter, opts);\n                }\n            }.wrap;\n        }\n\n        return accessor;\n    }\n};\n\npub const Indexed = struct {\n    getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,\n    enumerator: ?*const fn (handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,\n\n    const Opts = struct {\n        as_typed_array: bool = false,\n        null_as_undefined: bool = false,\n    };\n\n    fn init(comptime T: type, comptime getter: anytype, comptime enumerator: anytype, comptime opts: Opts) Indexed {\n        var indexed = Indexed{\n            .enumerator = null,\n            .getter = struct {\n                fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n                    const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n                    var caller: Caller = undefined;\n                    caller.init(v8_isolate);\n                    defer caller.deinit();\n\n                    return caller.getIndex(T, getter, idx, handle.?, .{\n                        .as_typed_array = opts.as_typed_array,\n                        .null_as_undefined = opts.null_as_undefined,\n                    });\n                }\n            }.wrap,\n        };\n\n        if (@typeInfo(@TypeOf(enumerator)) != .null) {\n            indexed.enumerator = struct {\n                fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n                    const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n                    var caller: Caller = undefined;\n                    caller.init(v8_isolate);\n                    defer caller.deinit();\n                    return caller.getEnumerator(T, enumerator, handle.?, .{});\n                }\n            }.wrap;\n        }\n\n        return indexed;\n    }\n};\n\npub const NamedIndexed = struct {\n    getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,\n    setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,\n    deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,\n\n    const Opts = struct {\n        as_typed_array: bool = false,\n        null_as_undefined: bool = false,\n    };\n\n    fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {\n        const getter_fn = struct {\n            fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n                const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n                var caller: Caller = undefined;\n                caller.init(v8_isolate);\n                defer caller.deinit();\n\n                return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{\n                    .as_typed_array = opts.as_typed_array,\n                    .null_as_undefined = opts.null_as_undefined,\n                });\n            }\n        }.wrap;\n\n        const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {\n            fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n                const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n                var caller: Caller = undefined;\n                caller.init(v8_isolate);\n                defer caller.deinit();\n\n                return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{\n                    .as_typed_array = opts.as_typed_array,\n                    .null_as_undefined = opts.null_as_undefined,\n                });\n            }\n        }.wrap;\n\n        const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {\n            fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n                const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n                var caller: Caller = undefined;\n                caller.init(v8_isolate);\n                defer caller.deinit();\n\n                return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{\n                    .as_typed_array = opts.as_typed_array,\n                    .null_as_undefined = opts.null_as_undefined,\n                });\n            }\n        }.wrap;\n\n        return .{\n            .getter = getter_fn,\n            .setter = setter_fn,\n            .deleter = deleter_fn,\n        };\n    }\n};\n\npub const Iterator = struct {\n    func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,\n    async: bool,\n\n    const Opts = struct {\n        async: bool = false,\n        null_as_undefined: bool = false,\n    };\n\n    fn init(comptime T: type, comptime struct_or_func: anytype, comptime opts: Opts) Iterator {\n        if (@typeInfo(@TypeOf(struct_or_func)) == .type) {\n            return .{\n                .async = opts.async,\n                .func = struct {\n                    fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                        const info = Caller.FunctionCallbackInfo{ .handle = handle.? };\n                        info.getReturnValue().set(info.getThis());\n                    }\n                }.wrap,\n            };\n        }\n\n        return .{\n            .async = opts.async,\n            .func = struct {\n                fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                    return Caller.Function.call(T, handle.?, struct_or_func, .{\n                        .null_as_undefined = opts.null_as_undefined,\n                    });\n                }\n            }.wrap,\n        };\n    }\n};\n\npub const Callable = struct {\n    func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,\n\n    const Opts = struct {\n        null_as_undefined: bool = false,\n    };\n\n    fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {\n        return .{ .func = struct {\n            fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {\n                Caller.Function.call(T, handle.?, func, .{\n                    .null_as_undefined = opts.null_as_undefined,\n                });\n            }\n        }.wrap };\n    }\n};\n\npub const Property = struct {\n    value: Value,\n    template: bool,\n    readonly: bool,\n\n    const Value = union(enum) {\n        null,\n        int: i64,\n        float: f64,\n        bool: bool,\n        string: []const u8,\n    };\n\n    const Opts = struct {\n        template: bool,\n        readonly: bool = true,\n    };\n\n    fn init(value: Value, opts: Opts) Property {\n        return .{\n            .value = value,\n            .template = opts.template,\n            .readonly = opts.readonly,\n        };\n    }\n};\n\nconst Finalizer = struct {\n    // The finalizer wrapper when called from Zig. This is only called on\n    // Origin.deinit\n    from_zig: *const fn (ctx: *anyopaque, session: *Session) void,\n\n    // The finalizer wrapper when called from V8. This may never be called\n    // (hence why we fallback to calling in Origin.deinit). If it is called,\n    // it is only ever called after we SetWeak on the Global.\n    from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,\n};\n\npub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n    const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n    var caller: Caller = undefined;\n    caller.init(v8_isolate);\n    defer caller.deinit();\n\n    const local = &caller.local;\n\n    var hs: js.HandleScope = undefined;\n    hs.init(local.isolate);\n    defer hs.deinit();\n\n    const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {\n        return 0;\n    };\n\n    const page = local.ctx.page;\n    const document = page.document;\n\n    if (document.getElementById(property, page)) |el| {\n        const js_val = local.zigValueToJs(el, .{}) catch return 0;\n        var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };\n        pc.getReturnValue().set(js_val);\n        return 1;\n    }\n\n    if (comptime IS_DEBUG) {\n        if (std.mem.startsWith(u8, property, \"__\")) {\n            // some frameworks will extend built-in types using a __ prefix\n            // these should always be safe to ignore.\n            return 0;\n        }\n\n        const ignored = std.StaticStringMap(void).initComptime(.{\n            .{ \"Deno\", {} },\n            .{ \"process\", {} },\n            .{ \"ShadyDOM\", {} },\n            .{ \"ShadyCSS\", {} },\n\n            // a lot of sites seem to like having their own window.config.\n            .{ \"config\", {} },\n\n            .{ \"litNonce\", {} },\n            .{ \"litHtmlVersions\", {} },\n            .{ \"litElementVersions\", {} },\n            .{ \"litHtmlPolyfillSupport\", {} },\n            .{ \"litElementHydrateSupport\", {} },\n            .{ \"litElementPolyfillSupport\", {} },\n            .{ \"reactiveElementVersions\", {} },\n\n            .{ \"recaptcha\", {} },\n            .{ \"grecaptcha\", {} },\n            .{ \"___grecaptcha_cfg\", {} },\n            .{ \"__recaptcha_api\", {} },\n            .{ \"__google_recaptcha_client\", {} },\n\n            .{ \"CLOSURE_FLAGS\", {} },\n            .{ \"__REACT_DEVTOOLS_GLOBAL_HOOK__\", {} },\n            .{ \"ApplePaySession\", {} },\n        });\n        if (!ignored.has(property)) {\n            const key = std.fmt.bufPrint(&local.ctx.page.buf, \"Window:{s}\", .{property}) catch return 0;\n            logUnknownProperty(local, key) catch return 0;\n        }\n    }\n\n    // not intercepted\n    return 0;\n}\n\n// Only used for debugging\npub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n    if (comptime !IS_DEBUG) {\n        @compileError(\"unknownObjectPropertyCallback should only be used in debug builds\");\n    }\n\n    return struct {\n        fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {\n            const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;\n\n            var caller: Caller = undefined;\n            caller.init(v8_isolate);\n            defer caller.deinit();\n\n            const local = &caller.local;\n\n            var hs: js.HandleScope = undefined;\n            hs.init(local.isolate);\n            defer hs.deinit();\n\n            const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {\n                return 0;\n            };\n\n            if (std.mem.startsWith(u8, property, \"__\")) {\n                // some frameworks will extend built-in types using a __ prefix\n                // these should always be safe to ignore.\n                return 0;\n            }\n\n            if (std.mem.startsWith(u8, property, \"jQuery\")) {\n                return 0;\n            }\n\n            if (JsApi == @import(\"../webapi/cdata/Text.zig\").JsApi or JsApi == @import(\"../webapi/cdata/Comment.zig\").JsApi) {\n                if (std.mem.eql(u8, property, \"tagName\")) {\n                    // knockout does this, a lot.\n                    return 0;\n                }\n            }\n\n            if (JsApi == @import(\"../webapi/element/Html.zig\").JsApi or JsApi == @import(\"../webapi/Element.zig\").JsApi or JsApi == @import(\"../webapi/element/html/Custom.zig\").JsApi) {\n                // react ?\n                if (std.mem.eql(u8, property, \"props\")) return 0;\n                if (std.mem.eql(u8, property, \"hydrated\")) return 0;\n                if (std.mem.eql(u8, property, \"isHydrated\")) return 0;\n            }\n\n            if (JsApi == @import(\"../webapi/Console.zig\").JsApi) {\n                if (std.mem.eql(u8, property, \"firebug\")) return 0;\n            }\n\n            const ignored = std.StaticStringMap(void).initComptime(.{});\n            if (!ignored.has(property)) {\n                const key = std.fmt.bufPrint(&local.ctx.page.buf, \"{s}:{s}\", .{ if (@hasDecl(JsApi.Meta, \"name\")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;\n                logUnknownProperty(local, key) catch return 0;\n            }\n            // not intercepted\n            return 0;\n        }\n    }.wrap;\n}\n\nfn logUnknownProperty(local: *const js.Local, key: []const u8) !void {\n    const ctx = local.ctx;\n    const gop = try ctx.unknown_properties.getOrPut(ctx.arena, key);\n    if (gop.found_existing) {\n        gop.value_ptr.count += 1;\n    } else {\n        gop.key_ptr.* = try ctx.arena.dupe(u8, key);\n        gop.value_ptr.* = .{\n            .count = 1,\n            .first_stack = try ctx.arena.dupe(u8, (try local.stackTrace()) orelse \"???\"),\n        };\n    }\n}\n\n// Given a Type, returns the length of the prototype chain, including self\nfn prototypeChainLength(comptime T: type) usize {\n    var l: usize = 1;\n    var Next = T;\n    while (PrototypeType(Next)) |N| {\n        Next = N;\n        l += 1;\n    }\n    return l;\n}\n\n// Given a Type, gets its prototype Type (if any)\nfn PrototypeType(comptime T: type) ?type {\n    if (!@hasField(T, \"_proto\")) {\n        return null;\n    }\n    return Struct(std.meta.fieldInfo(T, ._proto).type);\n}\n\nfn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {\n    var index: usize = 0;\n    var flat: [countFlattenedTypes(Types)]type = undefined;\n    for (Types) |T| {\n        if (@hasDecl(T, \"registerTypes\")) {\n            for (T.registerTypes()) |TT| {\n                flat[index] = TT.JsApi;\n                index += 1;\n            }\n        } else {\n            flat[index] = T.JsApi;\n            index += 1;\n        }\n    }\n    return flat;\n}\n\nfn countFlattenedTypes(comptime Types: []const type) usize {\n    var c: usize = 0;\n    for (Types) |T| {\n        c += if (@hasDecl(T, \"registerTypes\")) T.registerTypes().len else 1;\n    }\n    return c;\n}\n\n//  T => T\n// *T => T\npub fn Struct(comptime T: type) type {\n    return switch (@typeInfo(T)) {\n        .@\"struct\" => T,\n        .pointer => |ptr| ptr.child,\n        else => @compileError(\"Expecting Struct or *Struct, got: \" ++ @typeName(T)),\n    };\n}\n\npub const JsApiLookup = struct {\n    /// Integer type we use for `JsApiLookup` enum. Can be u8 at min.\n    pub const BackingInt = std.math.IntFittingRange(0, @max(std.math.maxInt(u8), JsApis.len));\n\n    /// Imagine we have a type `Cat` which has a getter:\n    ///\n    ///    fn get_owner(self: *Cat) *Owner {\n    ///        return self.owner;\n    ///    }\n    ///\n    /// When we execute `caller.getter`, we'll end up doing something like:\n    ///\n    ///    const res = @call(.auto, Cat.get_owner, .{cat_instance});\n    ///\n    /// How do we turn `res`, which is an *Owner, into something we can return\n    /// to v8? We need the ObjectTemplate associated with Owner. How do we\n    /// get that? Well, we store all the ObjectTemplates in an array that's\n    /// tied to env. So we do something like:\n    ///\n    ///    env.templates[index_of_owner].initInstance(...);\n    ///\n    /// But how do we get that `index_of_owner`? `Index` is an enum\n    /// that looks like:\n    ///\n    ///    pub const Enum = enum(BackingInt) {\n    ///        cat = 0,\n    ///        owner = 1,\n    ///        ...\n    ///    }\n    ///\n    /// (`BackingInt` is calculated at comptime regarding to interfaces we have)\n    /// So to get the template index of `owner`, simply do:\n    ///\n    ///    const index_id = types.getId(@TypeOf(res));\n    ///\n    pub const Enum = blk: {\n        var fields: [JsApis.len]std.builtin.Type.EnumField = undefined;\n        for (JsApis, 0..) |JsApi, i| {\n            fields[i] = .{ .name = @typeName(JsApi), .value = i };\n        }\n\n        break :blk @Type(.{\n            .@\"enum\" = .{\n                .fields = &fields,\n                .tag_type = BackingInt,\n                .is_exhaustive = true,\n                .decls = &.{},\n            },\n        });\n    };\n\n    /// Returns a boolean indicating if a type exist in the lookup.\n    pub inline fn has(t: type) bool {\n        return @hasField(Enum, @typeName(t));\n    }\n\n    /// Returns the `Enum` for the given type.\n    pub inline fn getIndex(t: type) Enum {\n        return @field(Enum, @typeName(t));\n    }\n\n    /// Returns the ID for the given type.\n    pub inline fn getId(t: type) BackingInt {\n        return @intFromEnum(getIndex(t));\n    }\n};\n\npub const SubType = enum {\n    @\"error\",\n    array,\n    arraybuffer,\n    dataview,\n    date,\n    generator,\n    iterator,\n    map,\n    node,\n    promise,\n    proxy,\n    regexp,\n    set,\n    typedarray,\n    wasmvalue,\n    weakmap,\n    weakset,\n    webassemblymemory,\n};\n\npub const JsApis = flattenTypes(&.{\n    @import(\"../webapi/AbortController.zig\"),\n    @import(\"../webapi/AbortSignal.zig\"),\n    @import(\"../webapi/CData.zig\"),\n    @import(\"../webapi/cdata/Comment.zig\"),\n    @import(\"../webapi/cdata/Text.zig\"),\n    @import(\"../webapi/cdata/CDATASection.zig\"),\n    @import(\"../webapi/cdata/ProcessingInstruction.zig\"),\n    @import(\"../webapi/collections.zig\"),\n    @import(\"../webapi/Console.zig\"),\n    @import(\"../webapi/Crypto.zig\"),\n    @import(\"../webapi/Permissions.zig\"),\n    @import(\"../webapi/StorageManager.zig\"),\n    @import(\"../webapi/CSS.zig\"),\n    @import(\"../webapi/css/CSSRule.zig\"),\n    @import(\"../webapi/css/CSSRuleList.zig\"),\n    @import(\"../webapi/css/CSSStyleDeclaration.zig\"),\n    @import(\"../webapi/css/CSSStyleRule.zig\"),\n    @import(\"../webapi/css/CSSStyleSheet.zig\"),\n    @import(\"../webapi/css/CSSStyleProperties.zig\"),\n    @import(\"../webapi/css/FontFace.zig\"),\n    @import(\"../webapi/css/FontFaceSet.zig\"),\n    @import(\"../webapi/css/MediaQueryList.zig\"),\n    @import(\"../webapi/css/StyleSheetList.zig\"),\n    @import(\"../webapi/Document.zig\"),\n    @import(\"../webapi/HTMLDocument.zig\"),\n    @import(\"../webapi/XMLDocument.zig\"),\n    @import(\"../webapi/History.zig\"),\n    @import(\"../webapi/KeyValueList.zig\"),\n    @import(\"../webapi/DocumentFragment.zig\"),\n    @import(\"../webapi/DocumentType.zig\"),\n    @import(\"../webapi/ShadowRoot.zig\"),\n    @import(\"../webapi/DOMException.zig\"),\n    @import(\"../webapi/DOMImplementation.zig\"),\n    @import(\"../webapi/DOMTreeWalker.zig\"),\n    @import(\"../webapi/DOMNodeIterator.zig\"),\n    @import(\"../webapi/DOMRect.zig\"),\n    @import(\"../webapi/DOMParser.zig\"),\n    @import(\"../webapi/XMLSerializer.zig\"),\n    @import(\"../webapi/AbstractRange.zig\"),\n    @import(\"../webapi/Range.zig\"),\n    @import(\"../webapi/NodeFilter.zig\"),\n    @import(\"../webapi/Element.zig\"),\n    @import(\"../webapi/element/DOMStringMap.zig\"),\n    @import(\"../webapi/element/Attribute.zig\"),\n    @import(\"../webapi/element/Html.zig\"),\n    @import(\"../webapi/element/html/IFrame.zig\"),\n    @import(\"../webapi/element/html/Anchor.zig\"),\n    @import(\"../webapi/element/html/Area.zig\"),\n    @import(\"../webapi/element/html/Audio.zig\"),\n    @import(\"../webapi/element/html/Base.zig\"),\n    @import(\"../webapi/element/html/Body.zig\"),\n    @import(\"../webapi/element/html/BR.zig\"),\n    @import(\"../webapi/element/html/Button.zig\"),\n    @import(\"../webapi/element/html/Canvas.zig\"),\n    @import(\"../webapi/element/html/Custom.zig\"),\n    @import(\"../webapi/element/html/Data.zig\"),\n    @import(\"../webapi/element/html/DataList.zig\"),\n    @import(\"../webapi/element/html/Details.zig\"),\n    @import(\"../webapi/element/html/Dialog.zig\"),\n    @import(\"../webapi/element/html/Directory.zig\"),\n    @import(\"../webapi/element/html/DList.zig\"),\n    @import(\"../webapi/element/html/Div.zig\"),\n    @import(\"../webapi/element/html/Embed.zig\"),\n    @import(\"../webapi/element/html/FieldSet.zig\"),\n    @import(\"../webapi/element/html/Font.zig\"),\n    @import(\"../webapi/element/html/Form.zig\"),\n    @import(\"../webapi/element/html/Generic.zig\"),\n    @import(\"../webapi/element/html/Head.zig\"),\n    @import(\"../webapi/element/html/Heading.zig\"),\n    @import(\"../webapi/element/html/HR.zig\"),\n    @import(\"../webapi/element/html/Html.zig\"),\n    @import(\"../webapi/element/html/Image.zig\"),\n    @import(\"../webapi/element/html/Input.zig\"),\n    @import(\"../webapi/element/html/Label.zig\"),\n    @import(\"../webapi/element/html/Legend.zig\"),\n    @import(\"../webapi/element/html/LI.zig\"),\n    @import(\"../webapi/element/html/Link.zig\"),\n    @import(\"../webapi/element/html/Map.zig\"),\n    @import(\"../webapi/element/html/Media.zig\"),\n    @import(\"../webapi/element/html/Meta.zig\"),\n    @import(\"../webapi/element/html/Meter.zig\"),\n    @import(\"../webapi/element/html/Mod.zig\"),\n    @import(\"../webapi/element/html/Object.zig\"),\n    @import(\"../webapi/element/html/OL.zig\"),\n    @import(\"../webapi/element/html/OptGroup.zig\"),\n    @import(\"../webapi/element/html/Option.zig\"),\n    @import(\"../webapi/element/html/Output.zig\"),\n    @import(\"../webapi/element/html/Paragraph.zig\"),\n    @import(\"../webapi/element/html/Picture.zig\"),\n    @import(\"../webapi/element/html/Param.zig\"),\n    @import(\"../webapi/element/html/Pre.zig\"),\n    @import(\"../webapi/element/html/Progress.zig\"),\n    @import(\"../webapi/element/html/Quote.zig\"),\n    @import(\"../webapi/element/html/Script.zig\"),\n    @import(\"../webapi/element/html/Select.zig\"),\n    @import(\"../webapi/element/html/Slot.zig\"),\n    @import(\"../webapi/element/html/Source.zig\"),\n    @import(\"../webapi/element/html/Span.zig\"),\n    @import(\"../webapi/element/html/Style.zig\"),\n    @import(\"../webapi/element/html/Table.zig\"),\n    @import(\"../webapi/element/html/TableCaption.zig\"),\n    @import(\"../webapi/element/html/TableCell.zig\"),\n    @import(\"../webapi/element/html/TableCol.zig\"),\n    @import(\"../webapi/element/html/TableRow.zig\"),\n    @import(\"../webapi/element/html/TableSection.zig\"),\n    @import(\"../webapi/element/html/Template.zig\"),\n    @import(\"../webapi/element/html/TextArea.zig\"),\n    @import(\"../webapi/element/html/Time.zig\"),\n    @import(\"../webapi/element/html/Title.zig\"),\n    @import(\"../webapi/element/html/Track.zig\"),\n    @import(\"../webapi/element/html/Video.zig\"),\n    @import(\"../webapi/element/html/UL.zig\"),\n    @import(\"../webapi/element/html/Unknown.zig\"),\n    @import(\"../webapi/element/Svg.zig\"),\n    @import(\"../webapi/element/svg/Generic.zig\"),\n    @import(\"../webapi/encoding/TextDecoder.zig\"),\n    @import(\"../webapi/encoding/TextEncoder.zig\"),\n    @import(\"../webapi/encoding/TextEncoderStream.zig\"),\n    @import(\"../webapi/encoding/TextDecoderStream.zig\"),\n    @import(\"../webapi/Event.zig\"),\n    @import(\"../webapi/event/CompositionEvent.zig\"),\n    @import(\"../webapi/event/CustomEvent.zig\"),\n    @import(\"../webapi/event/ErrorEvent.zig\"),\n    @import(\"../webapi/event/MessageEvent.zig\"),\n    @import(\"../webapi/event/ProgressEvent.zig\"),\n    @import(\"../webapi/event/NavigationCurrentEntryChangeEvent.zig\"),\n    @import(\"../webapi/event/PageTransitionEvent.zig\"),\n    @import(\"../webapi/event/PopStateEvent.zig\"),\n    @import(\"../webapi/event/UIEvent.zig\"),\n    @import(\"../webapi/event/MouseEvent.zig\"),\n    @import(\"../webapi/event/PointerEvent.zig\"),\n    @import(\"../webapi/event/KeyboardEvent.zig\"),\n    @import(\"../webapi/event/FocusEvent.zig\"),\n    @import(\"../webapi/event/WheelEvent.zig\"),\n    @import(\"../webapi/event/TextEvent.zig\"),\n    @import(\"../webapi/event/InputEvent.zig\"),\n    @import(\"../webapi/event/PromiseRejectionEvent.zig\"),\n    @import(\"../webapi/MessageChannel.zig\"),\n    @import(\"../webapi/MessagePort.zig\"),\n    @import(\"../webapi/media/MediaError.zig\"),\n    @import(\"../webapi/media/TextTrackCue.zig\"),\n    @import(\"../webapi/media/VTTCue.zig\"),\n    @import(\"../webapi/animation/Animation.zig\"),\n    @import(\"../webapi/EventTarget.zig\"),\n    @import(\"../webapi/Location.zig\"),\n    @import(\"../webapi/Navigator.zig\"),\n    @import(\"../webapi/net/FormData.zig\"),\n    @import(\"../webapi/net/Headers.zig\"),\n    @import(\"../webapi/net/Request.zig\"),\n    @import(\"../webapi/net/Response.zig\"),\n    @import(\"../webapi/net/URLSearchParams.zig\"),\n    @import(\"../webapi/net/XMLHttpRequest.zig\"),\n    @import(\"../webapi/net/XMLHttpRequestEventTarget.zig\"),\n    @import(\"../webapi/streams/ReadableStream.zig\"),\n    @import(\"../webapi/streams/ReadableStreamDefaultReader.zig\"),\n    @import(\"../webapi/streams/ReadableStreamDefaultController.zig\"),\n    @import(\"../webapi/streams/WritableStream.zig\"),\n    @import(\"../webapi/streams/WritableStreamDefaultWriter.zig\"),\n    @import(\"../webapi/streams/WritableStreamDefaultController.zig\"),\n    @import(\"../webapi/streams/TransformStream.zig\"),\n    @import(\"../webapi/Node.zig\"),\n    @import(\"../webapi/storage/storage.zig\"),\n    @import(\"../webapi/URL.zig\"),\n    @import(\"../webapi/Window.zig\"),\n    @import(\"../webapi/Performance.zig\"),\n    @import(\"../webapi/PluginArray.zig\"),\n    @import(\"../webapi/MutationObserver.zig\"),\n    @import(\"../webapi/IntersectionObserver.zig\"),\n    @import(\"../webapi/CustomElementRegistry.zig\"),\n    @import(\"../webapi/ResizeObserver.zig\"),\n    @import(\"../webapi/IdleDeadline.zig\"),\n    @import(\"../webapi/Blob.zig\"),\n    @import(\"../webapi/File.zig\"),\n    @import(\"../webapi/FileList.zig\"),\n    @import(\"../webapi/FileReader.zig\"),\n    @import(\"../webapi/Screen.zig\"),\n    @import(\"../webapi/VisualViewport.zig\"),\n    @import(\"../webapi/PerformanceObserver.zig\"),\n    @import(\"../webapi/navigation/Navigation.zig\"),\n    @import(\"../webapi/navigation/NavigationHistoryEntry.zig\"),\n    @import(\"../webapi/navigation/NavigationActivation.zig\"),\n    @import(\"../webapi/canvas/CanvasRenderingContext2D.zig\"),\n    @import(\"../webapi/canvas/WebGLRenderingContext.zig\"),\n    @import(\"../webapi/canvas/OffscreenCanvas.zig\"),\n    @import(\"../webapi/canvas/OffscreenCanvasRenderingContext2D.zig\"),\n    @import(\"../webapi/SubtleCrypto.zig\"),\n    @import(\"../webapi/Selection.zig\"),\n    @import(\"../webapi/ImageData.zig\"),\n});\n"
  },
  {
    "path": "src/browser/js/js.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\npub const v8 = @import(\"v8\").c;\n\nconst string = @import(\"../../string.zig\");\n\npub const Env = @import(\"Env.zig\");\npub const bridge = @import(\"bridge.zig\");\npub const Caller = @import(\"Caller.zig\");\npub const Origin = @import(\"Origin.zig\");\npub const Context = @import(\"Context.zig\");\npub const Local = @import(\"Local.zig\");\npub const Inspector = @import(\"Inspector.zig\");\npub const Snapshot = @import(\"Snapshot.zig\");\npub const Platform = @import(\"Platform.zig\");\npub const Isolate = @import(\"Isolate.zig\");\npub const HandleScope = @import(\"HandleScope.zig\");\n\npub const Value = @import(\"Value.zig\");\npub const Array = @import(\"Array.zig\");\npub const String = @import(\"String.zig\");\npub const Object = @import(\"Object.zig\");\npub const TryCatch = @import(\"TryCatch.zig\");\npub const Function = @import(\"Function.zig\");\npub const Promise = @import(\"Promise.zig\");\npub const Module = @import(\"Module.zig\");\npub const BigInt = @import(\"BigInt.zig\");\npub const Number = @import(\"Number.zig\");\npub const Integer = @import(\"Integer.zig\");\npub const PromiseResolver = @import(\"PromiseResolver.zig\");\npub const PromiseRejection = @import(\"PromiseRejection.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub fn Bridge(comptime T: type) type {\n    return bridge.Builder(T);\n}\n\n// If a function returns a []i32, should that map to a plain-old\n// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll\n// map arrays/slices to the JavaScript arrays. If you want a TypedArray\n// wrap it in this.\n// Also, this type has nothing to do with the Env. But we place it here\n// for consistency. Want a callback? Env.Callback. Want a JsObject?\n// Env.JsObject. Want a TypedArray? Env.TypedArray.\npub fn TypedArray(comptime T: type) type {\n    return struct {\n        values: []const T,\n\n        pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {\n            return .{ .values = try allocator.dupe(T, self.values) };\n        }\n    };\n}\n\npub const ArrayBuffer = struct {\n    values: []const u8,\n\n    pub fn dupe(self: ArrayBuffer, allocator: Allocator) !ArrayBuffer {\n        return .{ .values = try allocator.dupe(u8, self.values) };\n    }\n};\n\npub const ArrayType = enum(u8) {\n    int8,\n    uint8,\n    uint8_clamped,\n    int16,\n    uint16,\n    int32,\n    uint32,\n    float16,\n    float32,\n    float64,\n};\n\npub fn ArrayBufferRef(comptime kind: ArrayType) type {\n    return struct {\n        const Self = @This();\n\n        const BackingInt = switch (kind) {\n            .int8 => i8,\n            .uint8, .uint8_clamped => u8,\n            .int16 => i16,\n            .uint16 => u16,\n            .int32 => i32,\n            .uint32 => u32,\n            .float16 => f16,\n            .float32 => f32,\n            .float64 => f64,\n        };\n\n        local: *const Local,\n        handle: *const v8.Value,\n\n        /// Persisted typed array.\n        pub const Global = struct {\n            handle: v8.Global,\n\n            pub fn deinit(self: *Global) void {\n                v8.v8__Global__Reset(&self.handle);\n            }\n\n            pub fn local(self: *const Global, l: *const Local) Self {\n                return .{ .local = l, .handle = v8.v8__Global__Get(&self.handle, l.isolate.handle).? };\n            }\n        };\n\n        pub fn init(local: *const Local, size: usize) Self {\n            const ctx = local.ctx;\n            const isolate = ctx.isolate;\n            const bits = switch (@typeInfo(BackingInt)) {\n                .int => |n| n.bits,\n                .float => |f| f.bits,\n                else => unreachable,\n            };\n\n            var array_buffer: *const v8.ArrayBuffer = undefined;\n            if (size == 0) {\n                array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;\n            } else {\n                const buffer_len = size * bits / 8;\n                const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;\n                const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);\n                array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;\n            }\n\n            const handle: *const v8.Value = switch (comptime kind) {\n                .int8 => @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, size).?),\n                .uint8 => @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, size).?),\n                .uint8_clamped => @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, size).?),\n                .int16 => @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, size).?),\n                .uint16 => @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, size).?),\n                .int32 => @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, size).?),\n                .uint32 => @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, size).?),\n                .float16 => @ptrCast(v8.v8__Float16Array__New(array_buffer, 0, size).?),\n                .float32 => @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, size).?),\n                .float64 => @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, size).?),\n            };\n\n            return .{ .local = local, .handle = handle };\n        }\n\n        pub fn persist(self: *const Self) !Global {\n            var ctx = self.local.ctx;\n            var global: v8.Global = undefined;\n            v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);\n            try ctx.trackGlobal(global);\n\n            return .{ .handle = global };\n        }\n    };\n}\n\n// If a WebAPI takes a []const u8, then we'll coerce any JS value to that string\n// so null -> \"null\". But if a WebAPI takes an optional string, ?[]const u8,\n// how should we handle null? If the parameter _isn't_ passed, then it's obvious\n// that it should be null, but what if `null` is passed? It's ambiguous, should\n// that be null, or \"null\"? It could depend on the api. So, `null` passed to\n// ?[]const u8 will be `null`. If you want it to be \"null\", use a `.js.NullableString`.\npub const NullableString = struct {\n    value: []const u8,\n};\n\npub const Exception = struct {\n    local: *const Local,\n    handle: *const v8.Value,\n};\n\n// These are simple types that we can convert to JS with only an isolate. This\n// is separated from the Caller's zigValueToJs to make it available when we\n// don't have a caller (i.e., when setting static attributes on types)\npub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) *const v8.Value else ?*const v8.Value {\n    switch (@typeInfo(@TypeOf(value))) {\n        .void => return isolate.initUndefined(),\n        .null => if (comptime null_as_undefined) return isolate.initUndefined() else return isolate.initNull(),\n        .bool => return if (value) isolate.initTrue() else isolate.initFalse(),\n        .int => |n| {\n            if (comptime n.bits <= 32) {\n                return @ptrCast(isolate.initInteger(value).handle);\n            }\n            if (value >= 0 and value <= 4_294_967_295) {\n                return @ptrCast(isolate.initInteger(@as(u32, @intCast(value))).handle);\n            }\n            return @ptrCast(isolate.initBigInt(value).handle);\n        },\n        .comptime_int => {\n            if (value > -2_147_483_648 and value <= 4_294_967_295) {\n                return @ptrCast(isolate.initInteger(value).handle);\n            }\n            return @ptrCast(isolate.initBigInt(value).handle);\n        },\n        .float, .comptime_float => return @ptrCast(isolate.initNumber(value).handle),\n        .pointer => |ptr| {\n            if (ptr.size == .slice and ptr.child == u8) {\n                return @ptrCast(isolate.initStringHandle(value));\n            }\n            if (ptr.size == .one) {\n                const one_info = @typeInfo(ptr.child);\n                if (one_info == .array and one_info.array.child == u8) {\n                    return @ptrCast(isolate.initStringHandle(value));\n                }\n            }\n        },\n        .array => return simpleZigValueToJs(isolate, &value, fail, null_as_undefined),\n        .optional => {\n            if (value) |v| {\n                return simpleZigValueToJs(isolate, v, fail, null_as_undefined);\n            }\n            if (comptime null_as_undefined) {\n                return isolate.initUndefined();\n            }\n            return isolate.initNull();\n        },\n        .@\"struct\" => {\n            switch (@TypeOf(value)) {\n                string.String => return isolate.initStringHandle(value.str()),\n                ArrayBuffer => {\n                    const values = value.values;\n                    const len = values.len;\n                    const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);\n                    if (len > 0) {\n                        const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));\n                        @memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);\n                    }\n                    const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);\n                    return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);\n                },\n                // zig fmt: off\n                TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),\n                TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64),\n                TypedArray(f32), TypedArray(f64),\n                // zig fmt: on\n                => {\n                    const values = value.values;\n                    const value_type = @typeInfo(@TypeOf(values)).pointer.child;\n                    const len = values.len;\n                    const bits = switch (@typeInfo(value_type)) {\n                        .int => |n| n.bits,\n                        .float => |f| f.bits,\n                        else => @compileError(\"Invalid TypeArray type: \" ++ @typeName(value_type)),\n                    };\n\n                    var array_buffer: *const v8.ArrayBuffer = undefined;\n                    if (len == 0) {\n                        array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;\n                    } else {\n                        const buffer_len = len * bits / 8;\n                        const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;\n                        const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));\n                        @memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);\n                        const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);\n                        array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;\n                    }\n\n                    switch (@typeInfo(value_type)) {\n                        .int => |n| switch (n.signedness) {\n                            .unsigned => switch (n.bits) {\n                                8 => return @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, len).?),\n                                16 => return @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, len).?),\n                                32 => return @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, len).?),\n                                64 => return @ptrCast(v8.v8__BigUint64Array__New(array_buffer, 0, len).?),\n                                else => {},\n                            },\n                            .signed => switch (n.bits) {\n                                8 => return @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, len).?),\n                                16 => return @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, len).?),\n                                32 => return @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, len).?),\n                                64 => return @ptrCast(v8.v8__BigInt64Array__New(array_buffer, 0, len).?),\n                                else => {},\n                            },\n                        },\n                        .float => |f| switch (f.bits) {\n                            32 => return @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, len).?),\n                            64 => return @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, len).?),\n                            else => {},\n                        },\n                        else => {},\n                    }\n                    // We normally don't fail in this function unless fail == true\n                    // but this can never be valid.\n                    @compileError(\"Invalid TypeArray type: \" ++ @typeName(value_type));\n                },\n                inline String, BigInt, Integer, Number, Value, Object => return value.handle,\n                else => {},\n            }\n        },\n        .@\"union\" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail, null_as_undefined),\n        .@\"enum\" => {\n            const T = @TypeOf(value);\n            if (@hasDecl(T, \"toString\")) {\n                return simpleZigValueToJs(isolate, value.toString(), fail, null_as_undefined);\n            }\n        },\n        else => {},\n    }\n    if (fail) {\n        @compileError(\"Unsupported Zig type \" ++ @typeName(@TypeOf(value)));\n    }\n    return null;\n}\n\n// These are here, and not in Inspector.zig, because Inspector.zig isn't always\n// included (e.g. in the wpt build).\n\n// This is called from V8. Whenever the v8 inspector has to describe a value\n// it'll call this function to gets its [optional] subtype - which, from V8's\n// point of view, is an arbitrary string.\npub export fn v8_inspector__Client__IMPL__valueSubtype(\n    _: *v8.InspectorClientImpl,\n    c_value: *const v8.Value,\n) callconv(.c) [*c]const u8 {\n    const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;\n    return if (external_entry.subtype) |st| @tagName(st) else null;\n}\n\n// Same as valueSubType above, but for the optional description field.\n// From what I can tell, some drivers _need_ the description field to be\n// present, even if it's empty. So if we have a subType for the value, we'll\n// put an empty description.\npub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(\n    _: *v8.InspectorClientImpl,\n    v8_context: *const v8.Context,\n    c_value: *const v8.Value,\n) callconv(.c) [*c]const u8 {\n    _ = v8_context;\n\n    // We _must_ include a non-null description in order for the subtype value\n    // to be included. Besides that, I don't know if the value has any meaning\n    const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;\n    return if (external_entry.subtype == null) null else \"\";\n}\n\ntest \"TaggedAnyOpaque\" {\n    // If we grow this, fine, but it should be a conscious decision\n    try std.testing.expectEqual(24, @sizeOf(@import(\"TaggedOpaque.zig\")));\n}\n"
  },
  {
    "path": "src/browser/markdown.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Page = @import(\"Page.zig\");\nconst URL = @import(\"URL.zig\");\nconst TreeWalker = @import(\"webapi/TreeWalker.zig\");\nconst CData = @import(\"webapi/CData.zig\");\nconst Element = @import(\"webapi/Element.zig\");\nconst Node = @import(\"webapi/Node.zig\");\nconst isAllWhitespace = @import(\"../string.zig\").isAllWhitespace;\n\npub const Opts = struct {\n    // Options for future customization (e.g., dialect)\n};\n\nconst State = struct {\n    const ListType = enum { ordered, unordered };\n    const ListState = struct {\n        type: ListType,\n        index: usize,\n    };\n\n    list_depth: usize = 0,\n    list_stack: [32]ListState = undefined,\n    pre_node: ?*Node = null,\n    in_code: bool = false,\n    in_table: bool = false,\n    table_row_index: usize = 0,\n    table_col_count: usize = 0,\n    last_char_was_newline: bool = true,\n};\n\nfn shouldAddSpacing(tag: Element.Tag) bool {\n    return switch (tag) {\n        .p, .h1, .h2, .h3, .h4, .h5, .h6, .blockquote, .pre, .table => true,\n        else => false,\n    };\n}\n\nfn isLayoutBlock(tag: Element.Tag) bool {\n    return switch (tag) {\n        .main, .section, .article, .nav, .aside, .header, .footer, .div, .ul, .ol => true,\n        else => false,\n    };\n}\n\nfn isStandaloneAnchor(el: *Element) bool {\n    const node = el.asNode();\n    const parent = node.parentNode() orelse return false;\n    const parent_el = parent.is(Element) orelse return false;\n\n    if (!isLayoutBlock(parent_el.getTag())) return false;\n\n    var prev = node.previousSibling();\n    while (prev) |p| : (prev = p.previousSibling()) {\n        if (isSignificantText(p)) return false;\n        if (p.is(Element)) |pe| {\n            if (isVisibleElement(pe)) break;\n        }\n    }\n\n    var next = node.nextSibling();\n    while (next) |n| : (next = n.nextSibling()) {\n        if (isSignificantText(n)) return false;\n        if (n.is(Element)) |ne| {\n            if (isVisibleElement(ne)) break;\n        }\n    }\n\n    return true;\n}\n\nfn isSignificantText(node: *Node) bool {\n    const text = node.is(Node.CData.Text) orelse return false;\n    return !isAllWhitespace(text.getWholeText());\n}\n\nfn isVisibleElement(el: *Element) bool {\n    const tag = el.getTag();\n    return !tag.isMetadata() and tag != .svg;\n}\n\nfn getAnchorLabel(el: *Element) ?[]const u8 {\n    return el.getAttributeSafe(comptime .wrap(\"aria-label\")) orelse el.getAttributeSafe(comptime .wrap(\"title\"));\n}\n\nfn hasBlockDescendant(root: *Node) bool {\n    var tw = TreeWalker.FullExcludeSelf.Elements.init(root, .{});\n    while (tw.next()) |el| {\n        if (el.getTag().isBlock()) return true;\n    }\n    return false;\n}\n\nfn hasVisibleContent(root: *Node) bool {\n    var tw = TreeWalker.FullExcludeSelf.init(root, .{});\n    while (tw.next()) |node| {\n        if (isSignificantText(node)) return true;\n        if (node.is(Element)) |el| {\n            if (!isVisibleElement(el)) {\n                tw.skipChildren();\n            } else if (el.getTag() == .img) {\n                return true;\n            }\n        }\n    }\n    return false;\n}\n\nconst Context = struct {\n    state: State,\n    writer: *std.Io.Writer,\n    page: *Page,\n\n    fn ensureNewline(self: *Context) !void {\n        if (!self.state.last_char_was_newline) {\n            try self.writer.writeByte('\\n');\n            self.state.last_char_was_newline = true;\n        }\n    }\n\n    fn render(self: *Context, node: *Node) error{WriteFailed}!void {\n        switch (node._type) {\n            .document, .document_fragment => {\n                try self.renderChildren(node);\n            },\n            .element => |el| {\n                try self.renderElement(el);\n            },\n            .cdata => |cd| {\n                if (node.is(Node.CData.Text)) |_| {\n                    var text = cd.getData().str();\n                    if (self.state.pre_node) |pre| {\n                        if (node.parentNode() == pre and node.nextSibling() == null) {\n                            text = std.mem.trimRight(u8, text, \" \\t\\r\\n\");\n                        }\n                    }\n                    try self.renderText(text);\n                }\n            },\n            else => {},\n        }\n    }\n\n    fn renderChildren(self: *Context, parent: *Node) !void {\n        var it = parent.childrenIterator();\n        while (it.next()) |child| {\n            try self.render(child);\n        }\n    }\n\n    fn renderElement(self: *Context, el: *Element) !void {\n        const tag = el.getTag();\n\n        if (!isVisibleElement(el)) return;\n\n        // --- Opening Tag Logic ---\n\n        // Ensure block elements start on a new line (double newline for paragraphs etc)\n        if (tag.isBlock() and !self.state.in_table) {\n            try self.ensureNewline();\n            if (shouldAddSpacing(tag)) {\n                try self.writer.writeByte('\\n');\n            }\n        } else if (tag == .li or tag == .tr) {\n            try self.ensureNewline();\n        }\n\n        // Prefixes\n        switch (tag) {\n            .h1 => try self.writer.writeAll(\"# \"),\n            .h2 => try self.writer.writeAll(\"## \"),\n            .h3 => try self.writer.writeAll(\"### \"),\n            .h4 => try self.writer.writeAll(\"#### \"),\n            .h5 => try self.writer.writeAll(\"##### \"),\n            .h6 => try self.writer.writeAll(\"###### \"),\n            .ul => {\n                if (self.state.list_depth < self.state.list_stack.len) {\n                    self.state.list_stack[self.state.list_depth] = .{ .type = .unordered, .index = 0 };\n                    self.state.list_depth += 1;\n                }\n            },\n            .ol => {\n                if (self.state.list_depth < self.state.list_stack.len) {\n                    self.state.list_stack[self.state.list_depth] = .{ .type = .ordered, .index = 1 };\n                    self.state.list_depth += 1;\n                }\n            },\n            .li => {\n                const indent = if (self.state.list_depth > 0) self.state.list_depth - 1 else 0;\n                for (0..indent) |_| try self.writer.writeAll(\"  \");\n\n                if (self.state.list_depth > 0 and self.state.list_stack[self.state.list_depth - 1].type == .ordered) {\n                    const current_list = &self.state.list_stack[self.state.list_depth - 1];\n                    try self.writer.print(\"{d}. \", .{current_list.index});\n                    current_list.index += 1;\n                } else {\n                    try self.writer.writeAll(\"- \");\n                }\n                self.state.last_char_was_newline = false;\n            },\n            .table => {\n                self.state.in_table = true;\n                self.state.table_row_index = 0;\n                self.state.table_col_count = 0;\n            },\n            .tr => {\n                self.state.table_col_count = 0;\n                try self.writer.writeByte('|');\n            },\n            .td, .th => {\n                // Note: leading pipe handled by previous cell closing or tr opening\n                self.state.last_char_was_newline = false;\n                try self.writer.writeByte(' ');\n            },\n            .blockquote => {\n                try self.writer.writeAll(\"> \");\n                self.state.last_char_was_newline = false;\n            },\n            .pre => {\n                try self.writer.writeAll(\"```\\n\");\n                self.state.pre_node = el.asNode();\n                self.state.last_char_was_newline = true;\n            },\n            .code => {\n                if (self.state.pre_node == null) {\n                    try self.writer.writeByte('`');\n                    self.state.in_code = true;\n                    self.state.last_char_was_newline = false;\n                }\n            },\n            .b, .strong => {\n                try self.writer.writeAll(\"**\");\n                self.state.last_char_was_newline = false;\n            },\n            .i, .em => {\n                try self.writer.writeAll(\"*\");\n                self.state.last_char_was_newline = false;\n            },\n            .s, .del => {\n                try self.writer.writeAll(\"~~\");\n                self.state.last_char_was_newline = false;\n            },\n            .hr => {\n                try self.writer.writeAll(\"---\\n\");\n                self.state.last_char_was_newline = true;\n                return;\n            },\n            .br => {\n                if (self.state.in_table) {\n                    try self.writer.writeByte(' ');\n                } else {\n                    try self.writer.writeByte('\\n');\n                    self.state.last_char_was_newline = true;\n                }\n                return;\n            },\n            .img => {\n                try self.writer.writeAll(\"![\");\n                if (el.getAttributeSafe(comptime .wrap(\"alt\"))) |alt| {\n                    try self.escape(alt);\n                }\n                try self.writer.writeAll(\"](\");\n                if (el.getAttributeSafe(comptime .wrap(\"src\"))) |src| {\n                    const absolute_src = URL.resolve(self.page.call_arena, self.page.base(), src, .{ .encode = true }) catch src;\n                    try self.writer.writeAll(absolute_src);\n                }\n                try self.writer.writeAll(\")\");\n                self.state.last_char_was_newline = false;\n                return;\n            },\n            .anchor => {\n                const has_content = hasVisibleContent(el.asNode());\n                const label = getAnchorLabel(el);\n                const href_raw = el.getAttributeSafe(comptime .wrap(\"href\"));\n\n                if (!has_content and label == null and href_raw == null) return;\n\n                const has_block = hasBlockDescendant(el.asNode());\n                const href = if (href_raw) |h| URL.resolve(self.page.call_arena, self.page.base(), h, .{ .encode = true }) catch h else null;\n\n                if (has_block) {\n                    try self.renderChildren(el.asNode());\n                    if (href) |h| {\n                        if (!self.state.last_char_was_newline) try self.writer.writeByte('\\n');\n                        try self.writer.writeAll(\"([](\");\n                        try self.writer.writeAll(h);\n                        try self.writer.writeAll(\"))\\n\");\n                        self.state.last_char_was_newline = true;\n                    }\n                    return;\n                }\n\n                if (isStandaloneAnchor(el)) {\n                    if (!self.state.last_char_was_newline) try self.writer.writeByte('\\n');\n                    try self.writer.writeByte('[');\n                    if (has_content) {\n                        try self.renderChildren(el.asNode());\n                    } else {\n                        try self.writer.writeAll(label orelse \"\");\n                    }\n                    try self.writer.writeAll(\"](\");\n                    if (href) |h| {\n                        try self.writer.writeAll(h);\n                    }\n                    try self.writer.writeAll(\")\\n\");\n                    self.state.last_char_was_newline = true;\n                    return;\n                }\n\n                try self.writer.writeByte('[');\n                if (has_content) {\n                    try self.renderChildren(el.asNode());\n                } else {\n                    try self.writer.writeAll(label orelse \"\");\n                }\n                try self.writer.writeAll(\"](\");\n                if (href) |h| {\n                    try self.writer.writeAll(h);\n                }\n                try self.writer.writeByte(')');\n                self.state.last_char_was_newline = false;\n                return;\n            },\n            .input => {\n                const type_attr = el.getAttributeSafe(comptime .wrap(\"type\")) orelse return;\n                if (std.ascii.eqlIgnoreCase(type_attr, \"checkbox\")) {\n                    const checked = el.getAttributeSafe(comptime .wrap(\"checked\")) != null;\n                    try self.writer.writeAll(if (checked) \"[x] \" else \"[ ] \");\n                    self.state.last_char_was_newline = false;\n                }\n                return;\n            },\n            else => {},\n        }\n\n        // --- Render Children ---\n        try self.renderChildren(el.asNode());\n\n        // --- Closing Tag Logic ---\n\n        // Suffixes\n        switch (tag) {\n            .pre => {\n                if (!self.state.last_char_was_newline) {\n                    try self.writer.writeByte('\\n');\n                }\n                try self.writer.writeAll(\"```\\n\");\n                self.state.pre_node = null;\n                self.state.last_char_was_newline = true;\n            },\n            .code => {\n                if (self.state.pre_node == null) {\n                    try self.writer.writeByte('`');\n                    self.state.in_code = false;\n                    self.state.last_char_was_newline = false;\n                }\n            },\n            .b, .strong => {\n                try self.writer.writeAll(\"**\");\n                self.state.last_char_was_newline = false;\n            },\n            .i, .em => {\n                try self.writer.writeAll(\"*\");\n                self.state.last_char_was_newline = false;\n            },\n            .s, .del => {\n                try self.writer.writeAll(\"~~\");\n                self.state.last_char_was_newline = false;\n            },\n            .blockquote => {},\n            .ul, .ol => {\n                if (self.state.list_depth > 0) self.state.list_depth -= 1;\n            },\n            .table => {\n                self.state.in_table = false;\n            },\n            .tr => {\n                try self.writer.writeByte('\\n');\n                if (self.state.table_row_index == 0) {\n                    try self.writer.writeByte('|');\n                    for (0..self.state.table_col_count) |_| {\n                        try self.writer.writeAll(\"---|\");\n                    }\n                    try self.writer.writeByte('\\n');\n                }\n                self.state.table_row_index += 1;\n                self.state.last_char_was_newline = true;\n            },\n            .td, .th => {\n                try self.writer.writeAll(\" |\");\n                self.state.table_col_count += 1;\n                self.state.last_char_was_newline = false;\n            },\n            else => {},\n        }\n\n        // Post-block newlines\n        if (tag.isBlock() and !self.state.in_table) {\n            try self.ensureNewline();\n        }\n    }\n\n    fn renderText(self: *Context, text: []const u8) !void {\n        if (text.len == 0) return;\n\n        if (self.state.pre_node) |_| {\n            try self.writer.writeAll(text);\n            self.state.last_char_was_newline = text[text.len - 1] == '\\n';\n            return;\n        }\n\n        // Check for pure whitespace\n        if (isAllWhitespace(text)) {\n            if (!self.state.last_char_was_newline) {\n                try self.writer.writeByte(' ');\n            }\n            return;\n        }\n\n        // Collapse whitespace\n        var it = std.mem.tokenizeAny(u8, text, \" \\t\\n\\r\");\n        var first = true;\n        while (it.next()) |word| {\n            if (!first or (!self.state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {\n                try self.writer.writeByte(' ');\n            }\n\n            try self.escape(word);\n            self.state.last_char_was_newline = false;\n            first = false;\n        }\n\n        // Handle trailing whitespace from the original text\n        if (!first and !self.state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {\n            try self.writer.writeByte(' ');\n        }\n    }\n\n    fn escape(self: *Context, text: []const u8) !void {\n        for (text) |c| {\n            switch (c) {\n                '\\\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {\n                    try self.writer.writeByte('\\\\');\n                    try self.writer.writeByte(c);\n                },\n                else => try self.writer.writeByte(c),\n            }\n        }\n    }\n};\n\npub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {\n    _ = opts;\n    var ctx: Context = .{\n        .state = .{},\n        .writer = writer,\n        .page = page,\n    };\n    try ctx.render(node);\n    if (!ctx.state.last_char_was_newline) {\n        try writer.writeByte('\\n');\n    }\n}\n\nfn testMarkdownHTML(html: []const u8, expected: []const u8) !void {\n    const testing = @import(\"../testing.zig\");\n    const page = try testing.test_session.createPage();\n    defer testing.test_session.removePage();\n    page.url = \"http://localhost/\";\n\n    const doc = page.window._document;\n\n    const div = try doc.createElement(\"div\", null, page);\n    try page.parseHtmlAsChildren(div.asNode(), html);\n\n    var aw: std.Io.Writer.Allocating = .init(testing.allocator);\n    defer aw.deinit();\n    try dump(div.asNode(), .{}, &aw.writer, page);\n\n    try testing.expectString(expected, aw.written());\n}\n\ntest \"browser.markdown: basic\" {\n    try testMarkdownHTML(\"Hello world\", \"Hello world\\n\");\n}\n\ntest \"browser.markdown: whitespace\" {\n    try testMarkdownHTML(\"<span>A</span> <span>B</span>\", \"A B\\n\");\n}\n\ntest \"browser.markdown: escaping\" {\n    try testMarkdownHTML(\"<p># Not a header</p>\", \"\\n\\\\# Not a header\\n\");\n}\n\ntest \"browser.markdown: strikethrough\" {\n    try testMarkdownHTML(\"<s>deleted</s>\", \"~~deleted~~\\n\");\n}\n\ntest \"browser.markdown: task list\" {\n    try testMarkdownHTML(\n        \\\\<input type=\"checkbox\" checked><input type=\"checkbox\">\n    , \"[x] [ ] \\n\");\n}\n\ntest \"browser.markdown: ordered list\" {\n    try testMarkdownHTML(\n        \\\\<ol><li>First</li><li>Second</li></ol>\n    , \"1. First\\n2. Second\\n\");\n}\n\ntest \"browser.markdown: table\" {\n    try testMarkdownHTML(\n        \\\\<table><thead><tr><th>Head 1</th><th>Head 2</th></tr></thead>\n        \\\\<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>\n    ,\n        \\\\\n        \\\\| Head 1 | Head 2 |\n        \\\\|---|---|\n        \\\\| Cell 1 | Cell 2 |\n        \\\\\n    );\n}\n\ntest \"browser.markdown: nested lists\" {\n    try testMarkdownHTML(\n        \\\\<ul><li>Parent<ul><li>Child</li></ul></li></ul>\n    ,\n        \\\\- Parent\n        \\\\  - Child\n        \\\\\n    );\n}\n\ntest \"browser.markdown: blockquote\" {\n    try testMarkdownHTML(\"<blockquote>Hello world</blockquote>\", \"\\n> Hello world\\n\");\n}\n\ntest \"browser.markdown: links\" {\n    try testMarkdownHTML(\"<a href=\\\"/relative\\\">Link</a>\", \"[Link](http://localhost/relative)\\n\");\n}\n\ntest \"browser.markdown: images\" {\n    try testMarkdownHTML(\"<img src=\\\"logo.png\\\" alt=\\\"Logo\\\">\", \"![Logo](http://localhost/logo.png)\\n\");\n}\n\ntest \"browser.markdown: headings\" {\n    try testMarkdownHTML(\"<h1>Title</h1><h2>Subtitle</h2>\",\n        \\\\\n        \\\\# Title\n        \\\\\n        \\\\## Subtitle\n        \\\\\n    );\n}\n\ntest \"browser.markdown: code\" {\n    try testMarkdownHTML(\n        \\\\<p>Use git push</p>\n        \\\\<pre><code>line 1\n        \\\\line 2</code></pre>\n    ,\n        \\\\\n        \\\\Use git push\n        \\\\\n        \\\\```\n        \\\\line 1\n        \\\\line 2\n        \\\\```\n        \\\\\n    );\n}\n\ntest \"browser.markdown: block link\" {\n    try testMarkdownHTML(\n        \\\\<a href=\"https://example.com\">\n        \\\\  <h3>Title</h3>\n        \\\\  <p>Description</p>\n        \\\\</a>\n    ,\n        \\\\\n        \\\\### Title\n        \\\\\n        \\\\Description\n        \\\\([](https://example.com))\n        \\\\\n    );\n}\n\ntest \"browser.markdown: inline link\" {\n    try testMarkdownHTML(\n        \\\\<p>Visit <a href=\"https://example.com\">Example</a>.</p>\n    ,\n        \\\\\n        \\\\Visit [Example](https://example.com).\n        \\\\\n    );\n}\n\ntest \"browser.markdown: standalone anchors\" {\n    // Inside main, with whitespace between anchors -> treated as blocks\n    try testMarkdownHTML(\n        \\\\<main>\n        \\\\  <a href=\"1\">Link 1</a>\n        \\\\  <a href=\"2\">Link 2</a>\n        \\\\</main>\n    ,\n        \\\\[Link 1](http://localhost/1)\n        \\\\[Link 2](http://localhost/2)\n        \\\\\n    );\n}\n\ntest \"browser.markdown: mixed anchors in main\" {\n    // Anchors surrounded by text should remain inline\n    try testMarkdownHTML(\n        \\\\<main>\n        \\\\  Welcome <a href=\"1\">Link 1</a>.\n        \\\\</main>\n    ,\n        \\\\Welcome [Link 1](http://localhost/1). \n        \\\\\n    );\n}\n\ntest \"browser.markdown: skip empty links\" {\n    try testMarkdownHTML(\n        \\\\<a href=\"/\"></a>\n        \\\\<a href=\"/\"><svg></svg></a>\n    ,\n        \\\\[](http://localhost/)\n        \\\\[](http://localhost/)\n        \\\\\n    );\n}\n\ntest \"browser.markdown: resolve links\" {\n    const testing = @import(\"../testing.zig\");\n    const page = try testing.test_session.createPage();\n    defer testing.test_session.removePage();\n    page.url = \"https://example.com/a/index.html\";\n\n    const doc = page.window._document;\n    const div = try doc.createElement(\"div\", null, page);\n    try page.parseHtmlAsChildren(div.asNode(),\n        \\\\<a href=\"b\">Link</a>\n        \\\\<img src=\"../c.png\" alt=\"Img\">\n        \\\\<a href=\"/my page\">Space</a>\n    );\n\n    var aw: std.Io.Writer.Allocating = .init(testing.allocator);\n    defer aw.deinit();\n    try dump(div.asNode(), .{}, &aw.writer, page);\n\n    try testing.expectString(\n        \\\\[Link](https://example.com/a/b)\n        \\\\![Img](https://example.com/c.png) \n        \\\\[Space](https://example.com/my%20page)\n        \\\\\n    , aw.written());\n}\n\ntest \"browser.markdown: anchor fallback label\" {\n    try testMarkdownHTML(\n        \\\\<a href=\"/discord\" aria-label=\"Discord Server\"><svg></svg></a>\n    , \"[Discord Server](http://localhost/discord)\\n\");\n\n    try testMarkdownHTML(\n        \\\\<a href=\"/search\" title=\"Search Site\"><svg></svg></a>\n    , \"[Search Site](http://localhost/search)\\n\");\n\n    try testMarkdownHTML(\n        \\\\<a href=\"/no-label\"><svg></svg></a>\n    , \"[](http://localhost/no-label)\\n\");\n}\n"
  },
  {
    "path": "src/browser/parser/Parser.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst h5e = @import(\"html5ever.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Node = @import(\"../webapi/Node.zig\");\nconst Element = @import(\"../webapi/Element.zig\");\n\npub const AttributeIterator = h5e.AttributeIterator;\n\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub const ParsedNode = struct {\n    node: *Node,\n\n    // Data associated with this element to be passed back to html5ever as needed\n    // We only have this for Elements. For other types, like comments, it's null.\n    // html5ever should never ask us for this data on a non-element, and we'll\n    // assert that, with this opitonal, to make sure our assumption is correct.\n    data: ?*anyopaque,\n};\n\nconst Parser = @This();\n\npage: *Page,\nerr: ?Error,\ncontainer: ParsedNode,\narena: Allocator,\nstrings: std.StringHashMapUnmanaged(void),\n\npub fn init(arena: Allocator, node: *Node, page: *Page) Parser {\n    return .{\n        .err = null,\n        .page = page,\n        .strings = .empty,\n        .arena = arena,\n        .container = ParsedNode{\n            .data = null,\n            .node = node,\n        },\n    };\n}\n\nconst Error = struct {\n    err: anyerror,\n    source: Source,\n\n    const Source = enum {\n        pop,\n        append,\n        create_element,\n        create_comment,\n        create_processing_instruction,\n        append_doctype_to_document,\n        add_attrs_if_missing,\n        get_template_content,\n        remove_from_parent,\n        reparent_children,\n        append_before_sibling,\n        append_based_on_parent_node,\n    };\n};\n\npub fn parse(self: *Parser, html: []const u8) void {\n    h5e.html5ever_parse_document(\n        html.ptr,\n        html.len,\n        &self.container,\n        self,\n        createElementCallback,\n        getDataCallback,\n        appendCallback,\n        parseErrorCallback,\n        popCallback,\n        createCommentCallback,\n        createProcessingInstruction,\n        appendDoctypeToDocument,\n        addAttrsIfMissingCallback,\n        getTemplateContentsCallback,\n        removeFromParentCallback,\n        reparentChildrenCallback,\n        appendBeforeSiblingCallback,\n        appendBasedOnParentNodeCallback,\n    );\n}\n\npub fn parseXML(self: *Parser, xml: []const u8) void {\n    h5e.xml5ever_parse_document(\n        xml.ptr,\n        xml.len,\n        &self.container,\n        self,\n        createXMLElementCallback,\n        getDataCallback,\n        appendCallback,\n        parseErrorCallback,\n        popCallback,\n        createCommentCallback,\n        createProcessingInstruction,\n        appendDoctypeToDocument,\n        addAttrsIfMissingCallback,\n        getTemplateContentsCallback,\n        removeFromParentCallback,\n        reparentChildrenCallback,\n        appendBeforeSiblingCallback,\n        appendBasedOnParentNodeCallback,\n    );\n}\n\npub fn parseFragment(self: *Parser, html: []const u8) void {\n    h5e.html5ever_parse_fragment(\n        html.ptr,\n        html.len,\n        &self.container,\n        self,\n        createElementCallback,\n        getDataCallback,\n        appendCallback,\n        parseErrorCallback,\n        popCallback,\n        createCommentCallback,\n        createProcessingInstruction,\n        appendDoctypeToDocument,\n        addAttrsIfMissingCallback,\n        getTemplateContentsCallback,\n        removeFromParentCallback,\n        reparentChildrenCallback,\n        appendBeforeSiblingCallback,\n        appendBasedOnParentNodeCallback,\n    );\n}\n\npub const Streaming = struct {\n    parser: Parser,\n    handle: ?*anyopaque,\n\n    pub fn init(arena: Allocator, node: *Node, page: *Page) Streaming {\n        return .{\n            .handle = null,\n            .parser = Parser.init(arena, node, page),\n        };\n    }\n\n    pub fn deinit(self: *Streaming) void {\n        if (self.handle) |handle| {\n            h5e.html5ever_streaming_parser_destroy(handle);\n        }\n    }\n\n    pub fn start(self: *Streaming) !void {\n        lp.assert(self.handle == null, \"Parser.start non-null handle\", .{});\n\n        self.handle = h5e.html5ever_streaming_parser_create(\n            &self.parser.container,\n            &self.parser,\n            createElementCallback,\n            getDataCallback,\n            appendCallback,\n            parseErrorCallback,\n            popCallback,\n            createCommentCallback,\n            createProcessingInstruction,\n            appendDoctypeToDocument,\n            addAttrsIfMissingCallback,\n            getTemplateContentsCallback,\n            removeFromParentCallback,\n            reparentChildrenCallback,\n            appendBeforeSiblingCallback,\n            appendBasedOnParentNodeCallback,\n        ) orelse return error.ParserCreationFailed;\n    }\n\n    pub fn read(self: *Streaming, data: []const u8) !void {\n        const result = h5e.html5ever_streaming_parser_feed(\n            self.handle.?,\n            data.ptr,\n            data.len,\n        );\n\n        if (result != 0) {\n            // Parser panicked - clean up and return error\n            // Note: deinit will destroy the handle if it exists\n            if (self.handle) |handle| {\n                h5e.html5ever_streaming_parser_destroy(handle);\n                self.handle = null;\n            }\n            return error.ParserPanic;\n        }\n    }\n\n    pub fn done(self: *Streaming) void {\n        h5e.html5ever_streaming_parser_finish(self.handle.?);\n    }\n};\n\nfn parseErrorCallback(ctx: *anyopaque, err: h5e.StringSlice) callconv(.c) void {\n    _ = ctx;\n    _ = err;\n    // std.debug.print(\"PEC: {s}\\n\", .{err.slice()});\n}\n\nfn popCallback(ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._popCallback(getNode(node_ref)) catch |err| {\n        self.err = .{ .err = err, .source = .pop };\n    };\n}\n\nfn _popCallback(self: *Parser, node: *Node) !void {\n    try self.page.nodeComplete(node);\n}\n\nfn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {\n    return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .unknown);\n}\n\nfn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {\n    return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);\n}\n\nfn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {\n        self.err = .{ .err = err, .source = .create_element };\n        return null;\n    };\n}\nfn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {\n    const page = self.page;\n    const name = qname.local.slice();\n    const namespace_string = qname.ns.slice();\n    const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);\n    const node = try page.createElementNS(namespace, name, attributes);\n\n    const pn = try self.arena.create(ParsedNode);\n    pn.* = .{\n        .data = data,\n        .node = node,\n    };\n    return pn;\n}\n\nfn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*anyopaque {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    return self._createCommentCallback(str.slice()) catch |err| {\n        self.err = .{ .err = err, .source = .create_comment };\n        return null;\n    };\n}\nfn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {\n    const page = self.page;\n    const node = try page.createComment(str);\n    const pn = try self.arena.create(ParsedNode);\n    pn.* = .{\n        .data = null,\n        .node = node,\n    };\n    return pn;\n}\n\nfn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h5e.StringSlice) callconv(.c) ?*anyopaque {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    return self._createProcessingInstruction(target.slice(), data.slice()) catch |err| {\n        self.err = .{ .err = err, .source = .create_processing_instruction };\n        return null;\n    };\n}\nfn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque {\n    const page = self.page;\n    const node = try page.createProcessingInstruction(target, data);\n    const pn = try self.arena.create(ParsedNode);\n    pn.* = .{\n        .data = null,\n        .node = node,\n    };\n    return pn;\n}\n\nfn appendDoctypeToDocument(ctx: *anyopaque, name: h5e.StringSlice, public_id: h5e.StringSlice, system_id: h5e.StringSlice) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._appendDoctypeToDocument(name.slice(), public_id.slice(), system_id.slice()) catch |err| {\n        self.err = .{ .err = err, .source = .append_doctype_to_document };\n    };\n}\nfn _appendDoctypeToDocument(self: *Parser, name: []const u8, public_id: []const u8, system_id: []const u8) !void {\n    const page = self.page;\n\n    // Create the DocumentType node\n    const DocumentType = @import(\"../webapi/DocumentType.zig\");\n    const doctype = try page._factory.node(DocumentType{\n        ._proto = undefined,\n        ._name = try page.dupeString(name),\n        ._public_id = try page.dupeString(public_id),\n        ._system_id = try page.dupeString(system_id),\n    });\n\n    // Append it to the document\n    try page.appendNew(self.container.node, .{ .node = doctype.asNode() });\n}\n\nfn addAttrsIfMissingCallback(ctx: *anyopaque, target_ref: *anyopaque, attributes: h5e.AttributeIterator) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._addAttrsIfMissingCallback(getNode(target_ref), attributes) catch |err| {\n        self.err = .{ .err = err, .source = .add_attrs_if_missing };\n    };\n}\nfn _addAttrsIfMissingCallback(self: *Parser, node: *Node, attributes: h5e.AttributeIterator) !void {\n    const element = node.as(Element);\n    const page = self.page;\n\n    const attr_list = try element.getOrCreateAttributeList(page);\n    while (attributes.next()) |attr| {\n        const name = attr.name.local.slice();\n        const value = attr.value.slice();\n        // putNew only adds if the attribute doesn't already exist\n        try attr_list.putNew(name, value, page);\n    }\n}\n\nfn getTemplateContentsCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    return self._getTemplateContentsCallback(getNode(target_ref)) catch |err| {\n        self.err = .{ .err = err, .source = .get_template_content };\n        return null;\n    };\n}\n\nfn _getTemplateContentsCallback(self: *Parser, node: *Node) !*anyopaque {\n    const element = node.as(Element);\n    const template = element._type.html.is(Element.Html.Template) orelse unreachable;\n    const content_node = template.getContent().asNode();\n\n    // Create a ParsedNode wrapper for the content DocumentFragment\n    const pn = try self.arena.create(ParsedNode);\n    pn.* = .{\n        .data = null,\n        .node = content_node,\n    };\n    return pn;\n}\n\nfn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {\n    const pn: *ParsedNode = @ptrCast(@alignCast(ctx));\n    // For non-elements, data is null. But, we expect this to only ever\n    // be called for elements.\n    lp.assert(pn.data != null, \"Parser.getDataCallback null data\", .{});\n    return pn.data.?;\n}\n\nfn appendCallback(ctx: *anyopaque, parent_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._appendCallback(getNode(parent_ref), node_or_text) catch |err| {\n        self.err = .{ .err = err, .source = .append };\n    };\n}\nfn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !void {\n    // child node is guaranteed not to belong to another parent\n    switch (node_or_text.toUnion()) {\n        .node => |cpn| {\n            const child = getNode(cpn);\n            if (child._parent) |previous_parent| {\n                // html5ever says this can't happen, but we might be screwing up\n                // the node on our side. We shouldn't be, but we're seeing this\n                // in the wild, and I'm not sure why. In debug, let's crash so\n                // we can try to figure it out. In release, let's disconnect\n                // the child first.\n                if (comptime IS_DEBUG) {\n                    unreachable;\n                }\n                self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });\n            }\n            try self.page.appendNew(parent, .{ .node = child });\n        },\n        .text => |txt| try self.page.appendNew(parent, .{ .text = txt }),\n    }\n}\n\nfn removeFromParentCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._removeFromParentCallback(getNode(target_ref)) catch |err| {\n        self.err = .{ .err = err, .source = .remove_from_parent };\n    };\n}\nfn _removeFromParentCallback(self: *Parser, node: *Node) !void {\n    const parent = node.parentNode() orelse return;\n    _ = try parent.removeChild(node, self.page);\n}\n\nfn reparentChildrenCallback(ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._reparentChildrenCallback(getNode(node_ref), getNode(new_parent_ref)) catch |err| {\n        self.err = .{ .err = err, .source = .reparent_children };\n    };\n}\nfn _reparentChildrenCallback(self: *Parser, node: *Node, new_parent: *Node) !void {\n    try self.page.appendAllChildren(node, new_parent);\n}\n\nfn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._appendBeforeSiblingCallback(getNode(sibling_ref), node_or_text) catch |err| {\n        self.err = .{ .err = err, .source = .append_before_sibling };\n    };\n}\nfn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {\n    const parent = sibling.parentNode() orelse return error.NoParent;\n    const node: *Node = switch (node_or_text.toUnion()) {\n        .node => |cpn| blk: {\n            const child = getNode(cpn);\n            if (child._parent) |previous_parent| {\n                // A custom element constructor may have inserted the node into the\n                // DOM before the parser officially places it (e.g. via foster\n                // parenting). Detach it first so insertNodeRelative's assertion holds.\n                self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });\n            }\n            break :blk child;\n        },\n        .text => |txt| try self.page.createTextNode(txt),\n    };\n    try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});\n}\n\nfn appendBasedOnParentNodeCallback(ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {\n    const self: *Parser = @ptrCast(@alignCast(ctx));\n    self._appendBasedOnParentNodeCallback(getNode(element_ref), getNode(prev_element_ref), node_or_text) catch |err| {\n        self.err = .{ .err = err, .source = .append_based_on_parent_node };\n    };\n}\nfn _appendBasedOnParentNodeCallback(self: *Parser, element: *Node, prev_element: *Node, node_or_text: h5e.NodeOrText) !void {\n    if (element.parentNode()) |_| {\n        try self._appendBeforeSiblingCallback(element, node_or_text);\n    } else {\n        try self._appendCallback(prev_element, node_or_text);\n    }\n}\n\nfn getNode(ref: *anyopaque) *Node {\n    const pn: *ParsedNode = @ptrCast(@alignCast(ref));\n    return pn.node;\n}\n\nfn asUint(comptime string: anytype) std.meta.Int(\n    .unsigned,\n    @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0\n) {\n    const byteLength = @sizeOf(@TypeOf(string.*)) - 1;\n    const expectedType = *const [byteLength:0]u8;\n    if (@TypeOf(string) != expectedType) {\n        @compileError(\"expected : \" ++ @typeName(expectedType) ++ \", got: \" ++ @typeName(@TypeOf(string)));\n    }\n\n    return @bitCast(@as(*const [byteLength]u8, string).*);\n}\n"
  },
  {
    "path": "src/browser/parser/html5ever.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst ParsedNode = @import(\"Parser.zig\").ParsedNode;\n\npub extern \"c\" fn html5ever_parse_document(\n    html: [*c]const u8,\n    len: usize,\n    doc: *anyopaque,\n    ctx: *anyopaque,\n    createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,\n    elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,\n    appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,\n    popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,\n    createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,\n    createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,\n    appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,\n    addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,\n    getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,\n    removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,\n    reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,\n    appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,\n) void;\n\npub extern \"c\" fn html5ever_parse_fragment(\n    html: [*c]const u8,\n    len: usize,\n    doc: *anyopaque,\n    ctx: *anyopaque,\n    createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,\n    elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,\n    appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,\n    popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,\n    createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,\n    createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,\n    appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,\n    addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,\n    getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,\n    removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,\n    reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,\n    appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,\n) void;\n\npub extern \"c\" fn html5ever_attribute_iterator_next(ctx: *anyopaque) Nullable(Attribute);\npub extern \"c\" fn html5ever_attribute_iterator_count(ctx: *anyopaque) usize;\n\npub extern \"c\" fn html5ever_get_memory_usage() MemoryUsage;\n\npub const MemoryUsage = extern struct {\n    resident: usize,\n    allocated: usize,\n};\n\n// Streaming parser API\npub extern \"c\" fn html5ever_streaming_parser_create(\n    doc: *anyopaque,\n    ctx: *anyopaque,\n    createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,\n    elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,\n    appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,\n    popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,\n    createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,\n    createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,\n    appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,\n    addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,\n    getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,\n    removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,\n    reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,\n    appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,\n) ?*anyopaque;\n\npub extern \"c\" fn html5ever_streaming_parser_feed(\n    parser: *anyopaque,\n    html: [*c]const u8,\n    len: usize,\n) c_int;\n\npub extern \"c\" fn html5ever_streaming_parser_finish(\n    parser: *anyopaque,\n) void;\n\npub extern \"c\" fn html5ever_streaming_parser_destroy(\n    parser: *anyopaque,\n) void;\n\npub fn Nullable(comptime T: type) type {\n    return extern struct {\n        tag: u8,\n        value: T,\n\n        pub fn unwrap(self: @This()) ?T {\n            return if (self.tag == 0) null else self.value;\n        }\n\n        pub fn none() @This() {\n            return .{ .tag = 0, .value = undefined };\n        }\n    };\n}\n\npub const StringSlice = Slice(u8);\npub fn Slice(comptime T: type) type {\n    return extern struct {\n        ptr: [*]const T,\n        len: usize,\n\n        pub fn slice(self: @This()) []const T {\n            return self.ptr[0..self.len];\n        }\n    };\n}\n\npub const QualName = extern struct {\n    prefix: Nullable(StringSlice),\n    ns: StringSlice,\n    local: StringSlice,\n};\n\npub const Attribute = extern struct {\n    name: QualName,\n    value: StringSlice,\n};\n\npub const AttributeIterator = extern struct {\n    iter: *anyopaque,\n\n    pub fn next(self: AttributeIterator) ?Attribute {\n        return html5ever_attribute_iterator_next(self.iter).unwrap();\n    }\n\n    pub fn count(self: AttributeIterator) usize {\n        return html5ever_attribute_iterator_count(self.iter);\n    }\n};\n\npub const NodeOrText = extern struct {\n    tag: u8,\n    node: *anyopaque,\n    text: StringSlice,\n\n    pub fn toUnion(self: NodeOrText) Union {\n        if (self.tag == 0) {\n            return .{ .node = @ptrCast(@alignCast(self.node)) };\n        }\n        return .{ .text = self.text.slice() };\n    }\n\n    const Union = union(enum) {\n        node: *ParsedNode,\n        text: []const u8,\n    };\n};\n\npub extern \"c\" fn xml5ever_parse_document(\n    html: [*c]const u8,\n    len: usize,\n    doc: *anyopaque,\n    ctx: *anyopaque,\n    createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,\n    elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,\n    appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,\n    popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,\n    createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,\n    createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,\n    appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,\n    addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,\n    getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,\n    removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,\n    reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,\n    appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,\n    appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,\n) void;\n"
  },
  {
    "path": "src/browser/reflect.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n// Gets the Parent of child.\n// HtmlElement.of(script) -> *HTMLElement\npub fn Struct(comptime T: type) type {\n    return switch (@typeInfo(T)) {\n        .pointer => |ptr| ptr.child,\n        .@\"struct\" => T,\n        .void => T,\n        else => unreachable,\n    };\n}\n"
  },
  {
    "path": "src/browser/structured_data.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Page = @import(\"Page.zig\");\nconst URL = @import(\"URL.zig\");\nconst TreeWalker = @import(\"webapi/TreeWalker.zig\");\nconst Element = @import(\"webapi/Element.zig\");\nconst Node = @import(\"webapi/Node.zig\");\n\nconst Allocator = std.mem.Allocator;\n\n/// Key-value pair for structured data properties.\npub const Property = struct {\n    key: []const u8,\n    value: []const u8,\n};\n\npub const AlternateLink = struct {\n    href: []const u8,\n    hreflang: ?[]const u8,\n    type: ?[]const u8,\n    title: ?[]const u8,\n};\n\npub const StructuredData = struct {\n    json_ld: []const []const u8,\n    open_graph: []const Property,\n    twitter_card: []const Property,\n    meta: []const Property,\n    links: []const Property,\n    alternate: []const AlternateLink,\n\n    pub fn jsonStringify(self: *const StructuredData, jw: anytype) !void {\n        try jw.beginObject();\n\n        try jw.objectField(\"jsonLd\");\n        try jw.write(self.json_ld);\n\n        try jw.objectField(\"openGraph\");\n        try writeProperties(jw, self.open_graph);\n\n        try jw.objectField(\"twitterCard\");\n        try writeProperties(jw, self.twitter_card);\n\n        try jw.objectField(\"meta\");\n        try writeProperties(jw, self.meta);\n\n        try jw.objectField(\"links\");\n        try writeProperties(jw, self.links);\n\n        if (self.alternate.len > 0) {\n            try jw.objectField(\"alternate\");\n            try jw.beginArray();\n            for (self.alternate) |alt| {\n                try jw.beginObject();\n                try jw.objectField(\"href\");\n                try jw.write(alt.href);\n                if (alt.hreflang) |v| {\n                    try jw.objectField(\"hreflang\");\n                    try jw.write(v);\n                }\n                if (alt.type) |v| {\n                    try jw.objectField(\"type\");\n                    try jw.write(v);\n                }\n                if (alt.title) |v| {\n                    try jw.objectField(\"title\");\n                    try jw.write(v);\n                }\n                try jw.endObject();\n            }\n            try jw.endArray();\n        }\n\n        try jw.endObject();\n    }\n};\n\n/// Serializes properties as a JSON object. When a key appears multiple times\n/// (e.g. multiple og:image tags), values are grouped into an array.\n/// Alternatives considered: always-array values (verbose), or an array of\n/// {key, value} pairs (preserves order but less ergonomic for consumers).\nfn writeProperties(jw: anytype, properties: []const Property) !void {\n    try jw.beginObject();\n    for (properties, 0..) |prop, i| {\n        // Skip keys already written by an earlier occurrence.\n        var already_written = false;\n        for (properties[0..i]) |prev| {\n            if (std.mem.eql(u8, prev.key, prop.key)) {\n                already_written = true;\n                break;\n            }\n        }\n        if (already_written) continue;\n\n        // Count total occurrences to decide string vs array.\n        var count: usize = 0;\n        for (properties) |p| {\n            if (std.mem.eql(u8, p.key, prop.key)) count += 1;\n        }\n\n        try jw.objectField(prop.key);\n        if (count == 1) {\n            try jw.write(prop.value);\n        } else {\n            try jw.beginArray();\n            for (properties) |p| {\n                if (std.mem.eql(u8, p.key, prop.key)) {\n                    try jw.write(p.value);\n                }\n            }\n            try jw.endArray();\n        }\n    }\n    try jw.endObject();\n}\n\n/// Extract all structured data from the page.\npub fn collectStructuredData(\n    root: *Node,\n    arena: Allocator,\n    page: *Page,\n) !StructuredData {\n    var json_ld: std.ArrayList([]const u8) = .empty;\n    var open_graph: std.ArrayList(Property) = .empty;\n    var twitter_card: std.ArrayList(Property) = .empty;\n    var meta: std.ArrayList(Property) = .empty;\n    var links: std.ArrayList(Property) = .empty;\n    var alternate: std.ArrayList(AlternateLink) = .empty;\n\n    // Extract language from the root <html> element.\n    if (root.is(Element)) |root_el| {\n        if (root_el.getAttributeSafe(comptime .wrap(\"lang\"))) |lang| {\n            try meta.append(arena, .{ .key = \"language\", .value = lang });\n        }\n    } else {\n        // Root is document — check documentElement.\n        var children = root.childrenIterator();\n        while (children.next()) |child| {\n            const el = child.is(Element) orelse continue;\n            if (el.getTag() == .html) {\n                if (el.getAttributeSafe(comptime .wrap(\"lang\"))) |lang| {\n                    try meta.append(arena, .{ .key = \"language\", .value = lang });\n                }\n                break;\n            }\n        }\n    }\n\n    var tw = TreeWalker.Full.init(root, .{});\n    while (tw.next()) |node| {\n        const el = node.is(Element) orelse continue;\n\n        switch (el.getTag()) {\n            .script => {\n                try collectJsonLd(el, arena, &json_ld);\n                tw.skipChildren();\n            },\n            .meta => collectMeta(el, &open_graph, &twitter_card, &meta, arena) catch {},\n            .title => try collectTitle(node, arena, &meta),\n            .link => try collectLink(el, arena, page, &links, &alternate),\n            // Skip body subtree for non-JSON-LD — all other metadata is in <head>.\n            // JSON-LD can appear in <body> so we don't skip the whole body.\n            else => {},\n        }\n    }\n\n    return .{\n        .json_ld = json_ld.items,\n        .open_graph = open_graph.items,\n        .twitter_card = twitter_card.items,\n        .meta = meta.items,\n        .links = links.items,\n        .alternate = alternate.items,\n    };\n}\n\nfn collectJsonLd(\n    el: *Element,\n    arena: Allocator,\n    json_ld: *std.ArrayList([]const u8),\n) !void {\n    const type_attr = el.getAttributeSafe(comptime .wrap(\"type\")) orelse return;\n    if (!std.ascii.eqlIgnoreCase(type_attr, \"application/ld+json\")) return;\n\n    var buf: std.Io.Writer.Allocating = .init(arena);\n    try el.asNode().getTextContent(&buf.writer);\n    const text = buf.written();\n    if (text.len > 0) {\n        try json_ld.append(arena, std.mem.trim(u8, text, &std.ascii.whitespace));\n    }\n}\n\nfn collectMeta(\n    el: *Element,\n    open_graph: *std.ArrayList(Property),\n    twitter_card: *std.ArrayList(Property),\n    meta: *std.ArrayList(Property),\n    arena: Allocator,\n) !void {\n    // charset: <meta charset=\"...\"> (no content attribute needed).\n    if (el.getAttributeSafe(comptime .wrap(\"charset\"))) |charset| {\n        try meta.append(arena, .{ .key = \"charset\", .value = charset });\n    }\n\n    const content = el.getAttributeSafe(comptime .wrap(\"content\")) orelse return;\n\n    // Open Graph: <meta property=\"og:...\">\n    if (el.getAttributeSafe(comptime .wrap(\"property\"))) |property| {\n        if (std.mem.startsWith(u8, property, \"og:\")) {\n            try open_graph.append(arena, .{ .key = property[3..], .value = content });\n            return;\n        }\n        // Article, profile, etc. are OG sub-namespaces.\n        if (std.mem.startsWith(u8, property, \"article:\") or\n            std.mem.startsWith(u8, property, \"profile:\") or\n            std.mem.startsWith(u8, property, \"book:\") or\n            std.mem.startsWith(u8, property, \"music:\") or\n            std.mem.startsWith(u8, property, \"video:\"))\n        {\n            try open_graph.append(arena, .{ .key = property, .value = content });\n            return;\n        }\n    }\n\n    // Twitter Cards: <meta name=\"twitter:...\">\n    if (el.getAttributeSafe(comptime .wrap(\"name\"))) |name| {\n        if (std.mem.startsWith(u8, name, \"twitter:\")) {\n            try twitter_card.append(arena, .{ .key = name[8..], .value = content });\n            return;\n        }\n\n        // Standard meta tags by name.\n        const known_names = [_][]const u8{\n            \"description\", \"author\",    \"keywords\",    \"robots\",\n            \"viewport\",    \"generator\", \"theme-color\",\n        };\n        for (known_names) |known| {\n            if (std.ascii.eqlIgnoreCase(name, known)) {\n                try meta.append(arena, .{ .key = known, .value = content });\n                return;\n            }\n        }\n    }\n\n    // http-equiv (e.g. Content-Type, refresh)\n    if (el.getAttributeSafe(comptime .wrap(\"http-equiv\"))) |http_equiv| {\n        try meta.append(arena, .{ .key = http_equiv, .value = content });\n    }\n}\n\nfn collectTitle(\n    node: *Node,\n    arena: Allocator,\n    meta: *std.ArrayList(Property),\n) !void {\n    var buf: std.Io.Writer.Allocating = .init(arena);\n    try node.getTextContent(&buf.writer);\n    const text = std.mem.trim(u8, buf.written(), &std.ascii.whitespace);\n    if (text.len > 0) {\n        try meta.append(arena, .{ .key = \"title\", .value = text });\n    }\n}\n\nfn collectLink(\n    el: *Element,\n    arena: Allocator,\n    page: *Page,\n    links: *std.ArrayList(Property),\n    alternate: *std.ArrayList(AlternateLink),\n) !void {\n    const rel = el.getAttributeSafe(comptime .wrap(\"rel\")) orelse return;\n    const raw_href = el.getAttributeSafe(comptime .wrap(\"href\")) orelse return;\n    const href = URL.resolve(arena, page.base(), raw_href, .{ .encode = true }) catch raw_href;\n\n    if (std.ascii.eqlIgnoreCase(rel, \"alternate\")) {\n        try alternate.append(arena, .{\n            .href = href,\n            .hreflang = el.getAttributeSafe(comptime .wrap(\"hreflang\")),\n            .type = el.getAttributeSafe(comptime .wrap(\"type\")),\n            .title = el.getAttributeSafe(comptime .wrap(\"title\")),\n        });\n        return;\n    }\n\n    const relevant_rels = [_][]const u8{\n        \"canonical\",        \"icon\",       \"manifest\", \"shortcut icon\",\n        \"apple-touch-icon\", \"search\",     \"author\",   \"license\",\n        \"dns-prefetch\",     \"preconnect\",\n    };\n    for (relevant_rels) |known| {\n        if (std.ascii.eqlIgnoreCase(rel, known)) {\n            try links.append(arena, .{ .key = known, .value = href });\n            return;\n        }\n    }\n}\n\n// --- Tests ---\n\nconst testing = @import(\"../testing.zig\");\n\nfn testStructuredData(html: []const u8) !StructuredData {\n    const page = try testing.test_session.createPage();\n    defer testing.test_session.removePage();\n\n    const doc = page.window._document;\n    const div = try doc.createElement(\"div\", null, page);\n    try page.parseHtmlAsChildren(div.asNode(), html);\n\n    return collectStructuredData(div.asNode(), page.call_arena, page);\n}\n\nfn findProperty(props: []const Property, key: []const u8) ?[]const u8 {\n    for (props) |p| {\n        if (std.mem.eql(u8, p.key, key)) return p.value;\n    }\n    return null;\n}\n\ntest \"structured_data: json-ld\" {\n    const data = try testStructuredData(\n        \\\\<script type=\"application/ld+json\">\n        \\\\{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"headline\":\"Test\"}\n        \\\\</script>\n    );\n    try testing.expectEqual(1, data.json_ld.len);\n    try testing.expect(std.mem.indexOf(u8, data.json_ld[0], \"Article\") != null);\n}\n\ntest \"structured_data: multiple json-ld\" {\n    const data = try testStructuredData(\n        \\\\<script type=\"application/ld+json\">{\"@type\":\"Organization\"}</script>\n        \\\\<script type=\"application/ld+json\">{\"@type\":\"BreadcrumbList\"}</script>\n        \\\\<script type=\"text/javascript\">var x = 1;</script>\n    );\n    try testing.expectEqual(2, data.json_ld.len);\n}\n\ntest \"structured_data: open graph\" {\n    const data = try testStructuredData(\n        \\\\<meta property=\"og:title\" content=\"My Page\">\n        \\\\<meta property=\"og:description\" content=\"A description\">\n        \\\\<meta property=\"og:image\" content=\"https://example.com/img.jpg\">\n        \\\\<meta property=\"og:url\" content=\"https://example.com\">\n        \\\\<meta property=\"og:type\" content=\"article\">\n        \\\\<meta property=\"article:published_time\" content=\"2026-03-10\">\n    );\n    try testing.expectEqual(6, data.open_graph.len);\n    try testing.expectEqual(\"My Page\", findProperty(data.open_graph, \"title\").?);\n    try testing.expectEqual(\"article\", findProperty(data.open_graph, \"type\").?);\n    try testing.expectEqual(\"2026-03-10\", findProperty(data.open_graph, \"article:published_time\").?);\n}\n\ntest \"structured_data: open graph duplicate keys\" {\n    const data = try testStructuredData(\n        \\\\<meta property=\"og:title\" content=\"My Page\">\n        \\\\<meta property=\"og:image\" content=\"https://example.com/img1.jpg\">\n        \\\\<meta property=\"og:image\" content=\"https://example.com/img2.jpg\">\n        \\\\<meta property=\"og:image\" content=\"https://example.com/img3.jpg\">\n    );\n    // Duplicate keys are preserved as separate Property entries.\n    try testing.expectEqual(4, data.open_graph.len);\n\n    // Verify serialization groups duplicates into arrays.\n    const json = try std.json.Stringify.valueAlloc(testing.allocator, data, .{});\n    defer testing.allocator.free(json);\n\n    const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{});\n    defer parsed.deinit();\n    const og = parsed.value.object.get(\"openGraph\").?.object;\n    // \"title\" appears once → string.\n    switch (og.get(\"title\").?) {\n        .string => {},\n        else => return error.TestUnexpectedResult,\n    }\n    // \"image\" appears 3 times → array.\n    switch (og.get(\"image\").?) {\n        .array => |arr| try testing.expectEqual(3, arr.items.len),\n        else => return error.TestUnexpectedResult,\n    }\n}\n\ntest \"structured_data: twitter card\" {\n    const data = try testStructuredData(\n        \\\\<meta name=\"twitter:card\" content=\"summary_large_image\">\n        \\\\<meta name=\"twitter:site\" content=\"@example\">\n        \\\\<meta name=\"twitter:title\" content=\"My Page\">\n    );\n    try testing.expectEqual(3, data.twitter_card.len);\n    try testing.expectEqual(\"summary_large_image\", findProperty(data.twitter_card, \"card\").?);\n    try testing.expectEqual(\"@example\", findProperty(data.twitter_card, \"site\").?);\n}\n\ntest \"structured_data: meta tags\" {\n    const data = try testStructuredData(\n        \\\\<title>Page Title</title>\n        \\\\<meta name=\"description\" content=\"A test page\">\n        \\\\<meta name=\"author\" content=\"Test Author\">\n        \\\\<meta name=\"keywords\" content=\"test, example\">\n        \\\\<meta name=\"robots\" content=\"index, follow\">\n    );\n    try testing.expectEqual(\"Page Title\", findProperty(data.meta, \"title\").?);\n    try testing.expectEqual(\"A test page\", findProperty(data.meta, \"description\").?);\n    try testing.expectEqual(\"Test Author\", findProperty(data.meta, \"author\").?);\n    try testing.expectEqual(\"test, example\", findProperty(data.meta, \"keywords\").?);\n    try testing.expectEqual(\"index, follow\", findProperty(data.meta, \"robots\").?);\n}\n\ntest \"structured_data: link elements\" {\n    const data = try testStructuredData(\n        \\\\<link rel=\"canonical\" href=\"https://example.com/page\">\n        \\\\<link rel=\"icon\" href=\"/favicon.ico\">\n        \\\\<link rel=\"manifest\" href=\"/manifest.json\">\n        \\\\<link rel=\"stylesheet\" href=\"/style.css\">\n    );\n    try testing.expectEqual(3, data.links.len);\n    try testing.expectEqual(\"https://example.com/page\", findProperty(data.links, \"canonical\").?);\n    // stylesheet should be filtered out\n    try testing.expectEqual(null, findProperty(data.links, \"stylesheet\"));\n}\n\ntest \"structured_data: alternate links\" {\n    const data = try testStructuredData(\n        \\\\<link rel=\"alternate\" href=\"https://example.com/fr\" hreflang=\"fr\" title=\"French\">\n        \\\\<link rel=\"alternate\" href=\"https://example.com/de\" hreflang=\"de\">\n    );\n    try testing.expectEqual(2, data.alternate.len);\n    try testing.expectEqual(\"fr\", data.alternate[0].hreflang.?);\n    try testing.expectEqual(\"French\", data.alternate[0].title.?);\n    try testing.expectEqual(\"de\", data.alternate[1].hreflang.?);\n    try testing.expectEqual(null, data.alternate[1].title);\n}\n\ntest \"structured_data: non-metadata elements ignored\" {\n    const data = try testStructuredData(\n        \\\\<div>Just text</div>\n        \\\\<p>More text</p>\n        \\\\<a href=\"/link\">Link</a>\n    );\n    try testing.expectEqual(0, data.json_ld.len);\n    try testing.expectEqual(0, data.open_graph.len);\n    try testing.expectEqual(0, data.twitter_card.len);\n    try testing.expectEqual(0, data.meta.len);\n    try testing.expectEqual(0, data.links.len);\n}\n\ntest \"structured_data: charset and http-equiv\" {\n    const data = try testStructuredData(\n        \\\\<meta charset=\"utf-8\">\n        \\\\<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    );\n    try testing.expectEqual(\"utf-8\", findProperty(data.meta, \"charset\").?);\n    try testing.expectEqual(\"text/html; charset=utf-8\", findProperty(data.meta, \"Content-Type\").?);\n}\n\ntest \"structured_data: mixed content\" {\n    const data = try testStructuredData(\n        \\\\<title>My Site</title>\n        \\\\<meta property=\"og:title\" content=\"OG Title\">\n        \\\\<meta name=\"twitter:card\" content=\"summary\">\n        \\\\<meta name=\"description\" content=\"A page\">\n        \\\\<link rel=\"canonical\" href=\"https://example.com\">\n        \\\\<script type=\"application/ld+json\">{\"@type\":\"WebSite\"}</script>\n    );\n    try testing.expectEqual(1, data.json_ld.len);\n    try testing.expectEqual(1, data.open_graph.len);\n    try testing.expectEqual(1, data.twitter_card.len);\n    try testing.expectEqual(\"My Site\", findProperty(data.meta, \"title\").?);\n    try testing.expectEqual(\"A page\", findProperty(data.meta, \"description\").?);\n    try testing.expectEqual(1, data.links.len);\n}\n"
  },
  {
    "path": "src/browser/tests/animation/animation.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=animation>\n  let a1 = document.createElement('div').animate(null, null);\n  testing.expectEqual('idle', a1.playState);\n\n  let cb = [];\n  a1.finished.then((x) => {\n    cb.push(a1.playState);\n    cb.push(x == a1);\n  });\n  a1.ready.then(() => {\n    cb.push(a1.playState);\n    a1.play();\n    cb.push(a1.playState);\n  });\n  testing.eventually(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));\n</script>\n\n<script id=startTime>\n  let a2 = document.createElement('div').animate(null, null);\n  // startTime defaults to null\n  testing.expectEqual(null, a2.startTime);\n  // startTime is settable\n  a2.startTime = 42.5;\n  testing.expectEqual(42.5, a2.startTime);\n  // startTime can be reset to null\n  a2.startTime = null;\n  testing.expectEqual(null, a2.startTime);\n</script>\n\n<script id=onfinish>\n  let a3 = document.createElement('div').animate(null, null);\n  // onfinish defaults to null\n  testing.expectEqual(null, a3.onfinish);\n\n  let calls = [];\n  // onfinish callback should be scheduled and called asynchronously\n  a3.onfinish = function() { calls.push('finish'); };\n  a3.play();\n  testing.eventually(() => testing.expectEqual(['finish'], calls));\n</script>\n\n<script id=pause>\n  let a4 = document.createElement('div').animate(null, null);\n  let cb4 = [];\n  a4.finished.then((x) => { cb4.push(a4.playState) });\n  a4.ready.then(() => {\n    a4.play();\n    cb4.push(a4.playState)\n    a4.pause();\n    cb4.push(a4.playState)\n  });\n  testing.eventually(() => testing.expectEqual(['running', 'paused'], cb4));\n</script>\n\n<script id=finish>\n  let a5 = document.createElement('div').animate(null, null);\n  testing.expectEqual('idle', a5.playState);\n\n  let cb5 = [];\n  a5.finished.then((x) => { cb5.push(a5.playState) });\n  a5.ready.then(() => {\n    cb5.push(a5.playState);\n    a5.play();\n  });\n  testing.eventually(() => testing.expectEqual(['idle', 'finished'], cb5));\n</script>\n"
  },
  {
    "path": "src/browser/tests/blob.html",
    "content": "<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n<script src=\"./testing.js\"></script>\n\n<script id=basic>\n  {\n    const parts = [\"\\r\\nthe quick brown\\rfo\\rx\\r\", \"\\njumps over\\r\\nthe\\nlazy\\r\", \"\\ndog\"];\n    // \"transparent\" ending should not modify the final buffer.\n    const blob = new Blob(parts, { type: \"text/html\" });\n\n    const expected = parts.join(\"\");\n    testing.expectEqual(expected.length, blob.size);\n    testing.expectEqual(\"text/html\", blob.type);\n    testing.async(async () => { testing.expectEqual(expected, await blob.text()) });\n  }\n\n  {\n    const parts = [\"\\rhello\\r\", \"\\nwor\\r\\nld\"];\n    // \"native\" ending should modify the final buffer.\n    const blob = new Blob(parts, { endings: \"native\" });\n\n    const expected = \"\\nhello\\n\\nwor\\nld\";\n    testing.expectEqual(expected.length, blob.size);\n    testing.async(async () => { testing.expectEqual(expected, await blob.text()) });\n  }\n</script>\n\n<!-- Firefox and Safari only -->\n<script id=bytes>\n  {\n    const parts = [\"light \", \"panda \", \"rocks \", \"!\"];\n    const blob = new Blob(parts);\n\n    testing.async(async() => {\n      const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97,\n                                       110, 100, 97, 32, 114, 111, 99, 107, 115,\n                                       32, 33]);\n      const result = await blob.bytes();\n      testing.expectEqual(true, result instanceof Uint8Array);\n      testing.expectEqual(expected, result);\n    });\n  }\n\n  // Test for SIMD.\n  {\n    const parts = [\n      \"\\rThe opened package\\r\\nof potato\\nchi\\rps\",\n      \"held the\\r\\nanswer to the\\r mystery. Both det\\rectives looke\\r\\rd\\r\",\n      \"\\rat it but failed to realize\\nit was\\r\\nthe\\rkey\\r\\n\",\n      \"\\r\\nto solve the \\rcrime.\\r\"\n    ];\n\n    const blob = new Blob(parts, { type: \"text/html\", endings: \"native\" });\n    testing.expectEqual(161, blob.size);\n    testing.expectEqual(\"text/html\", blob.type);\n    testing.async(async() => {\n      const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110,\n                                       101, 100, 32, 112, 97, 99, 107, 97, 103,\n                                       101, 10, 111, 102, 32, 112, 111, 116, 97,\n                                       116, 111, 10, 99, 104, 105, 10, 112, 115,\n                                       104, 101, 108, 100, 32, 116, 104, 101, 10,\n                                       97, 110, 115, 119, 101, 114, 32, 116, 111,\n                                       32, 116, 104, 101, 10, 32, 109, 121, 115,\n                                       116, 101, 114, 121, 46, 32, 66, 111, 116,\n                                       104, 32, 100, 101, 116, 10, 101, 99, 116,\n                                       105, 118, 101, 115, 32, 108, 111, 111, 107,\n                                       101, 10, 10, 100, 10, 10, 97, 116, 32, 105,\n                                       116, 32, 98, 117, 116, 32, 102, 97, 105, 108,\n                                       101, 100, 32, 116, 111, 32, 114, 101, 97,\n                                       108, 105, 122, 101, 10, 105, 116, 32, 119, 97,\n                                       115, 10, 116, 104, 101, 10, 107, 101, 121,\n                                       10, 10, 116, 111, 32, 115, 111, 108, 118, 101,\n                                       32, 116, 104, 101, 32, 10, 99, 114, 105, 109,\n                                       101, 46, 10]);\n      const result = await blob.bytes();\n      testing.expectEqual(true, result instanceof Uint8Array);\n      testing.expectEqual(expected, result);\n    });\n  }\n</script>\n\n<script id=stream>\n  {\n    const parts = [\"may\", \"thy\", \"knife\", \"chip\", \"and\", \"shatter\"];\n    const blob = new Blob(parts);\n    const reader = blob.stream().getReader();\n\n    testing.async(async () => {\n      const {done: done, value: value} = await reader.read()\n      const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110,\n                                       105, 102, 101, 99, 104, 105, 112, 97,\n                                       110, 100, 115, 104, 97, 116, 116, 101,\n                                       114]);\n      testing.expectEqual(false, done);\n      testing.expectEqual(true, value instanceof Uint8Array);\n      testing.expectEqual(expected, value);\n    });\n  }\n</script>\n\n<script id=mime_parsing>\n  // MIME types are lowercased\n  {\n    const blob = new Blob([], { type: \"TEXT/HTML\" });\n    testing.expectEqual(\"text/html\", blob.type);\n  }\n\n  {\n    const blob = new Blob([], { type: \"Application/JSON\" });\n    testing.expectEqual(\"application/json\", blob.type);\n  }\n\n  // MIME with parameters - lowercased\n  {\n    const blob = new Blob([], { type: \"text/html; charset=UTF-8\" });\n    testing.expectEqual(\"text/html; charset=utf-8\", blob.type);\n  }\n\n  // Any ASCII string is accepted and lowercased (no MIME structure validation)\n  {\n    const blob = new Blob([], { type: \"invalid\" });\n    testing.expectEqual(\"invalid\", blob.type);\n  }\n\n  {\n    const blob = new Blob([], { type: \"/\" });\n    testing.expectEqual(\"/\", blob.type);\n  }\n\n  // Non-ASCII characters cause empty string (chars outside U+0020-U+007E)\n  {\n    const blob = new Blob([], { type: \"ý/x\" });\n    testing.expectEqual(\"\", blob.type);\n  }\n\n  {\n    const blob = new Blob([], { type: \"text/plàin\" });\n    testing.expectEqual(\"\", blob.type);\n  }\n\n  // Control characters cause empty string\n  {\n    const blob = new Blob([], { type: \"text/html\\x00\" });\n    testing.expectEqual(\"\", blob.type);\n  }\n\n  // Empty type stays empty\n  {\n    const blob = new Blob([]);\n    testing.expectEqual(\"\", blob.type);\n  }\n\n  {\n    const blob = new Blob([], { type: \"\" });\n    testing.expectEqual(\"\", blob.type);\n  }\n</script>\n\n<script id=slice>\n  {\n    const parts = [\"la\", \"symphonie\", \"des\", \"éclairs\"];\n    const blob = new Blob(parts);\n    testing.async(async () => {\n      const result = await blob.arrayBuffer();\n      testing.expectEqual(true, result instanceof ArrayBuffer)\n    });\n\n    let temp = blob.slice(0);\n    testing.expectEqual(blob.size, temp.size);\n    testing.async(async () => {\n      testing.expectEqual(\"lasymphoniedeséclairs\", await temp.text());\n    });\n\n    temp = blob.slice(-4, -2, \"custom\");\n    testing.expectEqual(2, temp.size);\n    testing.expectEqual(\"custom\", temp.type);\n    testing.async(async () => {\n      testing.expectEqual(\"ai\", await temp.text());\n    });\n\n    temp = blob.slice(14);\n    testing.expectEqual(8, temp.size);\n    testing.async(async () => {\n      testing.expectEqual(\"éclairs\", await temp.text());\n    });\n\n    temp = blob.slice(6, -10, \"text/eclair\");\n    testing.expectEqual(6, temp.size);\n    testing.expectEqual(\"text/eclair\", temp.type);\n    testing.async(async () => {\n      testing.expectEqual(\"honied\", await temp.text());\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/canvas/canvas_rendering_context_2d.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=CanvasRenderingContext2D>\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n  testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);\n  // We can't really test this but let's try to call it at least.\n  ctx.fillRect(0, 0, 0, 0);\n}\n</script>\n\n<script id=CanvasRenderingContext2D#fillStyle>\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n\n  // Black by default.\n  testing.expectEqual(ctx.fillStyle, \"#000000\");\n  ctx.fillStyle = \"red\";\n  testing.expectEqual(ctx.fillStyle, \"#ff0000\");\n  ctx.fillStyle = \"rebeccapurple\";\n  testing.expectEqual(ctx.fillStyle, \"#663399\");\n  // No changes made if color is invalid.\n  ctx.fillStyle = \"invalid-color\";\n  testing.expectEqual(ctx.fillStyle, \"#663399\");\n  ctx.fillStyle = \"#fc0\";\n  testing.expectEqual(ctx.fillStyle, \"#ffcc00\");\n  ctx.fillStyle = \"#ff0000\";\n  testing.expectEqual(ctx.fillStyle, \"#ff0000\");\n  ctx.fillStyle = \"#fF00000F\";\n  testing.expectEqual(ctx.fillStyle, \"rgba(255, 0, 0, 0.06)\");\n}\n</script>\n\n<script id=\"CanvasRenderingContext2D#createImageData(width, height)\">\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n\n  const imageData = ctx.createImageData(100, 200);\n  testing.expectEqual(true, imageData instanceof ImageData);\n  testing.expectEqual(imageData.width, 100);\n  testing.expectEqual(imageData.height, 200);\n  testing.expectEqual(imageData.data.length, 100 * 200 * 4);\n  testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);\n\n  // All pixels should be initialized to 0.\n  testing.expectEqual(imageData.data[0], 0);\n  testing.expectEqual(imageData.data[1], 0);\n  testing.expectEqual(imageData.data[2], 0);\n  testing.expectEqual(imageData.data[3], 0);\n}\n</script>\n\n<script id=\"CanvasRenderingContext2D#createImageData(imageData)\">\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n\n  const source = ctx.createImageData(50, 75);\n  const imageData = ctx.createImageData(source);\n  testing.expectEqual(true, imageData instanceof ImageData);\n  testing.expectEqual(imageData.width, 50);\n  testing.expectEqual(imageData.height, 75);\n  testing.expectEqual(imageData.data.length, 50 * 75 * 4);\n}\n</script>\n\n<script id=\"CanvasRenderingContext2D#putImageData\">\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n\n  const imageData = ctx.createImageData(10, 10);\n  testing.expectEqual(true, imageData instanceof ImageData);\n  // Modify some pixel data.\n  imageData.data[0] = 255;\n  imageData.data[1] = 0;\n  imageData.data[2] = 0;\n  imageData.data[3] = 255;\n\n  // putImageData should not throw.\n  ctx.putImageData(imageData, 0, 0);\n  ctx.putImageData(imageData, 10, 20);\n  // With dirty rect parameters.\n  ctx.putImageData(imageData, 0, 0, 0, 0, 5, 5);\n}\n</script>\n\n<script id=\"CanvasRenderingContext2D#getImageData\">\n{\n  const element = document.createElement(\"canvas\");\n  element.width = 100;\n  element.height = 50;\n  const ctx = element.getContext(\"2d\");\n\n  const imageData = ctx.getImageData(0, 0, 10, 20);\n  testing.expectEqual(true, imageData instanceof ImageData);\n  testing.expectEqual(imageData.width, 10);\n  testing.expectEqual(imageData.height, 20);\n  testing.expectEqual(imageData.data.length, 10 * 20 * 4);\n  testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);\n\n  // Undrawn canvas should return transparent black pixels.\n  testing.expectEqual(imageData.data[0], 0);\n  testing.expectEqual(imageData.data[1], 0);\n  testing.expectEqual(imageData.data[2], 0);\n  testing.expectEqual(imageData.data[3], 0);\n}\n</script>\n\n<script id=\"CanvasRenderingContext2D#getImageData invalid\">\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n\n  // Zero or negative width/height should throw IndexSizeError.\n  testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));\n  testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, 0));\n  testing.expectError('Index or size', () => ctx.getImageData(0, 0, -5, 10));\n  testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));\n}\n</script>\n\n\n<script id=\"getter\">\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n  testing.expectEqual('10px sans-serif', ctx.font);\n  ctx.font = 'bold 48px serif'\n  testing.expectEqual('bold 48px serif', ctx.font);\n\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/canvas/offscreen_canvas.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=OffscreenCanvas>\n{\n  const canvas = new OffscreenCanvas(256, 256);\n  testing.expectEqual(true, canvas instanceof OffscreenCanvas);\n  testing.expectEqual(canvas.width, 256);\n  testing.expectEqual(canvas.height, 256);\n}\n</script>\n\n<script id=OffscreenCanvas#width>\n{\n  const canvas = new OffscreenCanvas(100, 200);\n  testing.expectEqual(canvas.width, 100);\n  canvas.width = 300;\n  testing.expectEqual(canvas.width, 300);\n}\n</script>\n\n<script id=OffscreenCanvas#height>\n{\n  const canvas = new OffscreenCanvas(100, 200);\n  testing.expectEqual(canvas.height, 200);\n  canvas.height = 400;\n  testing.expectEqual(canvas.height, 400);\n}\n</script>\n\n<script id=OffscreenCanvas#getContext>\n{\n  const canvas = new OffscreenCanvas(64, 64);\n  const ctx = canvas.getContext(\"2d\");\n  testing.expectEqual(true, ctx instanceof OffscreenCanvasRenderingContext2D);\n  // We can't really test rendering but let's try to call it at least.\n  ctx.fillRect(0, 0, 10, 10);\n}\n</script>\n\n<script id=OffscreenCanvas#convertToBlob>\n{\n  const canvas = new OffscreenCanvas(64, 64);\n  const promise = canvas.convertToBlob();\n  testing.expectEqual(true, promise instanceof Promise);\n  // The promise should resolve to a Blob (even if empty)\n  promise.then(blob => {\n    testing.expectEqual(true, blob instanceof Blob);\n    testing.expectEqual(blob.size, 0); // Empty since no rendering\n  });\n}\n</script>\n\n<script id=HTMLCanvasElement#transferControlToOffscreen>\n{\n  const htmlCanvas = document.createElement(\"canvas\");\n  htmlCanvas.width = 128;\n  htmlCanvas.height = 96;\n  const offscreen = htmlCanvas.transferControlToOffscreen();\n  testing.expectEqual(true, offscreen instanceof OffscreenCanvas);\n  testing.expectEqual(offscreen.width, 128);\n  testing.expectEqual(offscreen.height, 96);\n}\n</script>\n\n<script id=OffscreenCanvasRenderingContext2D#getImageData>\n{\n  const canvas = new OffscreenCanvas(100, 50);\n  const ctx = canvas.getContext(\"2d\");\n\n  const imageData = ctx.getImageData(0, 0, 10, 20);\n  testing.expectEqual(true, imageData instanceof ImageData);\n  testing.expectEqual(imageData.width, 10);\n  testing.expectEqual(imageData.height, 20);\n  testing.expectEqual(imageData.data.length, 10 * 20 * 4);\n\n  // Undrawn canvas should return transparent black pixels.\n  testing.expectEqual(imageData.data[0], 0);\n  testing.expectEqual(imageData.data[1], 0);\n  testing.expectEqual(imageData.data[2], 0);\n  testing.expectEqual(imageData.data[3], 0);\n\n  // Zero or negative dimensions should throw.\n  testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));\n  testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/canvas/webgl_rendering_context.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=WebGLRenderingContext#getSupportedExtensions>\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"webgl\");\n  testing.expectEqual(true, ctx instanceof WebGLRenderingContext);\n\n  const supportedExtensions = ctx.getSupportedExtensions();\n  // The order Chrome prefer.\n  const expectedExtensions = [\n      \"ANGLE_instanced_arrays\",\n      \"EXT_blend_minmax\",\n      \"EXT_clip_control\",\n      \"EXT_color_buffer_half_float\",\n      \"EXT_depth_clamp\",\n      \"EXT_disjoint_timer_query\",\n      \"EXT_float_blend\",\n      \"EXT_frag_depth\",\n      \"EXT_polygon_offset_clamp\",\n      \"EXT_shader_texture_lod\",\n      \"EXT_texture_compression_bptc\",\n      \"EXT_texture_compression_rgtc\",\n      \"EXT_texture_filter_anisotropic\",\n      \"EXT_texture_mirror_clamp_to_edge\",\n      \"EXT_sRGB\",\n      \"KHR_parallel_shader_compile\",\n      \"OES_element_index_uint\",\n      \"OES_fbo_render_mipmap\",\n      \"OES_standard_derivatives\",\n      \"OES_texture_float\",\n      \"OES_texture_float_linear\",\n      \"OES_texture_half_float\",\n      \"OES_texture_half_float_linear\",\n      \"OES_vertex_array_object\",\n      \"WEBGL_blend_func_extended\",\n      \"WEBGL_color_buffer_float\",\n      \"WEBGL_compressed_texture_astc\",\n      \"WEBGL_compressed_texture_etc\",\n      \"WEBGL_compressed_texture_etc1\",\n      \"WEBGL_compressed_texture_pvrtc\",\n      \"WEBGL_compressed_texture_s3tc\",\n      \"WEBGL_compressed_texture_s3tc_srgb\",\n      \"WEBGL_debug_renderer_info\",\n      \"WEBGL_debug_shaders\",\n      \"WEBGL_depth_texture\",\n      \"WEBGL_draw_buffers\",\n      \"WEBGL_lose_context\",\n      \"WEBGL_multi_draw\",\n      \"WEBGL_polygon_mode\"\n  ];\n\n  testing.expectEqual(expectedExtensions.length, supportedExtensions.length);\n  for (let i = 0; i < expectedExtensions.length; i++) {\n    testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);\n  }\n}\n</script>\n\n<script id=WebGLRenderingCanvas#getExtension>\n// WEBGL_debug_renderer_info\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"webgl\");\n  const rendererInfo = ctx.getExtension(\"WEBGL_debug_renderer_info\");\n  testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);\n\n  const { UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL } = rendererInfo;\n  testing.expectEqual(UNMASKED_VENDOR_WEBGL, 0x9245);\n  testing.expectEqual(UNMASKED_RENDERER_WEBGL, 0x9246);\n\n  testing.expectEqual(\"\", ctx.getParameter(UNMASKED_VENDOR_WEBGL));\n  testing.expectEqual(\"\", ctx.getParameter(UNMASKED_RENDERER_WEBGL));\n}\n\n// WEBGL_lose_context\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"webgl\");\n  const loseContext = ctx.getExtension(\"WEBGL_lose_context\");\n  testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);\n\n  loseContext.loseContext();\n  loseContext.restoreContext();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/cdata/cdata_section.html",
    "content": "cdataClassName<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\"></div>\n\n<script id=\"createInHTMLDocument\">\n{\n  try {\n    document.createCDATASection('test');\n    testing.fail('Should have thrown NotSupportedError');\n  } catch (err) {\n    testing.expectEqual('NotSupportedError', err.name);\n  }\n}\n</script>\n\n<script id=\"createInXMLDocument\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('Hello World');\n\n  testing.expectEqual(4, cdata.nodeType);\n  testing.expectEqual('#cdata-section', cdata.nodeName);\n  testing.expectEqual('Hello World', cdata.data);\n  testing.expectEqual(11, cdata.length);\n}\n</script>\n\n<script id=\"cdataWithSpecialChars\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('<tag>&amp;\"quotes\"</tag>');\n\n  testing.expectEqual('<tag>&amp;\"quotes\"</tag>', cdata.data);\n}\n</script>\n\n<script id=\"cdataRejectsEndMarker\">\n{\n  const doc = new Document();\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => doc.createCDATASection('foo ]]> bar'));\n}\n</script>\n\n<script id=\"cdataRejectsEndMarkerEdgeCase\">\n{\n  const doc = new Document();\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => doc.createCDATASection(']]>'));\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => doc.createCDATASection('start]]>end'));\n}\n</script>\n\n<script id=\"cdataAllowsSimilarPatterns\">\n{\n  const doc = new Document();\n\n  const cdata1 = doc.createCDATASection(']>');\n  testing.expectEqual(']>', cdata1.data);\n\n  const cdata2 = doc.createCDATASection(']]');\n  testing.expectEqual(']]', cdata2.data);\n\n  const cdata3 = doc.createCDATASection('] ]>');\n  testing.expectEqual('] ]>', cdata3.data);\n}\n</script>\n\n<script id=\"cdataCharacterDataMethods\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('Hello');\n\n  cdata.appendData(' World');\n  testing.expectEqual('Hello World', cdata.data);\n  testing.expectEqual(11, cdata.length);\n\n  cdata.deleteData(5, 6);\n  testing.expectEqual('Hello', cdata.data);\n\n  cdata.insertData(0, 'Hi ');\n  testing.expectEqual('Hi Hello', cdata.data);\n\n  cdata.replaceData(0, 3, 'Bye');\n  testing.expectEqual('ByeHello', cdata.data);\n\n  const sub = cdata.substringData(0, 3);\n  testing.expectEqual('Bye', sub);\n}\n</script>\n\n<script id=\"cdataInheritance\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('test');\n\n  testing.expectEqual(true, cdata instanceof CDATASection);\n  testing.expectEqual(true, cdata instanceof Text);\n  testing.expectEqual(true, cdata instanceof CharacterData);\n  testing.expectEqual(true, cdata instanceof Node);\n}\n</script>\n\n<script id=\"cdataWholeText\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('test data');\n\n  testing.expectEqual('test data', cdata.wholeText);\n}\n</script>\n\n<script id=\"cdataClone\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('original data');\n\n  const clone = cdata.cloneNode(false);\n\n  testing.expectEqual(4, clone.nodeType);\n  testing.expectEqual('#cdata-section', clone.nodeName);\n  testing.expectEqual('original data', clone.data);\n  testing.expectEqual(true, clone !== cdata);\n}\n</script>\n\n<script id=\"cdataRemove\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('test');\n\n  const root = doc.createElement('root');\n  doc.appendChild(root);\n  root.appendChild(cdata);\n\n  testing.expectEqual(1, root.childNodes.length);\n  testing.expectEqual(root, cdata.parentNode);\n\n  cdata.remove();\n  testing.expectEqual(0, root.childNodes.length);\n  testing.expectEqual(null, cdata.parentNode);\n}\n</script>\n\n<script id=\"cdataBeforeAfter\">\n{\n  const doc = new Document();\n  const root = doc.createElement('root');\n  doc.appendChild(root);\n\n  const cdata = doc.createCDATASection('middle');\n  root.appendChild(cdata);\n\n  const text1 = doc.createTextNode('before');\n  const text2 = doc.createTextNode('after');\n\n  cdata.before(text1);\n  cdata.after(text2);\n\n  testing.expectEqual(3, root.childNodes.length);\n}\n</script>\n\n<script id=\"cdataReplaceWith\">\n{\n  const doc = new Document();\n  const root = doc.createElement('root');\n  doc.appendChild(root);\n\n  const cdata = doc.createCDATASection('old');\n  root.appendChild(cdata);\n\n  const replacement = doc.createTextNode('new');\n  cdata.replaceWith(replacement);\n\n  testing.expectEqual(1, root.childNodes.length);\n  testing.expectEqual('new', root.childNodes[0].data);\n  testing.expectEqual(null, cdata.parentNode);\n}\n</script>\n\n<script id=\"cdataSiblingNavigation\">\n{\n  const doc = new Document();\n  const root = doc.createElement('root');\n  doc.appendChild(root);\n\n  const elem1 = doc.createElement('first');\n  const cdata = doc.createCDATASection('middle');\n  const elem2 = doc.createElement('last');\n\n  root.appendChild(elem1);\n  root.appendChild(cdata);\n  root.appendChild(elem2);\n\n  testing.expectEqual('last', cdata.nextElementSibling.tagName);\n  testing.expectEqual('first', cdata.previousElementSibling.tagName);\n}\n</script>\n\n<script id=\"cdataEmptyString\">\n{\n  const doc = new Document();\n  const cdata = doc.createCDATASection('');\n\n  testing.expectEqual('', cdata.data);\n  testing.expectEqual(0, cdata.length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/cdata/character_data.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\"></div>\n\n<script id=\"lengthProperty\">\n{\n  // length property\n  const text = document.createTextNode('Hello');\n  testing.expectEqual(5, text.length);\n  testing.expectEqual(5, text.data.length);\n\n  const empty = document.createTextNode('');\n  testing.expectEqual(0, empty.length);\n\n  const comment = document.createComment('test comment');\n  testing.expectEqual(12, comment.length);\n}\n</script>\n\n<script id=\"appendDataBasic\">\n{\n  // appendData basic\n  const text = document.createTextNode('Hello');\n  text.appendData(' World');\n  testing.expectEqual('Hello World', text.data);\n  testing.expectEqual(11, text.length);\n}\n</script>\n\n<script id=\"appendDataEmpty\">\n{\n  // appendData to empty\n  const text = document.createTextNode('');\n  text.appendData('First');\n  testing.expectEqual('First', text.data);\n\n  // appendData empty string\n  text.appendData('');\n  testing.expectEqual('First', text.data);\n}\n</script>\n\n<script id=\"deleteDataBasic\">\n{\n  // deleteData from middle\n  const text = document.createTextNode('Hello World');\n  text.deleteData(5, 6); // Remove ' World'\n  testing.expectEqual('Hello', text.data);\n  testing.expectEqual(5, text.length);\n}\n</script>\n\n<script id=\"deleteDataStart\">\n{\n  // deleteData from start\n  const text = document.createTextNode('Hello World');\n  text.deleteData(0, 6); // Remove 'Hello '\n  testing.expectEqual('World', text.data);\n}\n</script>\n\n<script id=\"deleteDataEnd\">\n{\n  // deleteData from end\n  const text = document.createTextNode('Hello World');\n  text.deleteData(5, 100); // Remove ' World' (count exceeds length)\n  testing.expectEqual('Hello', text.data);\n}\n</script>\n\n<script id=\"deleteDataAll\">\n{\n  // deleteData everything\n  const text = document.createTextNode('Hello');\n  text.deleteData(0, 5);\n  testing.expectEqual('', text.data);\n  testing.expectEqual(0, text.length);\n}\n</script>\n\n<script id=\"deleteDataZeroCount\">\n{\n  // deleteData with count=0\n  const text = document.createTextNode('Hello');\n  text.deleteData(2, 0);\n  testing.expectEqual('Hello', text.data);\n}\n</script>\n\n<script id=\"deleteDataInvalidOffset\">\n{\n  // deleteData with invalid offset\n  const text = document.createTextNode('Hello');\n  testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => text.deleteData(10, 5));\n  testing.expectEqual('Hello', text.data); // unchanged\n}\n</script>\n\n<script id=\"insertDataMiddle\">\n{\n  // insertData in middle\n  const text = document.createTextNode('Hello');\n  text.insertData(5, ' World');\n  testing.expectEqual('Hello World', text.data);\n}\n</script>\n\n<script id=\"insertDataStart\">\n{\n  // insertData at start\n  const text = document.createTextNode('World');\n  text.insertData(0, 'Hello ');\n  testing.expectEqual('Hello World', text.data);\n}\n</script>\n\n<script id=\"insertDataEnd\">\n{\n  // insertData at end\n  const text = document.createTextNode('Hello');\n  text.insertData(5, ' World');\n  testing.expectEqual('Hello World', text.data);\n}\n</script>\n\n<script id=\"insertDataEmpty\">\n{\n  // insertData into empty\n  const text = document.createTextNode('');\n  text.insertData(0, 'Hello');\n  testing.expectEqual('Hello', text.data);\n}\n</script>\n\n<script id=\"insertDataInvalidOffset\">\n{\n  // insertData with invalid offset\n  const text = document.createTextNode('Hello');\n  testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => text.insertData(10, 'X'));\n  testing.expectEqual('Hello', text.data);\n}\n</script>\n\n<script id=\"replaceDataBasic\">\n{\n  // replaceData basic\n  const text = document.createTextNode('Hello World');\n  text.replaceData(6, 5, 'Universe');\n  testing.expectEqual('Hello Universe', text.data);\n}\n</script>\n\n<script id=\"replaceDataShorter\">\n{\n  // replaceData with shorter string\n  const text = document.createTextNode('Hello World');\n  text.replaceData(6, 5, 'Hi');\n  testing.expectEqual('Hello Hi', text.data);\n}\n</script>\n\n<script id=\"replaceDataLonger\">\n{\n  // replaceData with longer string\n  const text = document.createTextNode('Hello Hi');\n  text.replaceData(6, 2, 'World');\n  testing.expectEqual('Hello World', text.data);\n}\n</script>\n\n<script id=\"replaceDataExceedingCount\">\n{\n  // replaceData with count exceeding length\n  const text = document.createTextNode('Hello World');\n  text.replaceData(6, 100, 'Everyone');\n  testing.expectEqual('Hello Everyone', text.data);\n}\n</script>\n\n<script id=\"replaceDataZeroCount\">\n{\n  // replaceData with count=0 (acts like insert)\n  const text = document.createTextNode('Hello World');\n  text.replaceData(5, 0, '!!!');\n  testing.expectEqual('Hello!!! World', text.data);\n}\n</script>\n\n<script id=\"substringDataBasic\">\n{\n  // substringData basic\n  const text = document.createTextNode('Hello World');\n  const sub = text.substringData(0, 5);\n  testing.expectEqual('Hello', sub);\n  testing.expectEqual('Hello World', text.data); // original unchanged\n}\n</script>\n\n<script id=\"substringDataMiddle\">\n{\n  // substringData from middle\n  const text = document.createTextNode('Hello World');\n  const sub = text.substringData(6, 5);\n  testing.expectEqual('World', sub);\n}\n</script>\n\n<script id=\"substringDataExceedingCount\">\n{\n  // substringData with count exceeding length\n  const text = document.createTextNode('Hello World');\n  const sub = text.substringData(6, 100);\n  testing.expectEqual('World', sub);\n}\n</script>\n\n<script id=\"substringDataZeroCount\">\n{\n  // substringData with count=0\n  const text = document.createTextNode('Hello');\n  const sub = text.substringData(0, 0);\n  testing.expectEqual('', sub);\n}\n</script>\n\n<script id=\"substringDataInvalidOffset\">\n{\n  // substringData with invalid offset\n  const text = document.createTextNode('Hello');\n  testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => text.substringData(10, 5));\n}\n</script>\n\n<script id=\"commentCharacterData\">\n{\n  // CharacterData methods work on comments too\n  const comment = document.createComment('Hello');\n\n  comment.appendData(' World');\n  testing.expectEqual('Hello World', comment.data);\n\n  comment.deleteData(5, 6);\n  testing.expectEqual('Hello', comment.data);\n\n  comment.insertData(0, 'Start: ');\n  testing.expectEqual('Start: Hello', comment.data);\n\n  comment.replaceData(0, 7, 'End: ');\n  testing.expectEqual('End: Hello', comment.data);\n\n  const sub = comment.substringData(5, 5);\n  testing.expectEqual('Hello', sub);\n}\n</script>\n\n<script id=\"dataChangeNotifications\">\n{\n  // Verify data changes are reflected in DOM\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text = document.createTextNode('Original');\n  container.appendChild(text);\n\n  text.appendData(' Text');\n  testing.expectEqual('Original Text', container.textContent);\n\n  text.deleteData(0, 9);\n  testing.expectEqual('Text', container.textContent);\n\n  text.data = 'Changed';\n  testing.expectEqual('Changed', container.textContent);\n}\n</script>\n\n<script id=\"removeWithSiblings\">\n{\n  // remove() when node has siblings\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text1 = document.createTextNode('A');\n  const text2 = document.createTextNode('B');\n  const text3 = document.createTextNode('C');\n  container.appendChild(text1);\n  container.appendChild(text2);\n  container.appendChild(text3);\n\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual('ABC', container.textContent);\n\n  text2.remove();\n\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('AC', container.textContent);\n  testing.expectEqual(null, text2.parentNode);\n  testing.expectEqual(text3, text1.nextSibling);\n}\n</script>\n\n<script id=\"removeOnlyChild\">\n{\n  // remove() when node is only child\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text = document.createTextNode('Only');\n  container.appendChild(text);\n\n  testing.expectEqual(1, container.childNodes.length);\n\n  text.remove();\n\n  testing.expectEqual(0, container.childNodes.length);\n  testing.expectEqual(null, text.parentNode);\n}\n</script>\n\n<script id=\"removeNoParent\">\n{\n  // remove() when node has no parent (should do nothing)\n  const text = document.createTextNode('Orphan');\n  text.remove(); // Should not throw\n  testing.expectEqual(null, text.parentNode);\n}\n</script>\n\n<script id=\"removeCommentWithElementSiblings\">\n{\n  // remove() comment with element siblings\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const div1 = document.createElement('div');\n  div1.textContent = 'First';\n  const comment = document.createComment('middle');\n  const div2 = document.createElement('div');\n  div2.textContent = 'Last';\n\n  container.appendChild(div1);\n  container.appendChild(comment);\n  container.appendChild(div2);\n\n  testing.expectEqual(3, container.childNodes.length);\n\n  comment.remove();\n\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('DIV', container.childNodes[0].tagName);\n  testing.expectEqual('DIV', container.childNodes[1].tagName);\n  testing.expectEqual(div2, div1.nextSibling);\n}\n</script>\n\n<script id=\"beforeWithSiblings\">\n{\n  // before() when node has siblings\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text1 = document.createTextNode('A');\n  const text2 = document.createTextNode('C');\n  container.appendChild(text1);\n  container.appendChild(text2);\n\n  const textB = document.createTextNode('B');\n  text2.before(textB);\n\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual('ABC', container.textContent);\n  testing.expectEqual(textB, text1.nextSibling);\n  testing.expectEqual(text2, textB.nextSibling);\n}\n</script>\n\n<script id=\"beforeMultipleNodes\">\n{\n  // before() with multiple nodes\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text = document.createTextNode('Z');\n  container.appendChild(text);\n\n  const text1 = document.createTextNode('A');\n  const text2 = document.createTextNode('B');\n  const text3 = document.createTextNode('C');\n\n  text.before(text1, text2, text3);\n\n  testing.expectEqual(4, container.childNodes.length);\n  testing.expectEqual('ABCZ', container.textContent);\n}\n</script>\n\n<script id=\"beforeMixedTypes\">\n{\n  // before() with mixed node types\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const target = document.createTextNode('Target');\n  container.appendChild(target);\n\n  const elem = document.createElement('span');\n  elem.textContent = 'E';\n  const text = document.createTextNode('T');\n  const comment = document.createComment('C');\n\n  target.before(elem, text, comment);\n\n  testing.expectEqual(4, container.childNodes.length);\n  testing.expectEqual(1, container.childNodes[0].nodeType); // ELEMENT_NODE\n  testing.expectEqual(3, container.childNodes[1].nodeType); // TEXT_NODE\n  testing.expectEqual(8, container.childNodes[2].nodeType); // COMMENT_NODE\n  testing.expectEqual(3, container.childNodes[3].nodeType); // TEXT_NODE (target)\n}\n</script>\n\n<script id=\"beforeNoParent\">\n{\n  // before() when node has no parent (should do nothing)\n  const orphan = document.createTextNode('Orphan');\n  const text = document.createTextNode('Test');\n  orphan.before(text);\n\n  testing.expectEqual(null, orphan.parentNode);\n  testing.expectEqual(null, text.parentNode);\n}\n</script>\n\n<script id=\"beforeOnlyChild\">\n{\n  // before() when target is only child\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const target = document.createTextNode('B');\n  container.appendChild(target);\n\n  const textA = document.createTextNode('A');\n  target.before(textA);\n\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('AB', container.textContent);\n  testing.expectEqual(textA, container.firstChild);\n  testing.expectEqual(target, textA.nextSibling);\n}\n</script>\n\n<script id=\"afterWithSiblings\">\n{\n  // after() when node has siblings\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text1 = document.createTextNode('A');\n  const text2 = document.createTextNode('C');\n  container.appendChild(text1);\n  container.appendChild(text2);\n\n  const textB = document.createTextNode('B');\n  text1.after(textB);\n\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual('ABC', container.textContent);\n  testing.expectEqual(textB, text1.nextSibling);\n  testing.expectEqual(text2, textB.nextSibling);\n}\n</script>\n\n<script id=\"afterMultipleNodes\">\n{\n  // after() with multiple nodes\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text = document.createTextNode('A');\n  container.appendChild(text);\n\n  const text1 = document.createTextNode('B');\n  const text2 = document.createTextNode('C');\n  const text3 = document.createTextNode('D');\n\n  text.after(text1, text2, text3);\n\n  testing.expectEqual(4, container.childNodes.length);\n  testing.expectEqual('ABCD', container.textContent);\n}\n</script>\n\n<script id=\"afterMixedTypes\">\n{\n  // after() with mixed node types\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const target = document.createTextNode('Start');\n  container.appendChild(target);\n\n  const elem = document.createElement('div');\n  elem.textContent = 'E';\n  const comment = document.createComment('comment');\n  const text = document.createTextNode('T');\n\n  target.after(elem, comment, text);\n\n  testing.expectEqual(4, container.childNodes.length);\n  testing.expectEqual(3, container.childNodes[0].nodeType); // TEXT_NODE (target)\n  testing.expectEqual(1, container.childNodes[1].nodeType); // ELEMENT_NODE\n  testing.expectEqual(8, container.childNodes[2].nodeType); // COMMENT_NODE\n  testing.expectEqual(3, container.childNodes[3].nodeType); // TEXT_NODE\n}\n</script>\n\n<script id=\"afterNoParent\">\n{\n  // after() when node has no parent (should do nothing)\n  const orphan = document.createTextNode('Orphan');\n  const text = document.createTextNode('Test');\n  orphan.after(text);\n\n  testing.expectEqual(null, orphan.parentNode);\n  testing.expectEqual(null, text.parentNode);\n}\n</script>\n\n<script id=\"afterAsLastChild\">\n{\n  // after() when target is last child\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const target = document.createTextNode('A');\n  container.appendChild(target);\n\n  const textB = document.createTextNode('B');\n  target.after(textB);\n\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('AB', container.textContent);\n  testing.expectEqual(target, container.firstChild);\n  testing.expectEqual(textB, container.lastChild);\n  testing.expectEqual(null, textB.nextSibling);\n}\n</script>\n\n<script id=\"replaceWithSingleNode\">\n{\n  // replaceWith() with single node\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const old = document.createTextNode('Old');\n  container.appendChild(old);\n\n  const replacement = document.createTextNode('New');\n  old.replaceWith(replacement);\n\n  testing.expectEqual(1, container.childNodes.length);\n  testing.expectEqual('New', container.textContent);\n  testing.expectEqual(null, old.parentNode);\n  testing.expectEqual(container, replacement.parentNode);\n}\n</script>\n\n<script id=\"replaceWithMultipleNodes\">\n{\n  // replaceWith() with multiple nodes\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const old = document.createTextNode('X');\n  container.appendChild(old);\n\n  const text1 = document.createTextNode('A');\n  const text2 = document.createTextNode('B');\n  const text3 = document.createTextNode('C');\n\n  old.replaceWith(text1, text2, text3);\n\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual('ABC', container.textContent);\n  testing.expectEqual(null, old.parentNode);\n}\n</script>\n\n<script id=\"replaceWithOnlyChild\">\n{\n  // replaceWith() when target is only child\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const old = document.createTextNode('Only');\n  container.appendChild(old);\n\n  testing.expectEqual(1, container.childNodes.length);\n\n  const replacement = document.createTextNode('Replaced');\n  old.replaceWith(replacement);\n\n  testing.expectEqual(1, container.childNodes.length);\n  testing.expectEqual('Replaced', container.textContent);\n  testing.expectEqual(replacement, container.firstChild);\n}\n</script>\n\n<script id=\"replaceWithBetweenSiblings\">\n{\n  // replaceWith() when node has siblings on both sides\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text1 = document.createTextNode('A');\n  const text2 = document.createTextNode('X');\n  const text3 = document.createTextNode('C');\n  container.appendChild(text1);\n  container.appendChild(text2);\n  container.appendChild(text3);\n\n  const replacement = document.createTextNode('B');\n  text2.replaceWith(replacement);\n\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual('ABC', container.textContent);\n  testing.expectEqual(replacement, text1.nextSibling);\n  testing.expectEqual(text3, replacement.nextSibling);\n}\n</script>\n\n<script id=\"replaceWithMixedTypes\">\n{\n  // replaceWith() with mixed node types\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const old = document.createComment('old');\n  container.appendChild(old);\n\n  const elem = document.createElement('span');\n  elem.textContent = 'E';\n  const text = document.createTextNode('T');\n  const comment = document.createComment('C');\n\n  old.replaceWith(elem, text, comment);\n\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual(1, container.childNodes[0].nodeType); // ELEMENT_NODE\n  testing.expectEqual(3, container.childNodes[1].nodeType); // TEXT_NODE\n  testing.expectEqual(8, container.childNodes[2].nodeType); // COMMENT_NODE\n  testing.expectEqual(null, old.parentNode);\n}\n</script>\n\n<script id=\"nextElementSiblingText\">\n{\n  // nextElementSibling on text node with element siblings\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text1 = document.createTextNode('A');\n  const comment = document.createComment('comment');\n  const div = document.createElement('div');\n  div.id = 'found';\n  const text2 = document.createTextNode('B');\n\n  container.appendChild(text1);\n  container.appendChild(comment);\n  container.appendChild(div);\n  container.appendChild(text2);\n\n  testing.expectEqual('found', text1.nextElementSibling.id);\n  testing.expectEqual('found', comment.nextElementSibling.id);\n  testing.expectEqual(null, text2.nextElementSibling);\n}\n</script>\n\n<script id=\"nextElementSiblingNoElement\">\n{\n  // nextElementSibling when there's no element sibling\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text = document.createTextNode('A');\n  const comment = document.createComment('B');\n  container.appendChild(text);\n  container.appendChild(comment);\n\n  testing.expectEqual(null, text.nextElementSibling);\n  testing.expectEqual(null, comment.nextElementSibling);\n}\n</script>\n\n<script id=\"previousElementSiblingComment\">\n{\n  // previousElementSibling on comment with element siblings\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const div = document.createElement('div');\n  div.id = 'found';\n  const text = document.createTextNode('text');\n  const comment = document.createComment('comment');\n\n  container.appendChild(div);\n  container.appendChild(text);\n  container.appendChild(comment);\n\n  testing.expectEqual('found', text.previousElementSibling.id);\n  testing.expectEqual('found', comment.previousElementSibling.id);\n  testing.expectEqual(null, div.previousElementSibling);\n}\n</script>\n\n<script id=\"previousElementSiblingNoElement\">\n{\n  // previousElementSibling when there's no element sibling\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const text = document.createTextNode('A');\n  const comment = document.createComment('B');\n  container.appendChild(text);\n  container.appendChild(comment);\n\n  testing.expectEqual(null, text.previousElementSibling);\n  testing.expectEqual(null, comment.previousElementSibling);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/cdata/comment.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=comment>\n  testing.expectEqual('', new Comment().data);\n  testing.expectEqual('over 9000! ', new Comment('over 9000! ').data);\n\n  testing.expectEqual('null', new Comment(null).data);\n</script>\n"
  },
  {
    "path": "src/browser/tests/cdata/data.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=a><!-- spice --></div>\n<div id=b>flow</div>\n\n<script id=data>\n  testing.expectEqual(' spice ', $('#a').firstChild.data);\n  testing.expectEqual('flow', $('#b').firstChild.data);\n</script>\n"
  },
  {
    "path": "src/browser/tests/cdata/text.html",
    "content": "<!DOCTYPE html>\n<a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n\n<script src=\"../testing.js\"></script>\n<script id=text>\n  let t = new Text('foo');\n  testing.expectEqual('foo', t.data);\n\n  let emptyt = new Text();\n  testing.expectEqual('', emptyt.data);\n\n  let text = $('#link').firstChild;\n  testing.expectEqual('OK', text.wholeText);\n\n  text.data = 'OK modified';\n  let split = text.splitText('OK'.length);\n  testing.expectEqual(' modified', split.data);\n  testing.expectEqual('OK', text.data);\n\n    let x = new Text(null);\n    testing.expectEqual(\"null\", x.data);\n</script>\n"
  },
  {
    "path": "src/browser/tests/cdp/dom1.html",
    "content": "<p>1</p> <p>2</p>\n"
  },
  {
    "path": "src/browser/tests/cdp/dom2.html",
    "content": "<div><p>2</p></div>\n"
  },
  {
    "path": "src/browser/tests/cdp/dom3.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Test Page</title>\n</head>\n<body>\n    <h1>Test Page</h1>\n    <nav>\n        <a href=\"/page1\" id=\"link1\">First Link</a>\n        <a href=\"/page2\" id=\"link2\">Second Link</a>\n    </nav>\n    <form id=\"testForm\" action=\"/submit\" method=\"post\">\n        <label for=\"username\">Username:</label>\n        <input type=\"text\" id=\"username\" name=\"username\" placeholder=\"Enter username\">\n\n        <label for=\"email\">Email:</label>\n        <input type=\"email\" id=\"email\" name=\"email\" placeholder=\"Enter email\">\n\n        <label for=\"password\">Password:</label>\n        <input type=\"password\" id=\"password\" name=\"password\">\n\n        <button type=\"submit\">Submit</button>\n    </form>\n</body>\n</html>\n"
  },
  {
    "path": "src/browser/tests/cdp/registry1.html",
    "content": "<a id=a1>link1</a><div id=d2><p>other</p></div>\n"
  },
  {
    "path": "src/browser/tests/cdp/registry2.html",
    "content": "<a id=a1></a><a id=a2></a>\n"
  },
  {
    "path": "src/browser/tests/cdp/registry3.html",
    "content": "<a id=a1></a><div id=d2><a id=a2></a></div>\n"
  },
  {
    "path": "src/browser/tests/collections/radio_node_list.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<!-- Test fixtures for RadioNodeList -->\n<form id=\"test_form\">\n  <input type=\"radio\" name=\"color\" value=\"red\">\n  <input type=\"radio\" name=\"color\" value=\"green\">\n  <input type=\"radio\" name=\"color\" value=\"blue\">\n  <input type=\"text\" name=\"single_field\" value=\"test\">\n</form>\n\n<script id=\"multiple_returns_radio_node_list\">\n{\n  const form = $('#test_form');\n  const result = form.elements.namedItem('color');\n\n  testing.expectEqual('RadioNodeList', result.constructor.name);\n}\n</script>\n\n<script id=\"single_returns_element\">\n{\n  const form = $('#test_form');\n  const result = form.elements.namedItem('single_field');\n\n  testing.expectEqual('HTMLInputElement', result.constructor.name);\n  testing.expectEqual('single_field', result.name);\n}\n</script>\n\n<script id=\"none_returns_null\">\n{\n  const form = $('#test_form');\n  const result = form.elements.namedItem('nonexistent');\n\n  testing.expectEqual(null, result);\n}\n</script>\n\n<script id=\"length\">\n{\n  const form = $('#test_form');\n  const radios = form.elements.namedItem('color');\n\n  testing.expectEqual(3, radios.length);\n}\n</script>\n\n<script id=\"indexed_access\">\n{\n  const form = $('#test_form');\n  const radios = form.elements.namedItem('color');\n\n  testing.expectEqual('red', radios[0].value);\n  testing.expectEqual('green', radios[1].value);\n  testing.expectEqual('blue', radios[2].value);\n}\n</script>\n\n<script id=\"value_getter_no_checked\">\n{\n  const form = $('#test_form');\n  const radios = form.elements.namedItem('color');\n\n  testing.expectEqual('', radios.value);\n}\n</script>\n\n<script id=\"value_getter_with_checked\">\n{\n  const form = $('#test_form');\n  const inputs = form.querySelectorAll('input[name=\"color\"]');\n  inputs[1].checked = true;\n\n  const radios = form.elements.namedItem('color');\n  testing.expectEqual('green', radios.value);\n}\n</script>\n\n<script id=\"value_getter_on_default\">\n{\n  const form = document.createElement('form');\n  const r1 = document.createElement('input');\n  r1.type = 'radio';\n  r1.name = 'test';\n  r1.checked = true;\n  // no value attribute\n\n  const r2 = document.createElement('input');\n  r2.type = 'radio';\n  r2.name = 'test';\n  // no value attribute\n\n  form.appendChild(r1);\n  form.appendChild(r2);\n  document.body.appendChild(form);\n\n  // Multiple elements with same name returns RadioNodeList\n  const radios = form.elements.namedItem('test');\n  testing.expectEqual('RadioNodeList', radios.constructor.name);\n  testing.expectEqual('on', radios.value); // Checked radio with no value returns \"on\"\n\n  form.remove();\n}\n</script>\n\n<script id=\"value_setter\">\n{\n  const form = $('#test_form');\n  const radios = form.elements.namedItem('color');\n  const inputs = form.querySelectorAll('input[name=\"color\"]');\n\n  radios.value = 'blue';\n\n  testing.expectEqual(false, inputs[0].checked);\n  testing.expectEqual(false, inputs[1].checked);\n  testing.expectEqual(true, inputs[2].checked);\n  testing.expectEqual('blue', radios.value);\n}\n</script>\n\n<script id=\"value_setter_on\">\n{\n  const form = document.createElement('form');\n  const r1 = document.createElement('input');\n  r1.type = 'radio';\n  r1.name = 'test';\n  // no value attribute\n\n  const r2 = document.createElement('input');\n  r2.type = 'radio';\n  r2.name = 'test';\n  r2.value = 'on';\n\n  const r3 = document.createElement('input');\n  r3.type = 'radio';\n  r3.name = 'test';\n  r3.value = 'other';\n\n  form.appendChild(r1);\n  form.appendChild(r2);\n  form.appendChild(r3);\n  document.body.appendChild(form);\n\n  const radios = form.elements.namedItem('test');\n  radios.value = 'on';\n\n  // Should check first match (r1 with no value attribute)\n  testing.expectEqual(true, r1.checked);\n  testing.expectEqual(false, r2.checked);\n  testing.expectEqual(false, r3.checked);\n\n  form.remove();\n}\n</script>\n\n<script id=\"live_collection\">\n{\n  const form = document.createElement('form');\n  const r1 = document.createElement('input');\n  r1.type = 'radio';\n  r1.name = 'dynamic';\n  r1.value = 'a';\n\n  const r2 = document.createElement('input');\n  r2.type = 'radio';\n  r2.name = 'dynamic';\n  r2.value = 'b';\n\n  form.appendChild(r1);\n  form.appendChild(r2);\n  document.body.appendChild(form);\n\n  const radios = form.elements.namedItem('dynamic');\n  testing.expectEqual('RadioNodeList', radios.constructor.name);\n  testing.expectEqual(2, radios.length);\n\n  const r3 = document.createElement('input');\n  r3.type = 'radio';\n  r3.name = 'dynamic';\n  r3.value = 'c';\n  form.appendChild(r3);\n\n  testing.expectEqual(3, radios.length);\n  testing.expectEqual('c', radios[2].value);\n\n  r1.remove();\n  testing.expectEqual(2, radios.length);\n  testing.expectEqual('b', radios[0].value);\n\n  form.remove();\n}\n</script>\n\n<!-- Test non-radio elements with same name -->\n<form id=\"mixed_form\">\n  <input type=\"text\" name=\"mixed\" value=\"text1\">\n  <input type=\"text\" name=\"mixed\" value=\"text2\">\n</form>\n\n<script id=\"non_radio_elements\">\n{\n  const form = $('#mixed_form');\n  const result = form.elements.namedItem('mixed');\n\n  // Should still return RadioNodeList even for non-radio elements\n  testing.expectEqual('RadioNodeList', result.constructor.name);\n  testing.expectEqual(2, result.length);\n\n  // getValue should return \"\" for non-radio elements\n  testing.expectEqual('', result.value);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/console/console.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=\"time\">\n    // should not crash\n    console.time();\n    console.timeLog();\n    console.timeEnd();\n\n    console.time(\"test\");\n    console.timeLog(\"test\");\n    console.timeEnd(\"test\");\n\n    testing.expectEqual(true, true);\n</script>\n\n<script id=\"count\">\n    // should not crash\n    console.count();\n    console.count();\n    console.countReset();\n\n    console.count(\"test\");\n    console.count(\"test\");\n    console.countReset(\"test\");\n\n    testing.expectEqual(true, true);\n</script>\n"
  },
  {
    "path": "src/browser/tests/crypto.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=getRandomValues>\n  function isRandom(ta) {\n    let uniq = new Set(Array.from(ta));\n    testing.expectEqual(true, (uniq.size / ta.length) * 100 > 0.7)\n  }\n  {\n    let tu8a = new Uint8Array(100)\n    testing.expectEqual(tu8a, crypto.getRandomValues(tu8a))\n    isRandom(tu8a)\n\n    let ti8a = new Int8Array(100)\n    testing.expectEqual(ti8a, crypto.getRandomValues(ti8a))\n    isRandom(ti8a)\n  }\n\n  {\n    let tu16a = new Uint16Array(100)\n    testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))\n    isRandom(tu16a)\n\n    let ti16a = new Int16Array(100)\n    testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))\n    isRandom(ti16a)\n  }\n\n  {\n    let tu32a = new Uint32Array(100)\n    testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))\n    isRandom(tu32a)\n\n    let ti32a = new Int32Array(100)\n    testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))\n    isRandom(ti32a)\n  }\n\n  {\n    let tu64a = new BigUint64Array(100)\n    testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))\n    isRandom(tu64a)\n\n    let ti64a = new BigInt64Array(100)\n    testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))\n    isRandom(ti64a)\n  }\n</script>\n\n<script id=\"randomUUID\">\n  const uuid = crypto.randomUUID();\n  testing.expectEqual('string', typeof uuid);\n  testing.expectEqual(36, uuid.length);\n  const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n  testing.expectEqual(true, regex.test(uuid));\n</script>\n\n<script id=SubtleCrypto>\n  testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);\n</script>\n\n<script id=sign-and-verify-hmac>\n  testing.async(async () => {\n    let key = await crypto.subtle.generateKey(\n      {\n        name: \"HMAC\",\n        hash: { name: \"SHA-512\" },\n      },\n      true,\n      [\"sign\", \"verify\"],\n    );\n\n    testing.expectEqual(true, key instanceof CryptoKey);\n\n    const raw = await crypto.subtle.exportKey(\"raw\", key);\n    testing.expectEqual(128, raw.byteLength);\n\n    const encoder = new TextEncoder();\n\n    const signature = await crypto.subtle.sign(\n      \"HMAC\",\n      key,\n      encoder.encode(\"Hello, world!\")\n    );\n\n    testing.expectEqual(true, signature instanceof ArrayBuffer);\n\n    const result = await window.crypto.subtle.verify(\n      { name: \"HMAC\" },\n      key,\n      signature,\n      encoder.encode(\"Hello, world!\")\n    );\n\n    testing.expectEqual(true, result);\n  });\n</script>\n\n<script id=derive-shared-key-x25519>\n  testing.async(async () => {\n    const { privateKey, publicKey } = await crypto.subtle.generateKey(\n      { name: \"X25519\" },\n      true,\n      [\"deriveBits\"],\n    );\n\n    testing.expectEqual(true, privateKey instanceof CryptoKey);\n    testing.expectEqual(true, publicKey instanceof CryptoKey);\n\n    const sharedKey = await crypto.subtle.deriveBits(\n      {\n        name: \"X25519\",\n        public: publicKey,\n      },\n      privateKey,\n      128,\n    );\n\n    testing.expectEqual(16, sharedKey.byteLength);\n  });\n</script>\n\n<script id=\"digest\">\n  testing.async(async () => {\n    async function hash(algo, data) {\n      const buffer = await window.crypto.subtle.digest(algo, new TextEncoder().encode(data));\n      return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');\n    }\n    testing.expectEqual(\"a6a1e3375239f215f09a156df29c17c7d1ac6722\", await hash('sha-1', 'over 9000'));\n    testing.expectEqual(\"1bc375bb92459685194dda18a4b835f4e2972ec1bde6d9ab3db53fcc584a6580\", await hash('sha-256', 'over 9000'));\n    testing.expectEqual(\"a4260d64c2eea9fd30c1f895c5e48a26d817e19d3a700b61b3ce665864ff4b8e012bd357d345aa614c5f642dab865ea1\", await hash('sha-384', 'over 9000'));\n    testing.expectEqual(\"6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6\", await hash('sha-512', 'over 9000'));\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/css/font_face.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=\"constructor_basic\">\n{\n    const face = new FontFace(\"TestFont\", \"url(test.woff)\");\n    testing.expectTrue(face instanceof FontFace);\n}\n</script>\n\n<script id=\"constructor_name\">\n{\n    testing.expectEqual('FontFace', FontFace.name);\n}\n</script>\n\n<script id=\"family_property\">\n{\n    const face = new FontFace(\"MyFont\", \"url(font.woff2)\");\n    testing.expectEqual(\"MyFont\", face.family);\n}\n</script>\n\n<script id=\"status_is_loaded\">\n{\n    const face = new FontFace(\"F\", \"url(f.woff)\");\n    testing.expectEqual(\"loaded\", face.status);\n}\n</script>\n\n<script id=\"loaded_is_promise\">\n{\n    const face = new FontFace(\"F\", \"url(f.woff)\");\n    testing.expectTrue(face.loaded instanceof Promise);\n}\n</script>\n\n<script id=\"load_returns_promise\">\n{\n    const face = new FontFace(\"F\", \"url(f.woff)\");\n    testing.expectTrue(face.load() instanceof Promise);\n}\n</script>\n\n<script id=\"default_descriptors\">\n{\n    const face = new FontFace(\"F\", \"url(f.woff)\");\n    testing.expectEqual(\"normal\", face.style);\n    testing.expectEqual(\"normal\", face.weight);\n    testing.expectEqual(\"normal\", face.stretch);\n    testing.expectEqual(\"normal\", face.variant);\n    testing.expectEqual(\"normal\", face.featureSettings);\n    testing.expectEqual(\"auto\", face.display);\n}\n</script>\n\n<script id=\"document_fonts_add\">\n{\n    const face = new FontFace(\"AddedFont\", \"url(added.woff)\");\n    const result = document.fonts.add(face);\n    testing.expectTrue(result === document.fonts);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/css/font_face_set.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=\"document_fonts_exists\">\n{\n    testing.expectTrue(document.fonts !== undefined);\n    testing.expectTrue(document.fonts !== null);\n}\n</script>\n\n<script id=\"document_fonts_same_instance\">\n{\n    // Should return same instance each time\n    const f1 = document.fonts;\n    const f2 = document.fonts;\n    testing.expectTrue(f1 === f2);\n}\n</script>\n\n<script id=\"document_fonts_status\">\n{\n    testing.expectEqual('loaded', document.fonts.status);\n}\n</script>\n\n<script id=\"document_fonts_size\">\n{\n    testing.expectEqual(0, document.fonts.size);\n}\n</script>\n\n<script id=\"document_fonts_ready_is_promise\">\n{\n    const ready = document.fonts.ready;\n    testing.expectTrue(ready instanceof Promise);\n}\n</script>\n\n<script id=\"document_fonts_ready_resolves\">\n{\n    let resolved = false;\n    document.fonts.ready.then(() => { resolved = true; });\n    // Promise resolution is async; just confirm .then() does not throw\n    testing.expectTrue(typeof document.fonts.ready.then === 'function');\n}\n</script>\n\n<script id=\"document_fonts_check\">\n{\n    testing.expectTrue(document.fonts.check('16px sans-serif'));\n}\n</script>\n\n<script id=\"document_fonts_constructor_name\">\n{\n    testing.expectEqual('FontFaceSet', document.fonts.constructor.name);\n}\n</script>\n\n<script id=\"document_fonts_addEventListener\">\n{\n    let loading = false;\n    document.fonts.addEventListener('loading', function() {\n        loading = true;\n    });\n\n    let loadingdone = false;\n    document.fonts.addEventListener('loadingdone', function() {\n        loadingdone = true;\n    });\n\n    document.fonts.load(\"italic bold 16px Roboto\");\n\n    testing.eventually(() => {\n      testing.expectEqual(true, loading);\n      testing.expectEqual(true, loadingdone);\n    });\n    testing.expectEqual(true, true);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/css/media_query_list.html",
    "content": "<!DOCTYPE html>\n<head>\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body>\n</body>\n\n<script id=matchMedia_basic>\n{\n  const mql = window.matchMedia('(min-width: 600px)');\n  testing.expectEqual('object', typeof mql);\n  testing.expectEqual('(min-width: 600px)', mql.media);\n  testing.expectEqual(false, mql.matches);\n}\n</script>\n\n<script id=matchMedia_different_queries>\n{\n  const mql1 = window.matchMedia('(max-width: 1024px)');\n  testing.expectEqual('(max-width: 1024px)', mql1.media);\n  testing.expectEqual(false, mql1.matches);\n\n  const mql2 = window.matchMedia('(prefers-color-scheme: dark)');\n  testing.expectEqual('(prefers-color-scheme: dark)', mql2.media);\n  testing.expectEqual(false, mql2.matches);\n}\n</script>\n\n<script id=matchMedia_event_target>\n{\n  const mql = window.matchMedia('(orientation: portrait)');\n  testing.expectEqual('function', typeof mql.addEventListener);\n  testing.expectEqual('function', typeof mql.removeEventListener);\n  testing.expectEqual('function', typeof mql.dispatchEvent);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/css/stylesheet.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=\"document_styleSheets\">\n{\n    const sheets = document.styleSheets;\n    testing.expectTrue(sheets !== null);\n    testing.expectEqual(0, sheets.length);\n\n    // Should return same instance\n    const sheets2 = document.styleSheets;\n    testing.expectTrue(sheets === sheets2);\n}\n</script>\n\n<script id=\"CSSStyleSheet_basic\">\n{\n    const sheets = document.styleSheets;\n    testing.expectEqual('StyleSheetList', sheets.constructor.name);\n\n    // Test indexed access on empty list\n    testing.expectEqual(undefined, sheets[0]);\n    testing.expectEqual(undefined, sheets[99]);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_basic\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Set single property\n    style.cssText = 'color: red';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual(1, style.length);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_multiple\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Set multiple properties\n    style.cssText = 'color: red; background: blue; margin: 10px';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual('blue', style.getPropertyValue('background'));\n    testing.expectEqual('10px', style.getPropertyValue('margin'));\n    testing.expectEqual(3, style.length);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_important\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Set property with !important\n    style.cssText = 'color: red !important';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual('important', style.getPropertyPriority('color'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_important_multiple\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Mix of important and non-important\n    style.cssText = 'color: red !important; background: blue; margin: 10px !important';\n    testing.expectEqual('important', style.getPropertyPriority('color'));\n    testing.expectEqual('', style.getPropertyPriority('background'));\n    testing.expectEqual('important', style.getPropertyPriority('margin'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_whitespace\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Test whitespace handling\n    style.cssText = '  color  :  red  ;  background  :  blue  ';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual('blue', style.getPropertyValue('background'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_trailing_semicolon\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Trailing semicolon should be handled\n    style.cssText = 'color: red;';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual(1, style.length);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_no_semicolon\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Single property without semicolon\n    style.cssText = 'color: red';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual(1, style.length);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_empty_declarations\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Multiple semicolons should be ignored\n    style.cssText = 'color: red;;; background: blue';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual('blue', style.getPropertyValue('background'));\n    testing.expectEqual(2, style.length);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_replace\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Set initial properties\n    style.cssText = 'color: red; background: blue';\n    testing.expectEqual(2, style.length);\n\n    // Replace with new properties\n    style.cssText = 'margin: 10px';\n    testing.expectEqual('', style.getPropertyValue('color'));\n    testing.expectEqual('', style.getPropertyValue('background'));\n    testing.expectEqual('10px', style.getPropertyValue('margin'));\n    testing.expectEqual(1, style.length);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_clear\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    style.cssText = 'color: red; background: blue';\n    testing.expectEqual(2, style.length);\n\n    // Clear all properties\n    style.cssText = '';\n    testing.expectEqual(0, style.length);\n    testing.expectEqual('', style.getPropertyValue('color'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_colon_in_value\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // URL value with colon\n    style.cssText = 'background: url(http://example.com/image.png)';\n    testing.expectEqual('url(http://example.com/image.png)', style.getPropertyValue('background'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_important_whitespace\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Various whitespace around !important\n    style.cssText = 'color: red!important';\n    testing.expectEqual('important', style.getPropertyPriority('color'));\n\n    style.cssText = 'color: red  !  important';\n    testing.expectEqual('important', style.getPropertyPriority('color'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_case_insensitive_property\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Property names should be normalized to lowercase\n    style.cssText = 'COLOR: red; BACKGROUND: blue';\n    testing.expectEqual('red', style.getPropertyValue('color'));\n    testing.expectEqual('blue', style.getPropertyValue('background'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_setCssText_exclamation_not_important\">\n{\n    const div = document.createElement('div');\n    const style = div.style;\n\n    // Exclamation mark without \"important\" should be kept in value\n    style.cssText = 'content: \"hello!\"';\n    testing.expectEqual('\"hello!\"', style.getPropertyValue('content'));\n    testing.expectEqual('', style.getPropertyPriority('content'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_style_syncs_to_attribute\">\n{\n    // JS style modifications must be reflected in getAttribute.\n    const div = document.createElement('div');\n\n    // Named property assignment (element.style.X = ...)\n    div.style.opacity = '0';\n    testing.expectEqual('opacity: 0;', div.getAttribute('style'));\n\n    // Update existing property\n    div.style.opacity = '1';\n    testing.expectEqual('opacity: 1;', div.getAttribute('style'));\n\n    // Add a second property\n    div.style.color = 'red';\n    testing.expectTrue(div.getAttribute('style').includes('opacity: 1'));\n    testing.expectTrue(div.getAttribute('style').includes('color: red'));\n\n    // removeProperty syncs back\n    div.style.removeProperty('opacity');\n    testing.expectTrue(!div.getAttribute('style').includes('opacity'));\n    testing.expectTrue(div.getAttribute('style').includes('color: red'));\n\n    // setCssText syncs back\n    div.style.cssText = 'filter: blur(0px)';\n    testing.expectEqual('filter: blur(0px);', div.getAttribute('style'));\n\n    // setCssText with empty string clears attribute\n    div.style.cssText = '';\n    testing.expectEqual('', div.getAttribute('style'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_outerHTML_reflects_style_changes\">\n{\n    // outerHTML must reflect JS-modified styles (regression test for\n    // DOM serialization reading stale HTML-parsed attribute values).\n    const div = document.createElement('div');\n    div.setAttribute('style', 'filter:blur(10px);opacity:0');\n\n    div.style.filter = 'blur(0px)';\n    div.style.opacity = '1';\n\n    const html = div.outerHTML;\n    testing.expectTrue(html.includes('filter: blur(0px)'));\n    testing.expectTrue(html.includes('opacity: 1'));\n    testing.expectTrue(!html.includes('blur(10px)'));\n    testing.expectTrue(!html.includes('opacity:0'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_non_ascii_custom_property\">\n{\n    // Regression test: accessing element.style must not crash when the inline\n    // style attribute contains CSS custom properties with non-ASCII (UTF-8\n    // multibyte) names, such as French accented characters.\n    // The CSS Tokenizer's consumeName() must advance over whole UTF-8 sequences\n    // rather than byte-by-byte to avoid landing on a continuation byte.\n    const div = document.createElement('div');\n    div.setAttribute('style',\n        '--color-store-bulles-\\u00e9t\\u00e9-fg: #6a818f;' +\n        '--color-store-soir\\u00e9es-odl-fg: #56b3b3;' +\n        'color: red;'\n    );\n\n    // Must not crash, and ASCII properties that follow non-ASCII ones must be readable.\n    testing.expectEqual('red', div.style.getPropertyValue('color'));\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_normalize_zero_to_0px\">\n{\n    // Per CSSOM spec, unitless zero in length properties should serialize as \"0px\"\n    const div = document.createElement('div');\n\n    div.style.width = '0';\n    testing.expectEqual('0px', div.style.width);\n\n    div.style.margin = '0';\n    testing.expectEqual('0px', div.style.margin);\n\n    div.style.padding = '0';\n    testing.expectEqual('0px', div.style.padding);\n\n    div.style.top = '0';\n    testing.expectEqual('0px', div.style.top);\n\n    // Scroll properties\n    div.style.scrollMarginTop = '0';\n    testing.expectEqual('0px', div.style.scrollMarginTop);\n\n    div.style.scrollPaddingBottom = '0';\n    testing.expectEqual('0px', div.style.scrollPaddingBottom);\n\n    // Multi-column\n    div.style.columnWidth = '0';\n    testing.expectEqual('0px', div.style.columnWidth);\n\n    div.style.columnRuleWidth = '0';\n    testing.expectEqual('0px', div.style.columnRuleWidth);\n\n    // Outline shorthand\n    div.style.outline = '0';\n    testing.expectEqual('0px', div.style.outline);\n\n    // Shapes\n    div.style.shapeMargin = '0';\n    testing.expectEqual('0px', div.style.shapeMargin);\n\n    // Non-length properties should not be affected\n    div.style.opacity = '0';\n    testing.expectEqual('0', div.style.opacity);\n\n    div.style.zIndex = '0';\n    testing.expectEqual('0', div.style.zIndex);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_normalize_first_baseline\">\n{\n    // \"first baseline\" should serialize canonically as \"baseline\"\n    const div = document.createElement('div');\n\n    div.style.alignItems = 'first baseline';\n    testing.expectEqual('baseline', div.style.alignItems);\n\n    div.style.alignContent = 'first baseline';\n    testing.expectEqual('baseline', div.style.alignContent);\n\n    div.style.alignSelf = 'first baseline';\n    testing.expectEqual('baseline', div.style.alignSelf);\n\n    div.style.justifySelf = 'first baseline';\n    testing.expectEqual('baseline', div.style.justifySelf);\n\n    // \"last baseline\" should remain unchanged\n    div.style.alignItems = 'last baseline';\n    testing.expectEqual('last baseline', div.style.alignItems);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_normalize_duplicate_values\">\n{\n    // For 2-value shorthand properties, \"X X\" should collapse to \"X\"\n    const div = document.createElement('div');\n\n    div.style.placeContent = 'center center';\n    testing.expectEqual('center', div.style.placeContent);\n\n    div.style.placeContent = 'start start';\n    testing.expectEqual('start', div.style.placeContent);\n\n    div.style.gap = '10px 10px';\n    testing.expectEqual('10px', div.style.gap);\n\n    // Different values should not collapse\n    div.style.placeContent = 'center start';\n    testing.expectEqual('center start', div.style.placeContent);\n\n    div.style.gap = '10px 20px';\n    testing.expectEqual('10px 20px', div.style.gap);\n\n    // New shorthands\n    div.style.overflow = 'hidden hidden';\n    testing.expectEqual('hidden', div.style.overflow);\n\n    div.style.scrollSnapAlign = 'start start';\n    testing.expectEqual('start', div.style.scrollSnapAlign);\n\n    div.style.overscrollBehavior = 'auto auto';\n    testing.expectEqual('auto', div.style.overscrollBehavior);\n}\n</script>\n\n<script id=\"CSSStyleDeclaration_normalize_anchor_size\">\n{\n    // anchor-size() should serialize with dashed ident (anchor name) before size keyword\n    const div = document.createElement('div');\n\n    // Already canonical order - should stay the same\n    div.style.width = 'anchor-size(--foo width)';\n    testing.expectEqual('anchor-size(--foo width)', div.style.width);\n\n    // Non-canonical order - should be reordered\n    div.style.width = 'anchor-size(width --foo)';\n    testing.expectEqual('anchor-size(--foo width)', div.style.width);\n\n    // With fallback value\n    div.style.width = 'anchor-size(height --bar, 100px)';\n    testing.expectEqual('anchor-size(--bar height, 100px)', div.style.width);\n\n    // Different size keywords\n    div.style.width = 'anchor-size(block --baz)';\n    testing.expectEqual('anchor-size(--baz block)', div.style.width);\n\n    div.style.width = 'anchor-size(inline --qux)';\n    testing.expectEqual('anchor-size(--qux inline)', div.style.width);\n\n    div.style.width = 'anchor-size(self-block --test)';\n    testing.expectEqual('anchor-size(--test self-block)', div.style.width);\n\n    div.style.width = 'anchor-size(self-inline --test)';\n    testing.expectEqual('anchor-size(--test self-inline)', div.style.width);\n\n    // Without anchor name (implicit default anchor)\n    div.style.width = 'anchor-size(width)';\n    testing.expectEqual('anchor-size(width)', div.style.width);\n\n    // Nested anchor-size in fallback\n    div.style.width = 'anchor-size(width --foo, anchor-size(height --bar))';\n    testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/css.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=\"exists\">\n  testing.expectEqual('object', typeof CSS);\n  testing.expectEqual('function', typeof CSS.escape);\n  testing.expectEqual('function', typeof CSS.supports);\n</script>\n\n<script id=\"escape_basic\">\n  {\n    testing.expectEqual('hello', CSS.escape('hello'));\n    testing.expectEqual('world123', CSS.escape('world123'));\n    testing.expectEqual('foo-bar', CSS.escape('foo-bar'));\n    testing.expectEqual('_test', CSS.escape('_test'));\n  }\n</script>\n\n<script id=\"escape_first_character\">\n  {\n    testing.expectEqual('\\\\30 abc', CSS.escape('0abc'));\n    testing.expectEqual('\\\\31 23', CSS.escape('123'));\n    testing.expectEqual('\\\\-', CSS.escape('-'));\n    testing.expectEqual('-test', CSS.escape('-test'));\n    testing.expectEqual('--test', CSS.escape('--test'));\n    testing.expectEqual('-\\\\33 ', CSS.escape('-3'));\n  }\n</script>\n\n<script id=\"escape_special_characters\">\n  {\n    testing.expectEqual('hello\\\\ world', CSS.escape('hello world'));\n    testing.expectEqual('test\\\\!', CSS.escape('test!'));\n    testing.expectEqual('foo\\\\#bar', CSS.escape('foo#bar'));\n    testing.expectEqual('a\\\\(b\\\\)', CSS.escape('a(b)'));\n    testing.expectEqual('test\\\\@example', CSS.escape('test@example'));\n    testing.expectEqual('a\\\\[b\\\\]', CSS.escape('a[b]'));\n    testing.expectEqual('a\\\\{b\\\\}', CSS.escape('a{b}'));\n    testing.expectEqual('test\\\\:value', CSS.escape('test:value'));\n    testing.expectEqual('a\\\\.b', CSS.escape('a.b'));\n    testing.expectEqual('a\\\\,b', CSS.escape('a,b'));\n  }\n</script>\n\n<script id=\"escape_quotes\">\n  {\n    testing.expectEqual('test\\\\\"value', CSS.escape('test\"value'));\n    testing.expectEqual('test\\\\\\'value', CSS.escape(\"test'value\"));\n  }\n</script>\n\n<script id=\"supports_basic\">\n  {\n    testing.expectEqual(true, CSS.supports('display', 'block'));\n    testing.expectEqual(true, CSS.supports('position', 'relative'));\n    testing.expectEqual(true, CSS.supports('width', '100px'));\n    testing.expectEqual(true, CSS.supports('color', 'red'));\n  }\n</script>\n\n<script id=\"supports_common_properties\">\n  {\n    testing.expectEqual(true, CSS.supports('margin', '10px'));\n    testing.expectEqual(true, CSS.supports('padding', '5px'));\n    testing.expectEqual(true, CSS.supports('border', '1px solid black'));\n    testing.expectEqual(true, CSS.supports('background-color', 'blue'));\n    testing.expectEqual(true, CSS.supports('font-size', '16px'));\n    testing.expectEqual(true, CSS.supports('opacity', '0.5'));\n    testing.expectEqual(true, CSS.supports('z-index', '10'));\n  }\n</script>\n\n<script id=\"escape_null_character\">\n  {\n    testing.expectEqual('\\uFFFD', CSS.escape('\\x00'));\n    testing.expectEqual('test\\uFFFDvalue', CSS.escape('test\\x00value'));\n    testing.expectEqual('\\uFFFDabc', CSS.escape('\\x00abc'));\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/attribute_changed.html",
    "content": "<!DOCTYPE html>\n<body>\n<script src=\"../testing.js\"></script>\n<script id=\"attribute_changed\">\n{\n    let callbackCalls = [];\n\n    class MyElement extends HTMLElement {\n        static get observedAttributes() {\n            return ['data-foo', 'data-bar'];\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            callbackCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('my-element', MyElement);\n\n    const el = document.createElement('my-element');\n    testing.expectEqual(0, callbackCalls.length);\n\n    el.setAttribute('data-foo', 'value1');\n    testing.expectEqual(1, callbackCalls.length);\n    testing.expectEqual('data-foo', callbackCalls[0].name);\n    testing.expectEqual(null, callbackCalls[0].oldValue);\n    testing.expectEqual('value1', callbackCalls[0].newValue);\n\n    el.setAttribute('data-foo', 'value2');\n    testing.expectEqual(2, callbackCalls.length);\n    testing.expectEqual('data-foo', callbackCalls[1].name);\n    testing.expectEqual('value1', callbackCalls[1].oldValue);\n    testing.expectEqual('value2', callbackCalls[1].newValue);\n\n    el.setAttribute('data-bar', 'bar-value');\n    testing.expectEqual(3, callbackCalls.length);\n    testing.expectEqual('data-bar', callbackCalls[2].name);\n    testing.expectEqual(null, callbackCalls[2].oldValue);\n    testing.expectEqual('bar-value', callbackCalls[2].newValue);\n}\n\n{\n    let callbackCalls = [];\n\n    class ObservedElement extends HTMLElement {\n        static get observedAttributes() {\n            return ['watched'];\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            callbackCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('observed-element', ObservedElement);\n\n    const el = document.createElement('observed-element');\n    el.setAttribute('unwatched', 'value');\n    testing.expectEqual(0, callbackCalls.length);\n\n    el.setAttribute('watched', 'value');\n    testing.expectEqual(1, callbackCalls.length);\n}\n\n{\n    let callbackCalls = [];\n\n    class RemoveAttrElement extends HTMLElement {\n        static get observedAttributes() {\n            return ['test'];\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            callbackCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('remove-attr-element', RemoveAttrElement);\n\n    const el = document.createElement('remove-attr-element');\n    el.setAttribute('test', 'value');\n    testing.expectEqual(1, callbackCalls.length);\n\n    el.removeAttribute('test');\n    testing.expectEqual(2, callbackCalls.length);\n    testing.expectEqual('test', callbackCalls[1].name);\n    testing.expectEqual('value', callbackCalls[1].oldValue);\n    testing.expectEqual(null, callbackCalls[1].newValue);\n}\n\n{\n    let callbackCalls = [];\n\n    class NoObservedElement extends HTMLElement {\n        attributeChangedCallback(name, oldValue, newValue) {\n            callbackCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('no-observed-element', NoObservedElement);\n\n    const el = document.createElement('no-observed-element');\n    el.setAttribute('test', 'value');\n    testing.expectEqual(0, callbackCalls.length);\n}\n\n{\n    let callbackCalls = [];\n\n    class UpgradeAttrElement extends HTMLElement {\n        static get observedAttributes() {\n            return ['existing'];\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            callbackCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    const el = document.createElement('upgrade-attr-element');\n    el.setAttribute('existing', 'before-upgrade');\n    testing.expectEqual(0, callbackCalls.length);\n\n    customElements.define('upgrade-attr-element', UpgradeAttrElement);\n    testing.expectEqual(0, callbackCalls.length);\n\n    document.body.appendChild(el);\n\n    testing.expectEqual(1, callbackCalls.length);\n    testing.expectEqual('existing', callbackCalls[0].name);\n    testing.expectEqual(null, callbackCalls[0].oldValue);\n    testing.expectEqual('before-upgrade', callbackCalls[0].newValue);\n\n    el.setAttribute('existing', 'after-upgrade');\n    testing.expectEqual(2, callbackCalls.length);\n    testing.expectEqual('existing', callbackCalls[1].name);\n    testing.expectEqual('before-upgrade', callbackCalls[1].oldValue);\n    testing.expectEqual('after-upgrade', callbackCalls[1].newValue);\n}\n\n{\n    let callbackCalls = [];\n\n    class SetAttrMethodElement extends HTMLElement {\n        static get observedAttributes() {\n            return ['test'];\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            callbackCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('set-attr-method-element', SetAttrMethodElement);\n\n    const el = document.createElement('set-attr-method-element');\n    el.setAttribute('test', 'initial');\n    testing.expectEqual(1, callbackCalls.length);\n\n    el.setAttribute('test', 'initial');\n    testing.expectEqual(2, callbackCalls.length);\n    testing.expectEqual('initial', callbackCalls[1].oldValue);\n    testing.expectEqual('initial', callbackCalls[1].newValue);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/built_in.html",
    "content": "<!DOCTYPE html>\n<body>\n<script src=\"../testing.js\"></script>\n<script id=\"built_in\">\n{\n    class FancyButton extends HTMLElement {}\n    customElements.define('fancy-button', FancyButton, { extends: 'button' });\n\n    const definition = customElements.get('fancy-button');\n    testing.expectEqual(FancyButton, definition);\n}\n\n{\n    let threw = false;\n    try {\n        class BadExtends extends HTMLElement {}\n        customElements.define('bad-extends', BadExtends, { extends: 'not-a-real-element' });\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n\n{\n    let threw = false;\n    try {\n        class ExtendCustom extends HTMLElement {}\n        customElements.define('extend-custom', ExtendCustom, { extends: 'fancy-button' });\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n\n{\n    class FancyParagraph extends HTMLElement {}\n    customElements.define('fancy-paragraph', FancyParagraph, { extends: 'p' });\n\n    const definition = customElements.get('fancy-paragraph');\n    testing.expectEqual(FancyParagraph, definition);\n}\n\n{\n    let constructorCalled = 0;\n\n    class ConstructedButton extends HTMLElement {\n        constructor() {\n            super();\n            constructorCalled++;\n            this.clicked = false;\n        }\n    }\n\n    customElements.define('constructed-button', ConstructedButton, { extends: 'button' });\n\n    const btn = document.createElement('button', { is: 'constructed-button' });\n    testing.expectEqual(1, constructorCalled);\n    testing.expectEqual('BUTTON', btn.tagName);\n    testing.expectEqual(false, btn.clicked);\n}\n\n{\n    class WrongTag extends HTMLElement {}\n    customElements.define('wrong-tag', WrongTag, { extends: 'button' });\n\n    const div = document.createElement('div', { is: 'wrong-tag' });\n    testing.expectEqual('DIV', div.tagName);\n}\n\n{\n    let connectedCount = 0;\n    let disconnectedCount = 0;\n    let attributeChanges = [];\n\n    class LifecycleButton extends HTMLElement {\n        static get observedAttributes() {\n            return ['data-test'];\n        }\n\n        connectedCallback() {\n            connectedCount++;\n        }\n\n        disconnectedCallback() {\n            disconnectedCount++;\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            attributeChanges.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('lifecycle-button', LifecycleButton, { extends: 'button' });\n\n    const btn = document.createElement('button', { is: 'lifecycle-button' });\n    testing.expectEqual(0, connectedCount);\n\n    document.body.appendChild(btn);\n    testing.expectEqual(1, connectedCount);\n\n    btn.setAttribute('data-test', 'value1');\n    testing.expectEqual(1, attributeChanges.length);\n    testing.expectEqual('data-test', attributeChanges[0].name);\n    testing.expectEqual(null, attributeChanges[0].oldValue);\n    testing.expectEqual('value1', attributeChanges[0].newValue);\n\n    btn.remove();\n    testing.expectEqual(1, disconnectedCount);\n}\n</script>\n\n<script>\n{\n    let constructorCalled = 0;\n    let connectedCalled = 0;\n\n    class ParsedButton extends HTMLElement {\n        constructor() {\n            super();\n            constructorCalled++;\n        }\n\n        connectedCallback() {\n            connectedCalled++;\n        }\n    }\n\n    customElements.define('parsed-button', ParsedButton, { extends: 'button' });\n}\n</script>\n<button is=\"parsed-button\" id=\"parsed\"></button>\n<script>\n{\n    const btn = document.getElementById('parsed');\n    testing.expectEqual('BUTTON', btn.tagName);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/connected.html",
    "content": "<!DOCTYPE html>\n<body>\n<script src=\"../testing.js\"></script>\n<script id=\"connected\">\n{\n    let connectedCount = 0;\n\n    class MyElement extends HTMLElement {\n        connectedCallback() {\n            connectedCount++;\n        }\n    }\n\n    customElements.define('my-element', MyElement);\n\n    const el = document.createElement('my-element');\n    testing.expectEqual(0, connectedCount);\n\n    document.body.appendChild(el);\n    testing.expectEqual(1, connectedCount);\n}\n\n{\n    let order = [];\n\n    class ParentElement extends HTMLElement {\n        connectedCallback() {\n            order.push('parent');\n        }\n    }\n\n    class ChildElement extends HTMLElement {\n        connectedCallback() {\n            order.push('child');\n        }\n    }\n\n    customElements.define('parent-element', ParentElement);\n    customElements.define('child-element', ChildElement);\n\n    const parent = document.createElement('parent-element');\n    const child = document.createElement('child-element');\n    parent.appendChild(child);\n\n    testing.expectEqual(0, order.length);\n\n    document.body.appendChild(parent);\n    testing.expectEqual(2, order.length);\n    testing.expectEqual('parent', order[0]);\n    testing.expectEqual('child', order[1]);\n}\n\n{\n    let connectedCount = 0;\n\n    class MoveElement extends HTMLElement {\n        connectedCallback() {\n            connectedCount++;\n        }\n    }\n\n    customElements.define('move-element', MoveElement);\n\n    const el = document.createElement('move-element');\n    document.body.appendChild(el);\n    testing.expectEqual(1, connectedCount);\n\n    const div = document.createElement('div');\n    document.body.appendChild(div);\n    div.appendChild(el);\n    testing.expectEqual(1, connectedCount);\n}\n\n{\n    let connectedCount = 0;\n\n    class DetachedElement extends HTMLElement {\n        connectedCallback() {\n            connectedCount++;\n        }\n    }\n\n    customElements.define('detached-element', DetachedElement);\n\n    const container = document.createElement('div');\n    const el = document.createElement('detached-element');\n    container.appendChild(el);\n    testing.expectEqual(0, connectedCount);\n\n    document.body.appendChild(container);\n    testing.expectEqual(1, connectedCount);\n}\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/custom_elements/connected_from_parser.html",
    "content": "<!DOCTYPE html>\n<head>\n<script src=\"../testing.js\"></script>\n<script>\n{\n    // Define the custom element BEFORE the HTML is parsed\n    window.preParseConnectedCount = 0;\n\n    class PreParseElement extends HTMLElement {\n        connectedCallback() {\n            window.preParseConnectedCount++;\n        }\n    }\n\n    customElements.define('pre-parse-element', PreParseElement);\n}\n</script>\n</head>\n<body>\n<!-- This element is in the HTML and should have connectedCallback invoked -->\n<pre-parse-element id=\"static-element\"></pre-parse-element>\n\n<script id=\"test-static-element\">\n{\n    // connectedCallback should have been called for the element in HTML\n    testing.expectEqual(1, window.preParseConnectedCount);\n\n    const el = document.getElementById('static-element');\n    testing.expectTrue(el !== null);\n    testing.expectEqual('PRE-PARSE-ELEMENT', el.tagName);\n}\n</script>\n\n<script id=\"test-programmatic-still-works\">\n{\n    // Reset counter\n    window.preParseConnectedCount = 0;\n\n    // This should still work (programmatic creation)\n    const el = document.createElement('pre-parse-element');\n    testing.expectEqual(0, window.preParseConnectedCount);\n\n    document.body.appendChild(el);\n    testing.expectEqual(1, window.preParseConnectedCount);\n}\n</script>\n\n<script>\n{\n    window.nestedParentCount = 0;\n    window.nestedChildCount = 0;\n\n    class NestedParent extends HTMLElement {\n        connectedCallback() {\n            window.nestedParentCount++;\n        }\n    }\n\n    class NestedChild extends HTMLElement {\n        connectedCallback() {\n            window.nestedChildCount++;\n        }\n    }\n\n    customElements.define('nested-parent', NestedParent);\n    customElements.define('nested-child', NestedChild);\n}\n</script>\n\n<nested-parent id=\"parent-element\">\n    <nested-child id=\"child-element\"></nested-child>\n</nested-parent>\n\n<script id=\"verify-nested\">\n{\n    // Both parent and child should have connectedCallback invoked\n    testing.expectEqual(1, window.nestedParentCount);\n    testing.expectEqual(1, window.nestedChildCount);\n\n    const parent = document.getElementById('parent-element');\n    const child = document.getElementById('child-element');\n    testing.expectTrue(parent !== null);\n    testing.expectTrue(child !== null);\n}\n</script>\n\n<script>\n{\n    // Test attributeChangedCallback for initial attributes during parsing\n    window.attrChangedCalls = [];\n\n    class AttrElement extends HTMLElement {\n        static get observedAttributes() {\n            return ['foo', 'bar'];\n        }\n\n        attributeChangedCallback(name, oldValue, newValue) {\n            window.attrChangedCalls.push({ name, oldValue, newValue });\n        }\n    }\n\n    customElements.define('attr-element', AttrElement);\n}\n</script>\n\n<attr-element foo=\"value1\" bar=\"value2\" ignored=\"value3\"></attr-element>\n\n<script id=\"verify-attribute-changed\">\n{\n    // attributeChangedCallback should have been called for initial attributes\n    testing.expectEqual(2, window.attrChangedCalls.length);\n\n    testing.expectEqual('foo', window.attrChangedCalls[0].name);\n    testing.expectEqual(null, window.attrChangedCalls[0].oldValue);\n    testing.expectEqual('value1', window.attrChangedCalls[0].newValue);\n\n    testing.expectEqual('bar', window.attrChangedCalls[1].name);\n    testing.expectEqual(null, window.attrChangedCalls[1].oldValue);\n    testing.expectEqual('value2', window.attrChangedCalls[1].newValue);\n}\n</script>\n</body>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/constructor.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=\"constructor\">\n{\n    let constructorCalled = false;\n    let constructorThis = null;\n\n    class MyElement extends HTMLElement {\n        constructor() {\n            super();\n            constructorCalled = true;\n            constructorThis = this;\n            this.customProperty = 'initialized';\n        }\n    }\n\n    customElements.define('my-element', MyElement);\n\n    const el = document.createElement('my-element');\n    testing.expectEqual(true, constructorCalled);\n    testing.expectEqual(el, constructorThis);\n    testing.expectEqual('initialized', el.customProperty);\n}\n\n{\n    class CounterElement extends HTMLElement {\n        constructor() {\n            super();\n            this.count = 0;\n        }\n\n        increment() {\n            this.count++;\n        }\n    }\n\n    customElements.define('counter-element', CounterElement);\n\n    const counter = document.createElement('counter-element');\n    testing.expectEqual(0, counter.count);\n    counter.increment();\n    testing.expectEqual(1, counter.count);\n    counter.increment();\n    testing.expectEqual(2, counter.count);\n}\n\n{\n    class NoConstructorElement extends HTMLElement {}\n\n    customElements.define('no-constructor-element', NoConstructorElement);\n\n    const el = document.createElement('no-constructor-element');\n    testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);\n}\n</script>\n\n<div id=clone_container></div>\n\n<script id=clone>\n    {\n        let calls = 0;\n        class MyCloneElementA extends HTMLElement {\n            constructor() {\n                super();\n                calls += 1;\n                $('#clone_container').appendChild(this);\n            }\n        }\n        customElements.define('my-clone_element_a', MyCloneElementA);\n        const original = document.createElement('my-clone_element_a');\n         $('#clone_container').cloneNode(true);\n        testing.expectEqual(2, calls);\n    }\n</script>\n\n<div id=fragment_clone_container></div>\n\n<script id=clone_fragment>\n    {\n        let calls = 0;\n        class MyFragmentCloneElement extends HTMLElement {\n            constructor() {\n                super();\n                calls += 1;\n                $('#fragment_clone_container').appendChild(this);\n            }\n        }\n        customElements.define('my-fragment-clone-element', MyFragmentCloneElement);\n\n        // Create a DocumentFragment with a custom element\n        const fragment = document.createDocumentFragment();\n        const customEl = document.createElement('my-fragment-clone-element');\n        fragment.appendChild(customEl);\n\n        // Clone the fragment - this should trigger the crash\n        // because the constructor will attach the element during cloning\n        const clonedFragment = fragment.cloneNode(true);\n        testing.expectEqual(2, calls);\n    }\n</script>\n\n<div id=range_clone_container></div>\n\n<script id=clone_range>\n    {\n        let calls = 0;\n        class MyRangeCloneElement extends HTMLElement {\n            constructor() {\n                super();\n                calls += 1;\n                $('#range_clone_container').appendChild(this);\n            }\n        }\n        customElements.define('my-range-clone-element', MyRangeCloneElement);\n\n        // Create a container with a custom element\n        const container = document.createElement('div');\n        const customEl = document.createElement('my-range-clone-element');\n        container.appendChild(customEl);\n\n        // Create a range that includes the custom element\n        const range = document.createRange();\n        range.selectNodeContents(container);\n\n        // Clone the range contents - this should trigger the crash\n        // because the constructor will attach the element during cloning\n        const clonedContents = range.cloneContents();\n        testing.expectEqual(2, calls);\n    }\n</script>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/disconnected.html",
    "content": "<!DOCTYPE html>\n<body>\n<script src=\"../testing.js\"></script>\n<script id=\"disconnected\">\n{\n    let disconnectedCount = 0;\n\n    class MyElement extends HTMLElement {\n        disconnectedCallback() {\n            disconnectedCount++;\n        }\n    }\n\n    customElements.define('my-element', MyElement);\n\n    const el = document.createElement('my-element');\n    document.body.appendChild(el);\n    testing.expectEqual(0, disconnectedCount);\n\n    el.remove();\n    testing.expectEqual(1, disconnectedCount);\n}\n\n{\n    let order = [];\n\n    class ParentElement extends HTMLElement {\n        disconnectedCallback() {\n            order.push('parent');\n        }\n    }\n\n    class ChildElement extends HTMLElement {\n        disconnectedCallback() {\n            order.push('child');\n        }\n    }\n\n    customElements.define('parent-element', ParentElement);\n    customElements.define('child-element', ChildElement);\n\n    const parent = document.createElement('parent-element');\n    const child = document.createElement('child-element');\n    parent.appendChild(child);\n    document.body.appendChild(parent);\n\n    testing.expectEqual(0, order.length);\n\n    parent.remove();\n    testing.expectEqual(2, order.length);\n    testing.expectEqual('parent', order[0]);\n    testing.expectEqual('child', order[1]);\n}\n\n{\n    let disconnectedCount = 0;\n\n    class RemoveChildElement extends HTMLElement {\n        disconnectedCallback() {\n            disconnectedCount++;\n        }\n    }\n\n    customElements.define('remove-child-element', RemoveChildElement);\n\n    const el = document.createElement('remove-child-element');\n    document.body.appendChild(el);\n    testing.expectEqual(0, disconnectedCount);\n\n    document.body.removeChild(el);\n    testing.expectEqual(1, disconnectedCount);\n}\n\n{\n    let disconnectedCount = 0;\n\n    class MoveElement extends HTMLElement {\n        disconnectedCallback() {\n            disconnectedCount++;\n        }\n    }\n\n    customElements.define('move-element', MoveElement);\n\n    const el = document.createElement('move-element');\n    document.body.appendChild(el);\n    testing.expectEqual(0, disconnectedCount);\n\n    const div = document.createElement('div');\n    document.body.appendChild(div);\n    div.appendChild(el);\n    testing.expectEqual(0, disconnectedCount);\n}\n\n{\n    let disconnectedCount = 0;\n\n    class ReplaceChildElement extends HTMLElement {\n        disconnectedCallback() {\n            disconnectedCount++;\n        }\n    }\n\n    customElements.define('replace-child-element', ReplaceChildElement);\n\n    const el = document.createElement('replace-child-element');\n    const replacement = document.createElement('div');\n    document.body.appendChild(el);\n    testing.expectEqual(0, disconnectedCount);\n\n    document.body.replaceChild(replacement, el);\n    testing.expectEqual(1, disconnectedCount);\n}\n\n{\n    let connectedCount = 0;\n    let disconnectedCount = 0;\n\n    class LifecycleElement extends HTMLElement {\n        connectedCallback() {\n            connectedCount++;\n        }\n\n        disconnectedCallback() {\n            disconnectedCount++;\n        }\n    }\n\n    customElements.define('lifecycle-element', LifecycleElement);\n\n    const el = document.createElement('lifecycle-element');\n    testing.expectEqual(0, connectedCount);\n    testing.expectEqual(0, disconnectedCount);\n\n    document.body.appendChild(el);\n    testing.expectEqual(1, connectedCount);\n    testing.expectEqual(0, disconnectedCount);\n\n    el.remove();\n    testing.expectEqual(1, connectedCount);\n    testing.expectEqual(1, disconnectedCount);\n\n    document.body.appendChild(el);\n    testing.expectEqual(2, connectedCount);\n    testing.expectEqual(1, disconnectedCount);\n\n    el.remove();\n    testing.expectEqual(2, connectedCount);\n    testing.expectEqual(2, disconnectedCount);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/registry.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=\"registry\">\n{\n    testing.expectEqual(true, window.customElements !== undefined);\n    testing.expectEqual('function', typeof window.customElements.define);\n    testing.expectEqual('function', typeof window.customElements.get);\n}\n\n{\n    class MyElement extends HTMLElement {}\n    customElements.define('my-element', MyElement);\n    const retrieved = customElements.get('my-element');\n    testing.expectEqual(true, retrieved === MyElement);\n}\n\n{\n    const retrieved = customElements.get('not-defined');\n    testing.expectEqual(undefined, retrieved);\n}\n\n{\n    class AnotherElement extends HTMLElement {}\n    customElements.define('another-element', AnotherElement);\n\n    let threw = false;\n    try {\n        customElements.define('another-element', AnotherElement);\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n\n{\n    let threw = false;\n    try {\n        customElements.define('nohyphen', class extends HTMLElement {});\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n\n{\n    let threw = false;\n    try {\n        customElements.define('UPPERCASE-ELEMENT', class extends HTMLElement {});\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n\n{\n    let threw = false;\n    try {\n        customElements.define('annotation-xml', class extends HTMLElement {});\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n\n{\n    class TestElement extends HTMLElement {}\n    customElements.define('test-element', TestElement);\n\n    const el = document.createElement('test-element');\n    testing.expectEqual('TEST-ELEMENT', el.tagName);\n    testing.expectEqual(true, el instanceof HTMLElement);\n}\n\n{\n    const el = document.createElement('undefined-element');\n    testing.expectEqual('UNDEFINED-ELEMENT', el.tagName);\n}\n\n{\n    const el = document.createElement('no-hyphen-invalid');\n    testing.expectEqual('NO-HYPHEN-INVALID', el.tagName);\n}\n</script>\n\n<script id=\"whenDefined_already_defined\">\n{\n    class AlreadyDefined extends HTMLElement {}\n    customElements.define('already-defined', AlreadyDefined);\n\n    const promise = customElements.whenDefined('already-defined');\n    testing.expectEqual('object', typeof promise);\n    testing.expectEqual(true, promise instanceof Promise);\n}\n</script>\n\n<script id=\"whenDefined_not_yet_defined\">\n{\n    const promise = customElements.whenDefined('future-element');\n    testing.expectEqual('object', typeof promise);\n    testing.expectEqual(true, promise instanceof Promise);\n\n    // Now define it\n    class FutureElement extends HTMLElement {}\n    customElements.define('future-element', FutureElement);\n}\n</script>\n\n<script id=\"whenDefined_same_promise\">\n{\n    const promise1 = customElements.whenDefined('pending-element');\n    const promise2 = customElements.whenDefined('pending-element');\n\n    // Should return the same promise for the same name\n    testing.expectEqual(true, promise1 === promise2);\n\n    // Define it so cleanup happens\n    class PendingElement extends HTMLElement {}\n    customElements.define('pending-element', PendingElement);\n}\n</script>\n\n<script id=\"constructor_self_insert_foster_parent\">\n{\n    // Regression: custom element constructor inserting itself (via appendChild) during\n    // innerHTML parsing. When the element is not valid table content, the HTML5 parser\n    // foster-parents it before the <table> via appendBeforeSiblingCallback. That callback\n    // previously didn't check for an existing _parent before calling insertNodeRelative,\n    // causing the \"Page.insertNodeRelative parent\" assertion to fire.\n    let constructorCalled = 0;\n    let container;\n\n    class CtorSelfInsert extends HTMLElement {\n        constructor() {\n            super();\n            constructorCalled++;\n            // Insert self into container so _parent is set before the parser\n            // officially places this element via appendBeforeSiblingCallback.\n            if (container) container.appendChild(this);\n        }\n    }\n    customElements.define('ctor-self-insert', CtorSelfInsert);\n\n    container = document.createElement('div');\n    // ctor-self-insert is not valid table content; the parser foster-parents it\n    // before the <table>, calling appendBeforeSiblingCallback(sibling=table, node=element).\n    // At that point the element already has _parent=container from the constructor.\n    container.innerHTML = '<table><ctor-self-insert></ctor-self-insert></table>';\n\n    testing.expectEqual(1, constructorCalled);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/throw_on_dynamic_markup_insertion.html",
    "content": "<!DOCTYPE html>\n<head>\n<script src=\"../testing.js\"></script>\n<script>\n// Test that document.open/write/close throw InvalidStateError during custom element\n// reactions when the element is parsed from HTML\n\nwindow.constructorOpenException = null;\nwindow.constructorWriteException = null;\nwindow.constructorCloseException = null;\nwindow.constructorCalled = false;\n\nclass ThrowTestElement extends HTMLElement {\n    constructor() {\n        super();\n        window.constructorCalled = true;\n\n        // Try document.open on the same document during constructor - should throw\n        try {\n            document.open();\n        } catch (e) {\n            window.constructorOpenException = e;\n        }\n\n        // Try document.write on the same document during constructor - should throw\n        try {\n            document.write('<b>test</b>');\n        } catch (e) {\n            window.constructorWriteException = e;\n        }\n\n        // Try document.close on the same document during constructor - should throw\n        try {\n            document.close();\n        } catch (e) {\n            window.constructorCloseException = e;\n        }\n    }\n}\n\ncustomElements.define('throw-test-element', ThrowTestElement);\n</script>\n</head>\n<body>\n<!-- This element will be parsed from HTML, triggering the constructor -->\n<throw-test-element id=\"test-element\"></throw-test-element>\n\n<script id=\"verify_throws\">\n{\n    // Verify the constructor was called\n    testing.expectEqual(true, window.constructorCalled);\n\n    // Verify document.open threw InvalidStateError\n    testing.expectEqual(true, window.constructorOpenException !== null);\n    testing.expectEqual('InvalidStateError', window.constructorOpenException.name);\n\n    // Verify document.write threw InvalidStateError\n    testing.expectEqual(true, window.constructorWriteException !== null);\n    testing.expectEqual('InvalidStateError', window.constructorWriteException.name);\n\n    // Verify document.close threw InvalidStateError\n    testing.expectEqual(true, window.constructorCloseException !== null);\n    testing.expectEqual('InvalidStateError', window.constructorCloseException.name);\n}\n</script>\n</body>\n"
  },
  {
    "path": "src/browser/tests/custom_elements/upgrade.html",
    "content": "<!DOCTYPE html>\n<body>\n<my-early id=\"early\"></my-early>\n<script src=\"../testing.js\"></script>\n<script id=\"upgrade\">\n{\n    let constructorCalled = 0;\n    let connectedCalled = 0;\n\n    class MyEarly extends HTMLElement {\n        constructor() {\n            super();\n            constructorCalled++;\n            this.upgraded = true;\n        }\n\n        connectedCallback() {\n            connectedCalled++;\n        }\n    }\n\n    const early = document.getElementById('early');\n    testing.expectEqual(undefined, early.upgraded);\n    testing.expectEqual(0, constructorCalled);\n    testing.expectEqual(0, connectedCalled);\n\n    customElements.define('my-early', MyEarly);\n    testing.expectEqual(true, early.upgraded);\n    testing.expectEqual(1, constructorCalled);\n    // testing.expectEqual(1, connectedCalled);\n}\n\n// {\n//     let order = [];\n\n//     class UpgradeParent extends HTMLElement {\n//         constructor() {\n//             super();\n//             order.push('parent-constructor');\n//         }\n\n//         connectedCallback() {\n//             order.push('parent-connected');\n//         }\n//     }\n\n//     class UpgradeChild extends HTMLElement {\n//         constructor() {\n//             super();\n//             order.push('child-constructor');\n//         }\n\n//         connectedCallback() {\n//             order.push('child-connected');\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';\n//     document.body.appendChild(container);\n\n//     testing.expectEqual(0, order.length);\n\n//     customElements.define('upgrade-parent', UpgradeParent);\n//     testing.expectEqual(2, order.length);\n//     testing.expectEqual('parent-constructor', order[0]);\n//     testing.expectEqual('parent-connected', order[1]);\n\n//     customElements.define('upgrade-child', UpgradeChild);\n//     testing.expectEqual(4, order.length);\n//     testing.expectEqual('child-constructor', order[2]);\n//     testing.expectEqual('child-connected', order[3]);\n// }\n\n// {\n//     let connectedCalled = 0;\n\n//     class DetachedUpgrade extends HTMLElement {\n//         connectedCallback() {\n//             connectedCalled++;\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<detached-upgrade></detached-upgrade>';\n\n//     testing.expectEqual(0, connectedCalled);\n\n//     customElements.define('detached-upgrade', DetachedUpgrade);\n//     testing.expectEqual(0, connectedCalled);\n\n//     document.body.appendChild(container);\n//     testing.expectEqual(1, connectedCalled);\n// }\n\n// {\n//     let constructorCalled = 0;\n//     let connectedCalled = 0;\n\n//     class ManualUpgrade extends HTMLElement {\n//         constructor() {\n//             super();\n//             constructorCalled++;\n//             this.manuallyUpgraded = true;\n//         }\n\n//         connectedCallback() {\n//             connectedCalled++;\n//         }\n//     }\n\n//     customElements.define('manual-upgrade', ManualUpgrade);\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<manual-upgrade id=\"m1\"><manual-upgrade id=\"m2\"></manual-upgrade></manual-upgrade>';\n\n//     testing.expectEqual(2, constructorCalled);\n//     testing.expectEqual(0, connectedCalled);\n\n//     customElements.upgrade(container);\n\n//     testing.expectEqual(2, constructorCalled);\n//     testing.expectEqual(0, connectedCalled);\n\n//     const m1 = container.querySelector('#m1');\n//     const m2 = container.querySelector('#m2');\n//     testing.expectEqual(true, m1.manuallyUpgraded);\n//     testing.expectEqual(true, m2.manuallyUpgraded);\n\n//     document.body.appendChild(container);\n//     testing.expectEqual(2, connectedCalled);\n// }\n\n// {\n//     let alreadyUpgradedCalled = 0;\n\n//     class AlreadyUpgraded extends HTMLElement {\n//         constructor() {\n//             super();\n//             alreadyUpgradedCalled++;\n//         }\n//     }\n\n//     const elem = document.createElement('div');\n//     elem.innerHTML = '<already-upgraded></already-upgraded>';\n//     document.body.appendChild(elem);\n\n//     customElements.define('already-upgraded', AlreadyUpgraded);\n//     testing.expectEqual(1, alreadyUpgradedCalled);\n\n//     customElements.upgrade(elem);\n//     testing.expectEqual(1, alreadyUpgradedCalled);\n// }\n\n// {\n//     let attributeChangedCalls = [];\n\n//     class UpgradeWithAttrs extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['data-foo', 'data-bar'];\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             attributeChangedCalls.push({ name, oldValue, newValue });\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<upgrade-with-attrs data-foo=\"hello\" data-bar=\"world\"></upgrade-with-attrs>';\n//     document.body.appendChild(container);\n\n//     testing.expectEqual(0, attributeChangedCalls.length);\n\n//     customElements.define('upgrade-with-attrs', UpgradeWithAttrs);\n\n//     testing.expectEqual(2, attributeChangedCalls.length);\n//     testing.expectEqual('data-foo', attributeChangedCalls[0].name);\n//     testing.expectEqual(null, attributeChangedCalls[0].oldValue);\n//     testing.expectEqual('hello', attributeChangedCalls[0].newValue);\n//     testing.expectEqual('data-bar', attributeChangedCalls[1].name);\n//     testing.expectEqual(null, attributeChangedCalls[1].oldValue);\n//     testing.expectEqual('world', attributeChangedCalls[1].newValue);\n// }\n\n// {\n//     let attributeChangedCalls = [];\n//     let connectedCalls = 0;\n\n//     class DetachedWithAttrs extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['foo'];\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             attributeChangedCalls.push({ name, oldValue, newValue });\n//         }\n\n//         connectedCallback() {\n//             connectedCalls++;\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<detached-with-attrs foo=\"bar\"></detached-with-attrs>';\n\n//     testing.expectEqual(0, attributeChangedCalls.length);\n\n//     customElements.define('detached-with-attrs', DetachedWithAttrs);\n\n//     testing.expectEqual(0, attributeChangedCalls.length);\n//     testing.expectEqual(0, connectedCalls);\n\n//     document.body.appendChild(container);\n\n//     testing.expectEqual(1, attributeChangedCalls.length);\n//     testing.expectEqual('foo', attributeChangedCalls[0].name);\n//     testing.expectEqual(null, attributeChangedCalls[0].oldValue);\n//     testing.expectEqual('bar', attributeChangedCalls[0].newValue);\n//     testing.expectEqual(1, connectedCalls);\n// }\n\n// {\n//     let attributeChangedCalls = [];\n//     let constructorCalled = 0;\n\n//     class ManualUpgradeWithAttrs extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['x', 'y'];\n//         }\n\n//         constructor() {\n//             super();\n//             constructorCalled++;\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             attributeChangedCalls.push({ name, oldValue, newValue });\n//         }\n//     }\n\n//     customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<manual-upgrade-with-attrs x=\"1\" y=\"2\"></manual-upgrade-with-attrs>';\n\n//     testing.expectEqual(1, constructorCalled);\n//     testing.expectEqual(2, attributeChangedCalls.length);\n\n//     const elem = container.querySelector('manual-upgrade-with-attrs');\n//     elem.setAttribute('z', '3');\n\n//     customElements.upgrade(container);\n\n//     testing.expectEqual(1, constructorCalled);\n//     testing.expectEqual(2, attributeChangedCalls.length);\n// }\n\n// {\n//     let attributeChangedCalls = [];\n\n//     class MixedAttrs extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['watched'];\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             attributeChangedCalls.push({ name, oldValue, newValue });\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<mixed-attrs watched=\"yes\" ignored=\"no\" also-ignored=\"maybe\"></mixed-attrs>';\n//     document.body.appendChild(container);\n\n//     testing.expectEqual(0, attributeChangedCalls.length);\n\n//     customElements.define('mixed-attrs', MixedAttrs);\n\n//     testing.expectEqual(1, attributeChangedCalls.length);\n//     testing.expectEqual('watched', attributeChangedCalls[0].name);\n//     testing.expectEqual('yes', attributeChangedCalls[0].newValue);\n// }\n\n// {\n//     let attributeChangedCalls = [];\n\n//     class EmptyAttr extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['empty', 'non-empty'];\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             attributeChangedCalls.push({ name, oldValue, newValue });\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<empty-attr empty=\"\" non-empty=\"value\"></empty-attr>';\n//     document.body.appendChild(container);\n\n//     customElements.define('empty-attr', EmptyAttr);\n\n//     testing.expectEqual(2, attributeChangedCalls.length);\n//     testing.expectEqual('empty', attributeChangedCalls[0].name);\n//     testing.expectEqual('', attributeChangedCalls[0].newValue);\n//     testing.expectEqual('non-empty', attributeChangedCalls[1].name);\n//     testing.expectEqual('value', attributeChangedCalls[1].newValue);\n// }\n\n// {\n//     let parentCalls = [];\n//     let childCalls = [];\n\n//     class NestedParent extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['parent-attr'];\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             parentCalls.push({ name, oldValue, newValue });\n//         }\n//     }\n\n//     class NestedChild extends HTMLElement {\n//         static get observedAttributes() {\n//             return ['child-attr'];\n//         }\n\n//         attributeChangedCallback(name, oldValue, newValue) {\n//             childCalls.push({ name, oldValue, newValue });\n//         }\n//     }\n\n//     const container = document.createElement('div');\n//     container.innerHTML = '<nested-parent parent-attr=\"p\"><nested-child child-attr=\"c\"></nested-child></nested-parent>';\n//     document.body.appendChild(container);\n\n//     testing.expectEqual(0, parentCalls.length);\n//     testing.expectEqual(0, childCalls.length);\n\n//     customElements.define('nested-parent', NestedParent);\n\n//     testing.expectEqual(1, parentCalls.length);\n//     testing.expectEqual('parent-attr', parentCalls[0].name);\n//     testing.expectEqual('p', parentCalls[0].newValue);\n//     testing.expectEqual(0, childCalls.length);\n\n//     customElements.define('nested-child', NestedChild);\n\n//     testing.expectEqual(1, parentCalls.length);\n//     testing.expectEqual(1, childCalls.length);\n//     testing.expectEqual('child-attr', childCalls[0].name);\n//     testing.expectEqual('c', childCalls[0].newValue);\n// }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/adopt_import.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"test-container\">\n  <p id=\"test-p\" class=\"test-class\" data-foo=\"bar\">\n    <span>Child 1</span>\n    <span>Child 2</span>\n  </p>\n</div>\n\n<script id=\"importNodeShallow\">\n{\n  const original = $('#test-p');\n  const imported = document.importNode(original, false);\n\n  testing.expectEqual(1, imported.nodeType);\n  testing.expectEqual('P', imported.tagName);\n  testing.expectEqual('test-class', imported.className);\n  testing.expectEqual('bar', imported.getAttribute('data-foo'));\n  testing.expectEqual(null, imported.parentNode);\n  testing.expectEqual(false, imported.hasChildNodes());\n  testing.expectEqual(false, original.isSameNode(imported));\n  testing.expectEqual(document, imported.ownerDocument);\n}\n</script>\n\n<script id=\"importNodeDeep\">\n{\n  const original = $('#test-p');\n  const imported = document.importNode(original, true);\n\n  testing.expectEqual(1, imported.nodeType);\n  testing.expectEqual('P', imported.tagName);\n  testing.expectEqual('test-class', imported.className);\n  testing.expectEqual('bar', imported.getAttribute('data-foo'));\n  testing.expectEqual(null, imported.parentNode);\n  testing.expectEqual(true, imported.hasChildNodes());\n\n  testing.expectEqual(false, original.isSameNode(imported));\n  testing.expectEqual(false, original.firstChild.isSameNode(imported.firstChild));\n\n  const spans = imported.querySelectorAll('span');\n  testing.expectEqual(2, spans.length);\n  testing.expectEqual('Child 1', spans[0].textContent);\n  testing.expectEqual('Child 2', spans[1].textContent);\n  testing.expectEqual(document, imported.ownerDocument);\n}\n</script>\n\n<script id=\"importNodeDefault\">\n{\n  const el = document.createElement('div');\n  el.appendChild(document.createElement('span'));\n\n  const imported = document.importNode(el);\n  testing.expectEqual(false, imported.hasChildNodes());\n}\n</script>\n\n<script id=\"importNodeDetached\">\n{\n  const detached = document.createElement('div');\n  detached.textContent = 'detached';\n\n  const imported = document.importNode(detached, true);\n  testing.expectEqual('DIV', imported.tagName);\n  testing.expectEqual('detached', imported.textContent);\n  testing.expectEqual(null, imported.parentNode);\n  testing.expectEqual(false, detached.isSameNode(imported));\n  testing.expectEqual(document, imported.ownerDocument);\n}\n</script>\n\n<script id=\"importNodeTextNode\">\n{\n  const text = document.createTextNode('Hello World');\n  const imported = document.importNode(text, false);\n\n  testing.expectEqual(3, imported.nodeType);\n  testing.expectEqual('Hello World', imported.nodeValue);\n  testing.expectEqual(null, imported.parentNode);\n  testing.expectEqual(false, text.isSameNode(imported));\n  testing.expectEqual(document, imported.ownerDocument);\n}\n</script>\n\n<script id=\"importNodeComment\">\n{\n  const comment = document.createComment('test comment');\n  const imported = document.importNode(comment, false);\n\n  testing.expectEqual(8, imported.nodeType);\n  testing.expectEqual('test comment', imported.nodeValue);\n  testing.expectEqual(null, imported.parentNode);\n  testing.expectEqual(false, comment.isSameNode(imported));\n}\n</script>\n\n<script id=\"importNodeDoesNotModifyOriginal\">\n{\n  const original = $('#test-p');\n  const originalParent = original.parentNode;\n  const imported = document.importNode(original, true);\n\n  testing.expectEqual(originalParent, original.parentNode);\n  testing.expectEqual(true, original.isConnected);\n  testing.expectEqual(null, imported.parentNode);\n  testing.expectEqual(false, imported.isConnected);\n}\n</script>\n\n<script id=\"adoptNodeDetached\">\n{\n  const detached = document.createElement('div');\n  detached.textContent = 'detached';\n\n  const adopted = document.adoptNode(detached);\n  testing.expectEqual(detached, adopted);\n  testing.expectEqual('DIV', adopted.tagName);\n  testing.expectEqual('detached', adopted.textContent);\n  testing.expectEqual(null, adopted.parentNode);\n  testing.expectEqual(document, adopted.ownerDocument);\n}\n</script>\n\n<script id=\"adoptNodeRemovesFromParent\">\n{\n  const container = $('#test-container');\n  const p = $('#test-p');\n\n  testing.expectEqual(container, p.parentNode);\n  testing.expectEqual(true, p.isConnected);\n\n  const adopted = document.adoptNode(p);\n\n  testing.expectEqual(p, adopted);\n  testing.expectEqual(null, adopted.parentNode);\n  testing.expectEqual(false, adopted.isConnected);\n  testing.expectEqual(false, container.contains(p));\n  testing.expectEqual(document, adopted.ownerDocument);\n}\n</script>\n\n<script id=\"adoptNodeTextNode\">\n{\n  const text = document.createTextNode('Hello');\n  const adopted = document.adoptNode(text);\n\n  testing.expectEqual(text, adopted);\n  testing.expectEqual(3, adopted.nodeType);\n  testing.expectEqual('Hello', adopted.nodeValue);\n  testing.expectEqual(null, adopted.parentNode);\n}\n</script>\n\n<script id=\"adoptNodePreservesChildren\">\n{\n  const div = document.createElement('div');\n  const span = document.createElement('span');\n  span.textContent = 'child';\n  div.appendChild(span);\n\n  const adopted = document.adoptNode(div);\n\n  testing.expectEqual(div, adopted);\n  testing.expectEqual(true, adopted.hasChildNodes());\n  testing.expectEqual(span, adopted.firstChild);\n  testing.expectEqual('child', adopted.firstChild.textContent);\n}\n</script>\n\n<script id=\"importThenAppend\">\n{\n  const original = document.createElement('div');\n  original.textContent = 'test';\n\n  const imported = document.importNode(original, true);\n  document.body.appendChild(imported);\n\n  testing.expectEqual(document.body, imported.parentNode);\n  testing.expectEqual(true, imported.isConnected);\n  testing.expectEqual('test', imported.textContent);\n\n  imported.remove();\n}\n</script>\n\n<script id=\"adoptThenAppend\">\n{\n  const detached = document.createElement('div');\n  detached.textContent = 'adopted';\n\n  const adopted = document.adoptNode(detached);\n  document.body.appendChild(adopted);\n\n  testing.expectEqual(document.body, adopted.parentNode);\n  testing.expectEqual(true, adopted.isConnected);\n  testing.expectEqual('adopted', adopted.textContent);\n\n  adopted.remove();\n}\n</script>\n\n<script id=\"importNodeWithAttributes\">\n{\n  const input = document.createElement('input');\n  input.setAttribute('type', 'text');\n  input.setAttribute('name', 'username');\n  input.setAttribute('value', 'test');\n  input.setAttribute('data-custom', 'custom-value');\n\n  const imported = document.importNode(input, false);\n  testing.expectEqual('text', imported.getAttribute('type'));\n  testing.expectEqual('username', imported.getAttribute('name'));\n  testing.expectEqual('test', imported.getAttribute('value'));\n  testing.expectEqual('custom-value', imported.getAttribute('data-custom'));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/all_collection.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>Test Page</title>\n</head>\n<body>\n  <div id=\"first\">First</div>\n  <span name=\"second\">Second</span>\n  <p id=\"third\">Third</p>\n  <a href=\"#\" name=\"link\">Link</a>\n  <script src=\"../testing.js\"></script>\n  <script id=all_collection>\n    testing.expectEqual('undefined', typeof document.all);\n    testing.expectEqual('HTMLAllCollection', document.all.constructor.name);\n    testing.expectEqual(true, document.all == null);\n    testing.expectEqual(true, document.all == undefined);\n    testing.expectEqual(false, document.all === undefined);\n    testing.expectEqual(true, !document.all);\n    testing.expectEqual(false, !!document.all);\n\n    testing.expectEqual(10, document.all.length);\n\n    testing.expectEqual('HTML', document.all[0].tagName);\n    testing.expectEqual('HEAD', document.all[1].tagName);\n    testing.expectEqual('TITLE', document.all[2].tagName);\n    testing.expectEqual('BODY', document.all[3].tagName);\n    testing.expectEqual('DIV', document.all[4].tagName);\n\n    testing.expectEqual('DIV', document.all.first.tagName);\n    testing.expectEqual('First', document.all.first.textContent);\n    testing.expectEqual('P', document.all.third.tagName);\n    testing.expectEqual('Third', document.all.third.textContent);\n\n    testing.expectEqual('SPAN', document.all.second.tagName);\n    testing.expectEqual('Second', document.all.second.textContent);\n    testing.expectEqual('A', document.all.link.tagName);\n\n    testing.expectEqual('SPAN', document.all.item(5).tagName);\n    testing.expectEqual(null, document.all.item(999));\n    testing.expectEqual(null, document.all.item(-1));\n\n    testing.expectEqual('DIV', document.all.namedItem('first').tagName);\n    testing.expectEqual('SPAN', document.all.namedItem('second').tagName);\n    testing.expectEqual(null, document.all.namedItem('nonexistent'));\n\n    // Test callable functionality: document.all(index) and document.all(name)\n    testing.expectEqual('HTML', document.all(0).tagName);\n    testing.expectEqual('HEAD', document.all(1).tagName);\n    testing.expectEqual('DIV', document.all(4).tagName);\n    testing.expectEqual('DIV', document.all('first').tagName);\n    testing.expectEqual('First', document.all('first').textContent);\n    testing.expectEqual('SPAN', document.all('second').tagName);\n    testing.expectEqual('Second', document.all('second').textContent);\n    testing.expectEqual('P', document.all('third').tagName);\n    testing.expectEqual(undefined, document.all(999));\n    testing.expectEqual(undefined, document.all('nonexistent'));\n\n    let count = 0;\n    for (const el of document.all) {\n      count++;\n    }\n    testing.expectEqual(10, count);\n\n    const plainDoc = new Document();\n    testing.expectEqual('undefined', typeof plainDoc.all);\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "src/browser/tests/document/children.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<head>\n  <title>Test</title>\n</head>\n<body>\n  <div id=\"test\">Content</div>\n</body>\n\n<script id=document_children_basic>\n  {\n    const children = document.children;\n    testing.expectEqual('HTMLCollection', children.constructor.name);\n    testing.expectEqual(1, children.length);\n    testing.expectEqual('HTML', children[0].tagName);\n    testing.expectEqual(document.documentElement, children[0]);\n  }\n</script>\n\n<script id=document_children_live>\n  {\n    const children = document.children;\n    const initialLength = children.length;\n\n    testing.expectEqual(1, initialLength);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/collections.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<form id=\"form1\"></form>\n<img id=\"img1\" src=\"about:blank\">\n<script></script>\n<a id=\"link1\" href=\"#\"></a>\n<a id=\"link2\" href=\"/page2\"></a>\n\n<script id=collections>\n  testing.expectEqual(1, document.forms.length);\n  testing.expectEqual($('#form1'), document.forms[0]);\n  testing.expectEqual(1, document.images.length);\n  testing.expectEqual($('#img1'), document.images[0]);\n  testing.expectEqual(3, document.scripts.length);\n\n  testing.expectEqual(true, document.scripts[0].src.endsWith('testing.js'));\n  testing.expectEqual(document.scripts[1].src, '');\n  testing.expectEqual($('#collections'), document.scripts[2]);\n  // document.links only includes <a> elements with href attribute\n  testing.expectEqual(2, document.links.length);\n  testing.expectEqual($('#link1'), document.links[0]);\n  testing.expectEqual($('#link2'), document.links[1]);\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/create_element.html",
    "content": "<!DOCTYPE html>\n<body></body>\n<script src=\"../testing.js\"></script>\n<script id=createElement>\n  testing.expectEqual(1, document.createElement.length);\n\n  const div1 = document.createElement('div');\n  testing.expectEqual(true, div1 instanceof HTMLDivElement);\n  testing.expectEqual(\"DIV\", div1.tagName);\n  div1.id = \"hello\";\n  testing.expectEqual(null, $('#hello'));\n\n  const div2 = document.createElement('DIV');\n  testing.expectEqual(true, div2 instanceof HTMLDivElement);\n\n  document.getElementsByTagName('body')[0].appendChild(div1);\n  testing.expectEqual(div1, $('#hello'));\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/document/create_element_ns.html",
    "content": "<!DOCTYPE html>\n<body></body>\n<script src=\"../testing.js\"></script>\n<script id=createElementNS>\n  const htmlDiv1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');\n  testing.expectEqual('DIV', htmlDiv1.tagName);\n  testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);\n  testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);\n\n  // Per spec, createElementNS does NOT lowercase — 'DIV' != 'div', so this\n  // creates an HTMLUnknownElement, not an HTMLDivElement.\n  const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');\n  testing.expectEqual('DIV', htmlDiv2.tagName);\n  testing.expectEqual(false, htmlDiv2 instanceof HTMLDivElement);\n  testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);\n\n  const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');\n  testing.expectEqual('RecT', svgRect.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', svgRect.namespaceURI);\n\n  const mathElement = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math');\n  testing.expectEqual('math', mathElement.tagName);\n  testing.expectEqual('http://www.w3.org/1998/Math/MathML', mathElement.namespaceURI);\n\n  const xmlElement = document.createElementNS('http://www.w3.org/XML/1998/namespace', 'TEst');\n  testing.expectEqual('TEst', xmlElement.tagName);\n  testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);\n\n  const nullNsElement = document.createElementNS(null, 'span');\n  testing.expectEqual('span', nullNsElement.tagName);\n  testing.expectEqual(null, nullNsElement.namespaceURI);\n\n  const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');\n  testing.expectEqual('custom', unknownNsElement.tagName);\n  // Should be http://example.com/unknown\n  testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);\n\n  const regularDiv = document.createElement('div');\n  testing.expectEqual('DIV', regularDiv.tagName);\n  testing.expectEqual('div', regularDiv.localName);\n  testing.expectEqual(null, regularDiv.prefix);\n  testing.expectEqual('http://www.w3.org/1999/xhtml', regularDiv.namespaceURI);\n\n  const custom = document.createElementNS('test', 'te:ST');\n  testing.expectEqual('te:ST', custom.tagName);\n  testing.expectEqual('te', custom.prefix);\n  testing.expectEqual('ST', custom.localName);\n  testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/document-title.html",
    "content": "<!DOCTYPE html>\n<head id=\"the_head\">\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body id=the_body>\n</body>\n\n<script id=title_initially_empty>\n  testing.expectEqual('', document.title);\n</script>\n\n<script id=title_set_without_existing>\n  document.title = 'New Title';\n  testing.expectEqual('New Title', document.title);\n\n  const titleElement = document.head.querySelector('title');\n  testing.expectEqual(true, titleElement !== null);\n  testing.expectEqual('New Title', titleElement.textContent);\n</script>\n\n<script id=title_update_existing>\n  document.title = 'Updated Title';\n  testing.expectEqual('Updated Title', document.title);\n\n  const titleElements = document.head.querySelectorAll('title');\n  testing.expectEqual(1, titleElements.length);\n  testing.expectEqual('Updated Title', titleElements[0].textContent);\n</script>\n\n<script id=title_set_empty>\n  document.title = '';\n  testing.expectEqual('', document.title);\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/document.html",
    "content": "<!DOCTYPE html>\n<head id=\"the_head\">\n  <meta charset=\"UTF-8\">\n  <title>Test Document Title</title>\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body id=the_body>\n</body>\n\n<script id=document>\n  testing.expectEqual(10, document.childNodes[0].nodeType);\n  testing.expectEqual(null, document.parentNode);\n  testing.expectEqual(undefined, document.getCurrentScript);\n  testing.expectEqual(testing.BASE_URL + 'document/document.html', document.URL);\n  testing.expectEqual(window, document.defaultView);\n  testing.expectEqual(false, document.hidden);\n  testing.expectEqual(\"visible\", document.visibilityState);\n  testing.expectEqual(false, document.prerendering);\n  testing.expectEqual(undefined, Document.prerendering);\n</script>\n\n<script id=headAndbody>\n  testing.expectEqual($('#the_head'), document.head);\n  testing.expectEqual($('#the_body'), document.body);\n</script>\n\n<script id=documentElement>\n  testing.expectEqual($('#the_body').parentNode, document.documentElement);\n  testing.expectEqual(document.documentElement, document.scrollingElement);\n</script>\n\n<script id=title>\n  testing.expectEqual('Test Document Title', document.title);\n  document.title = 'New Title';\n  testing.expectEqual('New Title', document.title);\n  document.title = '';\n  testing.expectEqual('', document.title);\n</script>\n\n<script id=createTextNode>\n  const textNode = document.createTextNode('Hello World');\n  testing.expectEqual(3, textNode.nodeType);\n  testing.expectEqual('Hello World', textNode.nodeValue);\n  testing.expectEqual('Hello World', textNode.textContent);\n\n  const emptyText = document.createTextNode('');\n  testing.expectEqual('', emptyText.nodeValue);\n</script>\n\n<script id=document_metadata>\n  // Test document metadata properties on HTML document\n  testing.expectEqual('text/html', document.contentType);\n  testing.expectEqual('UTF-8', document.characterSet);\n  testing.expectEqual('UTF-8', document.charset);\n  testing.expectEqual('UTF-8', document.inputEncoding);\n  testing.expectEqual('CSS1Compat', document.compatMode);\n  testing.expectEqual(document.URL, document.documentURI);\n  testing.expectEqual('', document.referrer);\n  testing.expectEqual(testing.HOST, document.domain);\n</script>\n\n<script id=programmatic_document_metadata>\n  // Test document metadata properties on programmatically created document\n  const doc = new Document();\n  testing.expectEqual('application/xml', doc.contentType);\n  testing.expectEqual('UTF-8', doc.characterSet);\n  testing.expectEqual('UTF-8', doc.charset);\n  testing.expectEqual('UTF-8', doc.inputEncoding);\n  testing.expectEqual('CSS1Compat', doc.compatMode);\n  testing.expectEqual('', doc.referrer);\n  // Programmatic document should have empty domain (no URL/origin)\n  testing.expectEqual(testing.HOST, doc.domain);\n</script>\n\n<!-- Test anchors and links -->\n<a id=\"link1\" href=\"/page1\">Link 1</a>\n<a id=\"link2\" href=\"/page2\">Link 2</a>\n<a id=\"anchor1\" name=\"section1\">Anchor 1</a>\n<a id=\"anchor2\" name=\"section2\">Anchor 2</a>\n<a id=\"both\" href=\"/page3\" name=\"section3\">Both href and name</a>\n<a id=\"no_attrs\">No attributes</a>\n\n<script id=document_links>\n  {\n    // document.links should only include <a> elements with href attribute\n    const links = document.links;\n    testing.expectEqual('HTMLCollection', links.constructor.name);\n    testing.expectEqual(3, links.length);\n\n    // Should include link1, link2, and both\n    testing.expectEqual($('#link1'), links[0]);\n    testing.expectEqual($('#link2'), links[1]);\n    testing.expectEqual($('#both'), links[2]);\n\n    // Indexed access\n    testing.expectEqual($('#link1'), links.item(0));\n    testing.expectEqual($('#link2'), links.item(1));\n    testing.expectEqual($('#both'), links.item(2));\n    testing.expectEqual(null, links.item(3));\n  }\n</script>\n\n<script id=document_anchors>\n  {\n    // document.anchors should only include <a> elements with name attribute\n    const anchors = document.anchors;\n    testing.expectEqual('HTMLCollection', anchors.constructor.name);\n    testing.expectEqual(3, anchors.length);\n\n    // Should include anchor1, anchor2, and both\n    testing.expectEqual($('#anchor1'), anchors[0]);\n    testing.expectEqual($('#anchor2'), anchors[1]);\n    testing.expectEqual($('#both'), anchors[2]);\n\n    // Indexed access\n    testing.expectEqual($('#anchor1'), anchors.item(0));\n    testing.expectEqual($('#anchor2'), anchors.item(1));\n    testing.expectEqual($('#both'), anchors.item(2));\n    testing.expectEqual(null, anchors.item(3));\n  }\n</script>\n\n<script id=links_live_collection>\n  {\n    // Test that document.links is a live collection\n    const links = document.links;\n    const initialLength = links.length;\n\n    // Add a new link\n    const newLink = document.createElement('a');\n    newLink.href = '/new-page';\n    newLink.textContent = 'New Link';\n    document.body.appendChild(newLink);\n\n    testing.expectEqual(initialLength + 1, links.length);\n    testing.expectEqual(newLink, links[links.length - 1]);\n\n    // Remove href attribute - should no longer be in links\n    newLink.removeAttribute('href');\n    testing.expectEqual(initialLength, links.length);\n\n    // Add it back\n    newLink.href = '/another-page';\n    testing.expectEqual(initialLength + 1, links.length);\n\n    // Remove the element\n    newLink.remove();\n    testing.expectEqual(initialLength, links.length);\n  }\n</script>\n\n<script id=anchors_live_collection>\n  // Test that document.anchors is a live collection\n  const anchors = document.anchors;\n  const initialLength = anchors.length;\n\n  // Add a new anchor\n  const newAnchor = document.createElement('a');\n  newAnchor.name = 'new-section';\n  newAnchor.textContent = 'New Anchor';\n  document.body.appendChild(newAnchor);\n\n  testing.expectEqual(initialLength + 1, anchors.length);\n  testing.expectEqual(newAnchor, anchors[anchors.length - 1]);\n\n  // Remove name attribute - should no longer be in anchors\n  newAnchor.removeAttribute('name');\n  testing.expectEqual(initialLength, anchors.length);\n\n  // Add it back\n  newAnchor.name = 'another-section';\n  testing.expectEqual(initialLength + 1, anchors.length);\n\n  // Remove the element\n  newAnchor.remove();\n  testing.expectEqual(initialLength, anchors.length);\n</script>\n\n<script id=cookie_basic>\n  // Basic cookie operations\n  document.cookie = 'testbasic1=Oeschger';\n  testing.expectEqual(true, document.cookie.includes('testbasic1=Oeschger'));\n\n  document.cookie = 'testbasic2=tripe';\n  testing.expectEqual(true, document.cookie.includes('testbasic1=Oeschger'));\n  testing.expectEqual(true, document.cookie.includes('testbasic2=tripe'));\n\n  // HttpOnly should be ignored from JavaScript\n  const beforeHttp = document.cookie;\n  document.cookie = 'IgnoreMy=Ghost; HttpOnly';\n  testing.expectEqual(false, document.cookie.includes('IgnoreMy=Ghost'));\n\n  // Clean up\n  document.cookie = 'testbasic1=; Max-Age=0';\n  document.cookie = 'testbasic2=; Max-Age=0';\n</script>\n\n<script id=cookie_special_chars>\n  // Test special characters in cookie values\n  document.cookie = 'testspaces=hello world';\n  testing.expectEqual(true, document.cookie.includes('testspaces=hello world'));\n  document.cookie = 'testspaces=; Max-Age=0';\n\n  // Test various allowed special characters\n  document.cookie = 'testspecial=!#$%&\\'()*+-./';\n  testing.expectEqual(true, document.cookie.includes('testspecial='));\n  document.cookie = 'testspecial=; Max-Age=0';\n\n  // Semicolon terminates the cookie value\n  document.cookie = 'testsemi=before;after';\n  testing.expectEqual(true, document.cookie.includes('testsemi=before'));\n  testing.expectEqual(false, document.cookie.includes('after'));\n  document.cookie = 'testsemi=; Max-Age=0';\n</script>\n\n<script id=cookie_empty_name>\n  // Cookie with empty name (just a value)\n  document.cookie = 'teststandalone';\n  testing.expectEqual(true, document.cookie.includes('teststandalone'));\n  document.cookie = 'teststandalone; Max-Age=0';\n</script>\n\n<script id=cookie_whitespace>\n  // Names and values should be trimmed\n  document.cookie = '  testtrim  =  trimmed_value  ';\n  testing.expectEqual(true, document.cookie.includes('testtrim=trimmed_value'));\n  document.cookie = 'testtrim=; Max-Age=0';\n</script>\n\n<script id=cookie_max_age>\n  // Max-Age=0 should immediately delete\n  document.cookie = 'testtemp0=value; Max-Age=0';\n  testing.expectEqual(false, document.cookie.includes('testtemp0=value'));\n\n  // Negative Max-Age should also delete\n  document.cookie = 'testinstant=value';\n  testing.expectEqual(true, document.cookie.includes('testinstant=value'));\n  document.cookie = 'testinstant=value; Max-Age=-1';\n  testing.expectEqual(false, document.cookie.includes('testinstant=value'));\n\n  // Positive Max-Age should keep cookie\n  document.cookie = 'testkept=value; Max-Age=3600';\n  testing.expectEqual(true, document.cookie.includes('testkept=value'));\n  document.cookie = 'testkept=; Max-Age=0';\n</script>\n\n<script id=cookie_overwrite>\n  // Setting a cookie with the same name should overwrite\n  document.cookie = 'testoverwrite=first';\n  testing.expectEqual(true, document.cookie.includes('testoverwrite=first'));\n\n  document.cookie = 'testoverwrite=second';\n  testing.expectEqual(true, document.cookie.includes('testoverwrite=second'));\n  testing.expectEqual(false, document.cookie.includes('testoverwrite=first'));\n\n  document.cookie = 'testoverwrite=; Max-Age=0';\n</script>\n\n<script id=cookie_path>\n  // Path attribute\n  document.cookie = 'testpath1=value; Path=/';\n  testing.expectEqual(true, document.cookie.includes('testpath1=value'));\n\n  // Different path cookie should coexist\n  document.cookie = 'testpath2=value2; Path=/src';\n  testing.expectEqual(true, document.cookie.includes('testpath1=value'));\n\n  document.cookie = 'testpath1=; Max-Age=0; Path=/';\n  document.cookie = 'testpath2=; Max-Age=0; Path=/src';\n</script>\n\n<script id=cookie_invalid_chars>\n  // Control characters (< 32 or > 126) should be rejected\n  const beforeBad = document.cookie;\n\n  document.cookie = 'testbad1\\x00=value';\n  testing.expectEqual(false, document.cookie.includes('testbad1'));\n\n  document.cookie = 'testbad2\\x1F=value';\n  testing.expectEqual(false, document.cookie.includes('testbad2'));\n\n  document.cookie = 'testbad3=val\\x7F';\n  testing.expectEqual(false, document.cookie.includes('testbad3'));\n</script>\n\n<script id=createAttribute>\n  {\n    var attr = document.createAttribute('hello');\n    testing.expectEqual('hello', attr.name);\n    testing.expectEqual('', attr.value);\n\n    testing.withError((err) => {\n      testing.expectEqual(5, err.code);\n      testing.expectEqual(\"InvalidCharacterError\", err.name);\n    }, () => document.createAttribute(''));\n\n    testing.withError((err) => {\n      testing.expectEqual(5, err.code);\n      testing.expectEqual(\"InvalidCharacterError\", err.name);\n    }, () => document.createAttribute('.over'));\n  }\n</script>\n\n<script id=append>\n{\n  const doc = new Document();\n  const html = doc.createElement('html');\n  const body = doc.createElement('body');\n  const div1 = doc.createElement('div');\n  const div2 = doc.createElement('div');\n\n  doc.append(html);\n  testing.expectEqual(1, doc.childNodes.length);\n  testing.expectEqual(html, doc.childNodes[0]);\n\n  html.append(body);\n  testing.expectEqual(1, html.childNodes.length);\n  testing.expectEqual(body, html.childNodes[0]);\n\n  body.append(div1, div2);\n  testing.expectEqual(2, body.childNodes.length);\n  testing.expectEqual(div1, body.childNodes[0]);\n  testing.expectEqual(div2, body.childNodes[1]);\n\n  body.append('text node');\n  testing.expectEqual(3, body.childNodes.length);\n  testing.expectEqual(3, body.childNodes[2].nodeType);\n  testing.expectEqual('text node', body.childNodes[2].textContent);\n}\n</script>\n\n<script id=prepend>\n{\n  const doc = new Document();\n  const html = doc.createElement('html');\n  const body = doc.createElement('body');\n  const div1 = doc.createElement('div');\n  const div2 = doc.createElement('div');\n  const div3 = doc.createElement('div');\n\n  doc.prepend(html);\n  testing.expectEqual(1, doc.childNodes.length);\n  testing.expectEqual(html, doc.childNodes[0]);\n\n  html.prepend(body);\n  testing.expectEqual(1, html.childNodes.length);\n  testing.expectEqual(body, html.childNodes[0]);\n\n  body.append(div1);\n  body.prepend(div2, div3);\n  testing.expectEqual(3, body.childNodes.length);\n  testing.expectEqual(div2, body.childNodes[0]);\n  testing.expectEqual(div3, body.childNodes[1]);\n  testing.expectEqual(div1, body.childNodes[2]);\n\n  body.prepend('text node');\n  testing.expectEqual(4, body.childNodes.length);\n  testing.expectEqual(3, body.childNodes[0].nodeType);\n  testing.expectEqual('text node', body.childNodes[0].textContent);\n}\n</script>\n\n<script id=children>\n  testing.expectEqual(1, document.children.length);\n  testing.expectEqual('HTML', document.children.item(0).nodeName)\n  testing.expectEqual('HTML', document.firstElementChild.nodeName);\n  testing.expectEqual('HTML', document.lastElementChild.nodeName);\n  testing.expectEqual(1, document.childElementCount);\n\n  let nd = new Document();\n  testing.expectEqual(0, nd.children.length);\n  testing.expectEqual(null, nd.children.item(0));\n  testing.expectEqual(null, nd.firstElementChild);\n  testing.expectEqual(null, nd.lastElementChild);\n  testing.expectEqual(0, nd.childElementCount);\n</script>\n\n<script id=adoptedStyleSheets>\n  {\n    const acss = document.adoptedStyleSheets;\n    testing.expectEqual(0, acss.length);\n    acss.push(new CSSStyleSheet());\n    testing.expectEqual(1, acss.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/element_from_point.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<body>\n  <div id=\"div1\" style=\"width: 100px; height: 50px;\">Div 1</div>\n  <div id=\"div2\" style=\"width: 100px; height: 50px;\">Div 2</div>\n  <div id=\"hidden\" style=\"display: none; width: 100px; height: 50px;\">Hidden</div>\n  <div id=\"parent\" style=\"width: 100px; height: 50px;\">\n    <div id=\"child\" style=\"width: 80px; height: 30px;\">Child</div>\n  </div>\n</body>\n\n<script id=\"basic_usage\">\n{\n  // Test finding an element at a specific point\n  const div1 = document.getElementById('div1');\n  const rect1 = div1.getBoundingClientRect();\n\n  // Query near the top of div1 to avoid overlap with later elements\n  const x = rect1.left + 10;\n  const y = rect1.top + 5;\n  const element = document.elementFromPoint(x, y);\n\n  // Should return div1 or a parent (body/html) - not null\n  testing.expectTrue(element !== null);\n  // If it returns div1 specifically, that's ideal\n  // But we also accept parent elements\n  testing.expectTrue(element === div1 || element.tagName === 'BODY' || element.tagName === 'HTML');\n}\n</script>\n\n<script id=\"nested_elements\">\n{\n  // Test that nested elements are found (topmost in document order)\n  const child = document.getElementById('child');\n  const parent = document.getElementById('parent');\n  const rect = child.getBoundingClientRect();\n\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const element = document.elementFromPoint(centerX, centerY);\n\n  // Should return the child element (topmost in document order) or parent\n  testing.expectTrue(element !== null);\n  testing.expectTrue(element === child || element === parent ||\n                     element.tagName === 'BODY' || element.tagName === 'HTML');\n}\n</script>\n<!-- ZIGDOM new CSS Parser -->\n<!-- <script id=\"hidden_elements\">\n{\n  // Test that hidden elements are not returned\n  const hidden = document.getElementById('hidden');\n  console.warn('pre');\n  console.warn(hidden.checkVisibility());\n  const rect = hidden.getBoundingClientRect();\n\n  // Even though hidden element has dimensions, it shouldn't be returned\n  // because it has display: none\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const element = document.elementFromPoint(centerX, centerY);\n\n  // Should not return the hidden element (should return body or html instead)\n  testing.expectTrue(element === null || element.id !== 'hidden');\n}\n</script> -->\n\n<script id=\"outside_viewport\">\n{\n  // Test points outside all elements\n  const element = document.elementFromPoint(-1000, -1000);\n  testing.expectEqual(null, element);\n}\n</script>\n\n<script id=\"at_element_center\">\n{\n  // Test querying at the center of an element\n  const div1 = document.getElementById('div1');\n  const rect = div1.getBoundingClientRect();\n\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const element = document.elementFromPoint(centerX, centerY);\n\n  // Should return div1 or one of its ancestors (body/html)\n  testing.expectTrue(element !== null);\n}\n</script>\n\n<script id=\"dynamically_created\">\n{\n  // Test with dynamically created elements\n  const newDiv = document.createElement('div');\n  newDiv.style.width = '50px';\n  newDiv.style.height = '50px';\n  newDiv.textContent = 'Dynamic';\n  document.body.appendChild(newDiv);\n\n  const rect = newDiv.getBoundingClientRect();\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const element = document.elementFromPoint(centerX, centerY);\n\n  testing.expectEqual(newDiv, element);\n\n  // Clean up\n  newDiv.remove();\n}\n</script>\n\n<script id=\"elementsFromPoint_basic\">\n{\n  // Test that elementsFromPoint returns array of element and its ancestors\n  const div1 = document.getElementById('div1');\n  const rect = div1.getBoundingClientRect();\n\n  const x = rect.left + 10;\n  const y = rect.top + 5;\n  const elements = document.elementsFromPoint(x, y);\n\n  // Should return an array\n  testing.expectTrue(Array.isArray(elements));\n  testing.expectTrue(elements.length > 0);\n\n  // First element should be div1 or one of its ancestors\n  testing.expectTrue(elements[0] !== null);\n\n  // All elements should be ancestors of each other (parent chain)\n  for (let i = 1; i < elements.length; i++) {\n    let found = false;\n    let parent = elements[i - 1].parentElement;\n    while (parent) {\n      if (parent === elements[i]) {\n        found = true;\n        break;\n      }\n      parent = parent.parentElement;\n    }\n    testing.expectTrue(found || elements[i].tagName === 'HTML');\n  }\n}\n</script>\n\n<script id=\"elementsFromPoint_nested\">\n{\n  // Test with nested elements - should return child, parent, body, html in order\n  const child = document.getElementById('child');\n  const parent = document.getElementById('parent');\n  const rect = child.getBoundingClientRect();\n\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const elements = document.elementsFromPoint(centerX, centerY);\n\n  testing.expectTrue(Array.isArray(elements));\n  testing.expectTrue(elements.length >= 2); // At least child and parent (or more ancestors)\n\n  // First element should be the deepest (child or parent)\n  testing.expectTrue(elements[0] === child || elements[0] === parent ||\n                     elements[0].tagName === 'BODY' || elements[0].tagName === 'HTML');\n\n  // If we got child as first element, parent should be in the array\n  if (elements[0] === child) {\n    let foundParent = false;\n    for (let el of elements) {\n      if (el === parent) {\n        foundParent = true;\n        break;\n      }\n    }\n    testing.expectTrue(foundParent);\n  }\n}\n</script>\n\n<script id=\"elementsFromPoint_order\">\n{\n  // Test that elements are returned in order from topmost to deepest\n  const child = document.getElementById('child');\n  const parent = document.getElementById('parent');\n  const rect = child.getBoundingClientRect();\n\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const elements = document.elementsFromPoint(centerX, centerY);\n\n  // Elements should be ordered such that each element is a parent of the previous\n  for (let i = 1; i < elements.length; i++) {\n    const current = elements[i - 1];\n    const next = elements[i];\n\n    // next should be an ancestor of current\n    let isAncestor = false;\n    let p = current.parentElement;\n    while (p) {\n      if (p === next) {\n        isAncestor = true;\n        break;\n      }\n      p = p.parentElement;\n    }\n\n    testing.expectTrue(isAncestor || next.tagName === 'HTML');\n  }\n}\n</script>\n\n<script id=\"elementsFromPoint_outside\">\n{\n  // Test with point outside all elements\n  const elements = document.elementsFromPoint(-1000, -1000);\n\n  testing.expectTrue(Array.isArray(elements));\n  testing.expectEqual(0, elements.length);\n}\n</script>\n\n<!-- ZIGDOM new CSS Parser -->\n<!-- <script id=\"elementsFromPoint_hidden\">\n{\n  // Test that hidden elements are not included\n  const hidden = document.getElementById('hidden');\n  const rect = hidden.getBoundingClientRect();\n\n  const centerX = (rect.left + rect.right) / 2;\n  const centerY = (rect.top + rect.bottom) / 2;\n  const elements = document.elementsFromPoint(centerX, centerY);\n\n  // Should not include the hidden element\n  for (let el of elements) {\n    testing.expectTrue(el.id !== 'hidden');\n  }\n}\n</script>\n -->\n"
  },
  {
    "path": "src/browser/tests/document/focus.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body>\n  <input id=\"input1\" type=\"text\">\n  <input id=\"input2\" type=\"text\">\n  <button id=\"btn1\">Button</button>\n</body>\n\n<script id=\"activeElement_default\">\n{\n  const body = document.querySelector('body');\n  body.focus();\n  testing.expectEqual('BODY', document.activeElement.tagName);\n}\n</script>\n\n<script id=\"focus_method\">\n{\n  const input1 = $('#input1');\n  input1.focus();\n  testing.expectEqual(input1, document.activeElement);\n}\n</script>\n\n<script id=\"focus_events\">\n{\n  const input1 = $('#input1');\n  const input2 = $('#input2');\n\n  // Explicitly blur any focused element\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let focusCount = 0;\n  let blurCount = 0;\n\n  input1.addEventListener('focus', () => focusCount++);\n  input1.addEventListener('blur', () => blurCount++);\n  input2.addEventListener('focus', () => focusCount++);\n\n  input1.focus();\n  testing.expectEqual(1, focusCount);\n  testing.expectEqual(0, blurCount);\n\n  input2.focus();\n  testing.expectEqual(2, focusCount);\n  testing.expectEqual(1, blurCount);\n}\n</script>\n\n<script id=\"blur_method\">\n{\n  const btn = $('#btn1');\n  btn.focus();\n  testing.expectEqual(btn, document.activeElement);\n\n  btn.blur();\n  testing.expectEqual('BODY', document.activeElement.tagName);\n}\n</script>\n\n<script id=\"focus_already_focused\">\n{\n  const input1 = $('#input1');\n\n  // Explicitly blur any focused element\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let focusCount = 0;\n  input1.addEventListener('focus', () => focusCount++);\n\n  input1.focus();\n  testing.expectEqual(1, focusCount);\n\n  input1.focus();\n  testing.expectEqual(1, focusCount);\n}\n</script>\n\n\n<script id=\"focusin_focusout_events\">\n{\n  const input1 = $('#input1');\n  const input2 = $('#input2');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let events = [];\n\n  input1.addEventListener('focus', () => events.push('focus1'));\n  input1.addEventListener('focusin', () => events.push('focusin1'));\n  input1.addEventListener('blur', () => events.push('blur1'));\n  input1.addEventListener('focusout', () => events.push('focusout1'));\n  input2.addEventListener('focus', () => events.push('focus2'));\n  input2.addEventListener('focusin', () => events.push('focusin2'));\n\n  // Focus input1 — should fire focus then focusin\n  input1.focus();\n  testing.expectEqual('focus1,focusin1', events.join(','));\n\n  // Focus input2 — should fire blur, focusout on input1, then focus, focusin on input2\n  events = [];\n  input2.focus();\n  testing.expectEqual('blur1,focusout1,focus2,focusin2', events.join(','));\n}\n</script>\n\n<script id=\"focusin_bubbles\">\n{\n  const input1 = $('#input1');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let bodyFocusin = 0;\n  let bodyFocus = 0;\n\n  document.body.addEventListener('focusin', () => bodyFocusin++);\n  document.body.addEventListener('focus', () => bodyFocus++);\n\n  input1.focus();\n\n  // focusin should bubble to body, focus should not\n  testing.expectEqual(1, bodyFocusin);\n  testing.expectEqual(0, bodyFocus);\n}\n</script>\n\n<script id=\"focusout_bubbles\">\n{\n  const input1 = $('#input1');\n\n  input1.focus();\n\n  let bodyFocusout = 0;\n  let bodyBlur = 0;\n\n  document.body.addEventListener('focusout', () => bodyFocusout++);\n  document.body.addEventListener('blur', () => bodyBlur++);\n\n  input1.blur();\n\n  // focusout should bubble to body, blur should not\n  testing.expectEqual(1, bodyFocusout);\n  testing.expectEqual(0, bodyBlur);\n}\n</script>\n\n<script id=\"focus_relatedTarget\">\n{\n  const input1 = $('#input1');\n  const input2 = $('#input2');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let focusRelated = null;\n  let blurRelated = null;\n  let focusinRelated = null;\n  let focusoutRelated = null;\n\n  input1.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });\n  input1.addEventListener('focusout', (e) => { focusoutRelated = e.relatedTarget; });\n  input2.addEventListener('focus', (e) => { focusRelated = e.relatedTarget; });\n  input2.addEventListener('focusin', (e) => { focusinRelated = e.relatedTarget; });\n\n  input1.focus();\n  input2.focus();\n\n  // blur/focusout on input1 should have relatedTarget = input2 (gaining focus)\n  testing.expectEqual(input2, blurRelated);\n  testing.expectEqual(input2, focusoutRelated);\n\n  // focus/focusin on input2 should have relatedTarget = input1 (losing focus)\n  testing.expectEqual(input1, focusRelated);\n  testing.expectEqual(input1, focusinRelated);\n}\n</script>\n\n<script id=\"blur_relatedTarget_null\">\n{\n  const btn = $('#btn1');\n\n  btn.focus();\n\n  let blurRelated = 'not_set';\n  btn.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });\n  btn.blur();\n\n  // blur without moving to another element should have relatedTarget = null\n  testing.expectEqual(null, blurRelated);\n}\n</script>\n\n<script id=\"focus_event_properties\">\n{\n  const input1 = $('#input1');\n  const input2 = $('#input2');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let focusEvent = null;\n  let focusinEvent = null;\n  let blurEvent = null;\n  let focusoutEvent = null;\n\n  input1.addEventListener('blur', (e) => { blurEvent = e; });\n  input1.addEventListener('focusout', (e) => { focusoutEvent = e; });\n  input2.addEventListener('focus', (e) => { focusEvent = e; });\n  input2.addEventListener('focusin', (e) => { focusinEvent = e; });\n\n  input1.focus();\n  input2.focus();\n\n  // All four should be FocusEvent instances\n  testing.expectEqual(true, blurEvent instanceof FocusEvent);\n  testing.expectEqual(true, focusoutEvent instanceof FocusEvent);\n  testing.expectEqual(true, focusEvent instanceof FocusEvent);\n  testing.expectEqual(true, focusinEvent instanceof FocusEvent);\n\n  // All four should be composed per spec\n  testing.expectEqual(true, blurEvent.composed);\n  testing.expectEqual(true, focusoutEvent.composed);\n  testing.expectEqual(true, focusEvent.composed);\n  testing.expectEqual(true, focusinEvent.composed);\n\n  // None should be cancelable\n  testing.expectEqual(false, blurEvent.cancelable);\n  testing.expectEqual(false, focusoutEvent.cancelable);\n  testing.expectEqual(false, focusEvent.cancelable);\n  testing.expectEqual(false, focusinEvent.cancelable);\n\n  // blur/focus don't bubble, focusin/focusout do\n  testing.expectEqual(false, blurEvent.bubbles);\n  testing.expectEqual(true, focusoutEvent.bubbles);\n  testing.expectEqual(false, focusEvent.bubbles);\n  testing.expectEqual(true, focusinEvent.bubbles);\n}\n</script>\n\n<script id=\"focus_disconnected\">\n{\n  const focused = document.activeElement;\n  document.createElement('a').focus();\n  testing.expectEqual(focused, document.activeElement);\n}\n</script>\n\n<script id=\"click_focuses_element\">\n{\n  const input1 = $('#input1');\n  const input2 = $('#input2');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  let focusCount = 0;\n  let blurCount = 0;\n\n  input1.addEventListener('focus', () => focusCount++);\n  input1.addEventListener('blur', () => blurCount++);\n  input2.addEventListener('focus', () => focusCount++);\n\n  // Click input1 — should focus it and fire focus event\n  input1.click();\n  testing.expectEqual(input1, document.activeElement);\n  testing.expectEqual(1, focusCount);\n  testing.expectEqual(0, blurCount);\n\n  // Click input2 — should move focus, fire blur on input1 and focus on input2\n  input2.click();\n  testing.expectEqual(input2, document.activeElement);\n  testing.expectEqual(2, focusCount);\n  testing.expectEqual(1, blurCount);\n}\n</script>\n\n<script id=\"click_focuses_button\">\n{\n  const btn = $('#btn1');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  btn.click();\n  testing.expectEqual(btn, document.activeElement);\n}\n</script>\n\n<script id=\"focus_disconnected_no_blur\">\n{\n  const input1 = $('#input1');\n\n  if (document.activeElement) {\n    document.activeElement.blur();\n  }\n\n  input1.focus();\n  testing.expectEqual(input1, document.activeElement);\n\n  let blurCount = 0;\n  input1.addEventListener('blur', () => { blurCount++ });\n\n  // Focusing a disconnected element should be a no-op:\n  // blur must not fire on the currently focused element\n  document.createElement('a').focus();\n  testing.expectEqual(input1, document.activeElement);\n  testing.expectEqual(0, blurCount);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/get_element_by_id.html",
    "content": "<!DOCTYPE html>\n<body>\n  <script src=\"../testing.js\"></script>\n  <div id=\"div-1\">x: <p id=p1>d1</p></div>\n</body>\n\n<script id=getElementById>\n  testing.expectEqual(null, document.getElementById(null));\n  testing.expectEqual(null, document.getElementById(23));\n  testing.expectEqual(null, document.getElementById('value'));\n  testing.expectEqual('x: d1', document.getElementById('div-1').textContent);\n  testing.expectEqual('d1', document.getElementById('p1').textContent);\n  document.getElementById('p1').id = 'p2'\n  testing.expectEqual(null, document.getElementById('p1'));\n  testing.expectEqual('d1', document.getElementById('p2').textContent);\n\n  const div = document.createElement('div');\n  div.id = 'hello';\n  testing.expectEqual(null, document.getElementById('hello'));\n  const body = document.getElementsByTagName('body')[0];\n  body.appendChild(div);\n  testing.expectEqual('hello', document.getElementById('hello').id);\n\n  div.setAttribute('ID', 'world')\n  testing.expectEqual(null, document.getElementById('hello'));\n  testing.expectEqual('world', document.getElementById('world').id);\n\n  body.replaceChildren(div);\n  testing.expectEqual('world', document.getElementById('world').id);\n  testing.expectEqual(null, document.getElementById('div-1'));\n  testing.expectEqual(null, document.getElementById('p1'));\n\n  div.removeAttribute('id');\n  testing.expectEqual(null, document.getElementById('world'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/get_elements_by_class_name-multiple.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"div1\" class=\"foo bar\">Div 1</div>\n<div id=\"div2\" class=\"bar foo\">Div 2</div>\n<div id=\"div3\" class=\"foo bar baz\">Div 3</div>\n<div id=\"div4\" class=\"foo\">Div 4</div>\n<div id=\"div5\" class=\"bar\">Div 5</div>\n<div id=\"div6\" class=\"baz foo bar\">Div 6</div>\n\n<script id=getElementsByClassName_single>\n  {\n    const fooElements = document.getElementsByClassName('foo');\n    testing.expectEqual(5, fooElements.length);\n  }\n</script>\n\n<script id=getElementsByClassName_multiple>\n  {\n    // Should match elements that have BOTH foo AND bar\n    const fooBarElements = document.getElementsByClassName('foo bar');\n    testing.expectEqual(4, fooBarElements.length);\n\n    // Order shouldn't matter\n    const barFooElements = document.getElementsByClassName('bar foo');\n    testing.expectEqual(4, barFooElements.length);\n  }\n</script>\n\n<script id=getElementsByClassName_three>\n  {\n    // Should match elements that have ALL three classes\n    const threeClasses = document.getElementsByClassName('foo bar baz');\n    testing.expectEqual(2, threeClasses.length);\n  }\n</script>\n\n<script id=getElementsByClassName_whitespace>\n  {\n    // Multiple spaces, tabs, newlines should be treated as separators\n    const multiSpace = document.getElementsByClassName('foo  bar');\n    testing.expectEqual(4, multiSpace.length);\n\n    // Leading/trailing whitespace should be ignored\n    const withSpaces = document.getElementsByClassName('  foo bar  ');\n    testing.expectEqual(4, withSpaces.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/get_elements_by_class_name.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div class=\"foo\">foo1</div>\n<span class=\"foo\">foo2</span>\n<p class=\"foo\" id=\"foo3\">foo3</p>\n<div class=\"bar\">bar1</div>\n<div class=\"baz\">baz1</div>\n\n<div class=\"multi class names\">multi1</div>\n<span class=\"class multi\">multi2</span>\n\n<div id=\"container\">\n  <div class=\"nested\">nested1</div>\n  <div class=\"nested\">nested2</div>\n</div>\n\n<script>\n  const foos = document.getElementsByClassName('foo');\n</script>\n\n<script id=basic>\n  // testing.expectEqual(0, document.getElementsByClassName('nonexistent').length);\n  // testing.expectEqual(0, document.getElementsByClassName('unknown').length);\n\n  testing.expectEqual(true, foos instanceof HTMLCollection);\n  testing.expectEqual(3, foos.length);\n  testing.expectEqual(3, foos.length); // cache test\n  testing.expectEqual('foo1', foos[0].textContent);\n  testing.expectEqual('foo2', foos[1].textContent);\n  testing.expectEqual('foo3', foos[2].textContent);\n  testing.expectEqual('foo1', foos[0].textContent); // cache test\n  testing.expectEqual('foo3', foos[2].textContent);\n  testing.expectEqual('foo2', foos[1].textContent);\n  testing.expectEqual(undefined, foos[-1]);\n  testing.expectEqual(undefined, foos[3]);\n  testing.expectEqual(undefined, foos[100]);\n\n  const bars = document.getElementsByClassName('bar');\n  testing.expectEqual(1, bars.length);\n  testing.expectEqual('bar1', bars[0].textContent);\n\n  const bazs = document.getElementsByClassName('baz');\n  testing.expectEqual(1, bazs.length);\n  testing.expectEqual('baz1', bazs[0].textContent);\n</script>\n\n<script id=item>\n  testing.expectEqual('foo2', foos.item(1).textContent);\n  testing.expectEqual(null, foos.item(-1));\n  testing.expectEqual('foo1', foos.item(0).textContent);\n  testing.expectEqual(null, foos.item(-100));\n  testing.expectEqual(null, foos.item(3));\n  testing.expectEqual(null, foos.item(100));\n</script>\n\n<script id=namedItem>\n  testing.expectEqual('foo3', foos.namedItem('foo3').id);\n  testing.expectEqual(null, foos.namedItem('foo1'));\n  testing.expectEqual(null, foos.namedItem('bar'));\n  testing.expectEqual(null, foos.namedItem('nonexistent'));\n\n  testing.expectEqual('foo3', foos['foo3'].id);\n</script>\n\n<script id=iterator>\n  let acc = [];\n  for (let x of foos) {\n    acc.push(x.textContent);\n  }\n  testing.expectEqual(['foo1', 'foo2', 'foo3'], acc);\n</script>\n\n<script id=emptyString>\n  const empty = document.getElementsByClassName('');\n  testing.expectEqual(0, empty.length);\n</script>\n\n<script id=multipleClasses>\n  const multi = document.getElementsByClassName('multi');\n  testing.expectEqual(2, multi.length);\n  testing.expectEqual('multi1', multi[0].textContent);\n  testing.expectEqual('multi2', multi[1].textContent);\n\n  const classCol = document.getElementsByClassName('class');\n  testing.expectEqual(2, classCol.length);\n\n  const names = document.getElementsByClassName('names');\n  testing.expectEqual(1, names.length);\n  testing.expectEqual('multi1', names[0].textContent);\n</script>\n\n<script id=nested>\n  const nested = document.getElementsByClassName('nested');\n  testing.expectEqual(2, nested.length);\n  testing.expectEqual('nested1', nested[0].textContent);\n  testing.expectEqual('nested2', nested[1].textContent);\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/get_elements_by_name.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<input name=\"username\" value=\"john\">\n<input name=\"password\" type=\"password\">\n<input name=\"username\" value=\"jane\">\n<button name=\"submit\">Submit</button>\n<a name=\"section1\">Section 1</a>\n<a name=\"username\">User Link</a>\n\n<script id=getElementsByName_basic>\n  {\n    const usernames = document.getElementsByName('username');\n    testing.expectEqual(3, usernames.length);\n    testing.expectEqual('INPUT', usernames[0].tagName);\n    testing.expectEqual('john', usernames[0].value);\n    testing.expectEqual('INPUT', usernames[1].tagName);\n    testing.expectEqual('jane', usernames[1].value);\n    testing.expectEqual('A', usernames[2].tagName);\n\n    const passwords = document.getElementsByName('password');\n    testing.expectEqual(1, passwords.length);\n    testing.expectEqual('password', passwords[0].type);\n\n    const submits = document.getElementsByName('submit');\n    testing.expectEqual(1, submits.length);\n    testing.expectEqual('BUTTON', submits[0].tagName);\n  }\n</script>\n\n<script id=getElementsByName_empty>\n  {\n    const nonexistent = document.getElementsByName('nonexistent');\n    testing.expectEqual(0, nonexistent.length);\n  }\n</script>\n\n<script id=getElementsByName_live_collection>\n  {\n    const usernames = document.getElementsByName('username');\n    const initialLength = usernames.length;\n\n    const newInput = document.createElement('input');\n    newInput.name = 'username';\n    newInput.value = 'bob';\n    document.body.appendChild(newInput);\n\n    testing.expectEqual(initialLength + 1, usernames.length);\n    testing.expectEqual(newInput, usernames[usernames.length - 1]);\n\n    newInput.removeAttribute('name');\n    testing.expectEqual(initialLength, usernames.length);\n\n    newInput.name = 'username';\n    testing.expectEqual(initialLength + 1, usernames.length);\n\n    newInput.remove();\n    testing.expectEqual(initialLength, usernames.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/get_elements_by_tag_name-wildcard.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<head>\n  <title>Test</title>\n</head>\n<body>\n  <div id=\"div1\">\n    <span id=\"span1\">Text</span>\n  </div>\n  <p id=\"p1\">Paragraph</p>\n</body>\n\n<script id=getElementsByTagName_wildcard>\n  {\n    const allElements = document.getElementsByTagName('*');\n    testing.expectEqual('HTMLCollection', allElements.constructor.name);\n\n    // Should include: html, head, title, script (testing.js), body, div, span, p, script (this one)\n    // At least 9 elements\n    testing.expectEqual(true, allElements.length >= 9);\n\n    // Check some specific elements are included\n    let foundDiv = false;\n    let foundSpan = false;\n    let foundP = false;\n\n    for (let i = 0; i < allElements.length; i++) {\n      const el = allElements[i];\n      if (el.tagName === 'DIV') foundDiv = true;\n      if (el.tagName === 'SPAN') foundSpan = true;\n      if (el.tagName === 'P') foundP = true;\n    }\n\n    testing.expectEqual(true, foundDiv);\n    testing.expectEqual(true, foundSpan);\n    testing.expectEqual(true, foundP);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/get_elements_by_tag_name.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div>0</div>\n<div>1</div>\n<div>2</div>\n\n<p id=p1></p>\n<p id=p2></p>\n<p id=p3></p>\n\n<h1>H1</h1>\n<h2>H2</h2>\n<h3>H3</h3>\n<h4>H4</h4>\n<h5>H5</h5>\n<h6>H6</h6>\n<b>Bold</b>\n<i>Italic</i>\n<em>Emphasized</em>\n<strong>Strong</strong>\n<header>Header</header>\n<nav>Nav</nav>\n<main>Main</main>\n\n<script id=basic>\n  testing.expectEqual(24, document.getElementsByTagName('*').length);\n  testing.expectEqual(0, document.getElementsByTagName('a').length);\n  testing.expectEqual(0, document.getElementsByTagName('unknown').length);\n\n  const divs = document.getElementsByTagName('div');\n  testing.expectEqual(true, divs instanceof HTMLCollection);\n  testing.expectEqual(3, divs.length);\n  testing.expectEqual(3, divs.length);\n  testing.expectEqual('0', divs[0].textContent);\n  testing.expectEqual('0', divs[0].textContent);\n  testing.expectEqual('0', divs[0].textContent);\n  testing.expectEqual('1', divs[1].textContent);\n  testing.expectEqual('2', divs[2].textContent);\n  testing.expectEqual('2', divs[2].textContent);\n  testing.expectEqual('1', divs[1].textContent);\n  testing.expectEqual('1', divs[1].textContent);\n  testing.expectEqual('2', divs[2].textContent);\n  testing.expectEqual('0', divs[0].textContent);\n  testing.expectEqual(undefined, divs[-1]);\n  testing.expectEqual(undefined, divs[4]);\n\n  testing.expectEqual('2', divs.item(2).textContent);\n  testing.expectEqual(null, divs.item(-3));\n  testing.expectEqual('0', divs.item(0).textContent);\n  testing.expectEqual(null, divs.item(-100));\n</script>\n\n<script id=namedItem>\n  testing.expectEqual(null, divs.namedItem('d1'));\n\n  const ps = document.getElementsByTagName('p');\n  testing.expectEqual('p1', ps.namedItem('p1').id);\n  testing.expectEqual('p2', ps.namedItem('p2').id);\n  testing.expectEqual('p3', ps.namedItem('p3').id);\n  testing.expectEqual(null, ps.namedItem('p4'));\n\n  testing.expectEqual('p1', ps['p1'].id);\n</script>\n\n<script id=iterator>\n  let acc = [];\n  for (let x of ps) {\n    acc.push(x.id);\n  }\n  testing.expectEqual(['p1', 'p2', 'p3'], acc);\n</script>\n\n<script id=extendedTags>\n  // Test headings\n  testing.expectEqual(1, document.getElementsByTagName('h1').length);\n  testing.expectEqual('H1', document.getElementsByTagName('h1')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('h2').length);\n  testing.expectEqual('H2', document.getElementsByTagName('h2')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('h3').length);\n  testing.expectEqual(1, document.getElementsByTagName('h4').length);\n  testing.expectEqual(1, document.getElementsByTagName('h5').length);\n  testing.expectEqual(1, document.getElementsByTagName('h6').length);\n\n  // Test case insensitivity\n  testing.expectEqual(1, document.getElementsByTagName('H1').length);\n  testing.expectEqual('H1', document.getElementsByTagName('H1')[0].textContent);\n\n  // Test text formatting elements\n  testing.expectEqual(1, document.getElementsByTagName('b').length);\n  testing.expectEqual('Bold', document.getElementsByTagName('b')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('i').length);\n  testing.expectEqual('Italic', document.getElementsByTagName('i')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('em').length);\n  testing.expectEqual('Emphasized', document.getElementsByTagName('em')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('strong').length);\n  testing.expectEqual('Strong', document.getElementsByTagName('strong')[0].textContent);\n\n  // Test structural elements\n  testing.expectEqual(1, document.getElementsByTagName('header').length);\n  testing.expectEqual('Header', document.getElementsByTagName('header')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('nav').length);\n  testing.expectEqual('Nav', document.getElementsByTagName('nav')[0].textContent);\n  testing.expectEqual(1, document.getElementsByTagName('main').length);\n  testing.expectEqual('Main', document.getElementsByTagName('main')[0].textContent);\n</script>\n\n<script id=multipleIterators>\n{\n  const list = document.getElementsByTagName('p');\n\n  const iter1 = list[Symbol.iterator]();\n  const iter2 = list[Symbol.iterator]();\n\n  const val1_iter1 = iter1.next();\n  testing.expectEqual(false, val1_iter1.done);\n  testing.expectEqual('p1', val1_iter1.value.id);\n\n  const val1_iter2 = iter2.next();\n  testing.expectEqual(false, val1_iter2.done);\n  testing.expectEqual('p1', val1_iter2.value.id);\n\n  const val2_iter1 = iter1.next();\n  testing.expectEqual(false, val2_iter1.done);\n  testing.expectEqual('p2', val2_iter1.value.id);\n\n  const val2_iter2 = iter2.next();\n  testing.expectEqual(false, val2_iter2.done);\n  testing.expectEqual('p2', val2_iter2.value.id);\n}\n</script>\n\n<script id=iteratorLifetime>\n{\n  let iter;\n  {\n    const list = document.getElementsByTagName('p');\n    iter = list[Symbol.iterator]();\n  }\n\n  const val1 = iter.next();\n  testing.expectEqual(false, val1.done);\n  testing.expectEqual('p1', val1.value.id);\n\n  const val2 = iter.next();\n  testing.expectEqual(false, val2.done);\n  testing.expectEqual('p2', val2.value.id);\n\n  const val3 = iter.next();\n  testing.expectEqual(false, val3.done);\n  testing.expectEqual('p3', val3.value.id);\n\n  const val4 = iter.next();\n  testing.expectEqual(true, val4.done);\n}\n</script>\n\n<script id=list_caching>\n  {\n    // this ordering used to crash\n    const list = document.getElementsByTagName('div');\n    testing.expectEqual('0', list[0].textContent);\n    testing.expectEqual(3, list.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/insert_adjacent_element.html",
    "content": "<!DOCTYPE html>\n<head id=\"the_head\">\n  <title>Test Document Title</title>\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body>\n  <!-- This structure will get mutated by insertAdjacentElement test -->\n  <div id=\"insert-adjacent-element-outer-wrapper\">\n    <div id=\"insert-adjacent-element-inner-wrapper\">\n      <span></span>\n      <p>content</p>\n    </div>\n  </div>\n</body>\n\n<script id=insertAdjacentElement>\n  // Insert \"beforeend\".\n  const wrapper = $(\"#insert-adjacent-element-inner-wrapper\");\n  const h1 = document.createElement(\"h1\");\n  h1.innerHTML = \"title\";\n  wrapper.insertAdjacentElement(\"beforeend\", h1);\n  let newElement = wrapper.lastElementChild;\n  testing.expectEqual(\"H1\", newElement.tagName);\n  testing.expectEqual(\"title\", newElement.innerText);\n\n  // Insert \"beforebegin\".\n  const h2 = document.createElement(\"h2\");\n  h2.innerHTML = \"small title\";\n  wrapper.insertAdjacentElement(\"beforebegin\", h2);\n  newElement = wrapper.previousElementSibling;\n  testing.expectEqual(\"H2\", newElement.tagName);\n  testing.expectEqual(\"small title\", newElement.innerText);\n\n  // Insert \"afterend\".\n  const divAfterEnd = document.createElement(\"div\");\n  divAfterEnd.id = \"afterend\";\n  divAfterEnd.innerHTML = \"after end\";\n  wrapper.insertAdjacentElement(\"afterend\", divAfterEnd);\n  newElement = wrapper.nextElementSibling;\n  testing.expectEqual(\"DIV\", newElement.tagName);\n  testing.expectEqual(\"after end\", newElement.innerText);\n  testing.expectEqual(\"afterend\", newElement.id);\n\n  // Insert \"afterbegin\".\n  const divAfterBegin = document.createElement(\"div\");\n  divAfterBegin.className = \"afterbegin\";\n  divAfterBegin.innerHTML = \"after begin\";\n  wrapper.insertAdjacentElement(\"afterbegin\", divAfterBegin);\n  newElement = wrapper.firstElementChild;\n  testing.expectEqual(\"DIV\", newElement.tagName);\n  testing.expectEqual(\"after begin\", newElement.innerText);\n  testing.expectEqual(\"afterbegin\", newElement.className);\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/insert_adjacent_html.html",
    "content": "<!DOCTYPE html>\n<head id=\"the_head\">\n  <title>Test Document Title</title>\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body>\n  <!-- This structure will get mutated by insertAdjacentHTML test -->\n  <div id=\"insert-adjacent-html-outer-wrapper\">\n    <div id=\"insert-adjacent-html-inner-wrapper\">\n      <span></span>\n      <p>content</p>\n    </div>\n  </div>\n</body>\n\n<script id=insertAdjacentHTML>\n  // Insert \"beforeend\".\n  const wrapper = $(\"#insert-adjacent-html-inner-wrapper\");\n  wrapper.insertAdjacentHTML(\"beforeend\", \"<h1>title</h1>\");\n  let newElement = wrapper.lastElementChild;\n  testing.expectEqual(\"H1\", newElement.tagName);\n  testing.expectEqual(\"title\", newElement.innerText);\n\n  // Insert \"beforebegin\".\n  wrapper.insertAdjacentHTML(\"beforebegin\", \"<h2>small title</h2>\");\n  newElement = wrapper.previousElementSibling;\n  testing.expectEqual(\"H2\", newElement.tagName);\n  testing.expectEqual(\"small title\", newElement.innerText);\n\n  // Insert \"afterend\".\n  wrapper.insertAdjacentHTML(\"afterend\", \"<div id=\\\"afterend\\\">after end</div>\");\n  newElement = wrapper.nextElementSibling;\n  testing.expectEqual(\"DIV\", newElement.tagName);\n  testing.expectEqual(\"after end\", newElement.innerText);\n  testing.expectEqual(\"afterend\", newElement.id);\n\n  // Insert \"afterbegin\".\n  wrapper.insertAdjacentHTML(\"afterbegin\", \"<div class=\\\"afterbegin\\\">after begin</div><yy></yy>\");\n  newElement = wrapper.firstElementChild;\n  testing.expectEqual(\"DIV\", newElement.tagName);\n  testing.expectEqual(\"after begin\", newElement.innerText);\n  testing.expectEqual(\"afterbegin\", newElement.className);\n\n  const fuzzWrapper = document.createElement(\"div\");\n  fuzzWrapper.id = \"fuzz-wrapper\";\n  document.body.appendChild(fuzzWrapper);\n\n  const fuzzCases = [\n    // These cases have no <body> element (or empty body), so nothing is inserted\n    { name: \"empty string\", html: \"\", expectElements: 0 },\n    { name: \"comment only\", html: \"<!-- comment -->\", expectElements: 0 },\n    { name: \"doctype only\", html: \"<!DOCTYPE html>\", expectElements: 0 },\n    { name: \"full empty doc\", html: \"<!DOCTYPE html><html><head></head><body></body></html>\", expectElements: 0 },\n\n    { name: \"whitespace only\", html: \"   \", expectElements: 0 },\n    { name: \"newlines only\", html: \"\\n\\n\\n\", expectElements: 0 },\n    { name: \"just text\", html: \"plain text\", expectElements: 0 },\n    // Head-only elements: Extracted from <head> container\n    { name: \"empty meta\", html: \"<meta>\", expectElements: 1 },\n    { name: \"empty title\", html: \"<title></title>\", expectElements: 1 },\n    { name: \"empty head\", html: \"<head></head>\", expectElements: 0 },  // Container with no children\n    { name: \"empty body\", html: \"<body></body>\", expectElements: 0 },  // Container with no children\n    { name: \"empty html\", html: \"<html></html>\", expectElements: 0 },  // Container with no children\n    { name: \"meta only\", html: \"<meta charset='utf-8'>\", expectElements: 1 },\n    { name: \"title only\", html: \"<title>Test</title>\", expectElements: 1 },\n    { name: \"link only\", html: \"<link rel='stylesheet' href='test.css'>\", expectElements: 1 },\n    { name: \"meta and title\", html: \"<meta charset='utf-8'><title>Test</title>\", expectElements: 2 },\n    { name: \"script only\", html: \"<script>console.log('hi')<\\/script>\", expectElements: 1 },\n    { name: \"style only\", html: \"<style>body { color: red; }<\\/style>\", expectElements: 1 },\n    { name: \"unclosed div\", html: \"<div>content\", expectElements: 1 },\n    { name: \"unclosed span\", html: \"<span>text\", expectElements: 1 },\n    { name: \"invalid tag\", html: \"<notarealtag>content</notarealtag>\", expectElements: 1 },\n    { name: \"malformed\", html: \"<<div>>test<</div>>\", expectElements: 1 },  // Parser handles it\n    { name: \"just closing tag\", html: \"</div>\", expectElements: 0 },\n    { name: \"nested empty\", html: \"<div><div></div></div>\", expectElements: 1 },\n    { name: \"multiple top-level\", html: \"<span>1</span><span>2</span><span>3</span>\", expectElements: 3 },\n    { name: \"mixed text and elements\", html: \"Text before<b>bold</b>Text after\", expectElements: 1 },\n    { name: \"deeply nested\", html: \"<div><div><div><span>deep</span></div></div></div>\", expectElements: 1 },\n  ];\n\n  fuzzCases.forEach((tc, idx) => {\n    fuzzWrapper.innerHTML = \"\";\n    fuzzWrapper.insertAdjacentHTML(\"beforeend\", tc.html);\n    if (tc.expectElements !== fuzzWrapper.childElementCount) {\n      console.warn(`Fuzz idx: ${idx}`);\n      testing.expectEqual(tc.expectElements, fuzzWrapper.childElementCount);\n    }\n  });\n\n  // Clean up\n  document.body.removeChild(fuzzWrapper);\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/insert_adjacent_text.html",
    "content": "<!DOCTYPE html>\n<head id=\"the_head\">\n  <title>Test Document Title</title>\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body>\n  <!-- This structure will get mutated by insertAdjacentText test -->\n  <div id=\"insert-adjacent-text-outer-wrapper\">\n    <div id=\"insert-adjacent-text-inner-wrapper\">\n      <span></span>\n      <p>content</p>\n    </div>\n  </div>\n</body>\n\n<script id=insertAdjacentText>\n  const wrapper = $(\"#insert-adjacent-text-inner-wrapper\");\n\n  // Insert \"beforeend\".\n  {\n    wrapper.insertAdjacentText(\"beforeend\", \"atlas, rise!\");\n    const { nodeType, data } = wrapper.lastChild;\n    testing.expectEqual(nodeType, Node.TEXT_NODE);\n    testing.expectEqual(data, \"atlas, rise!\");\n  }\n\n  // Insert \"beforebegin\".\n  {\n    wrapper.insertAdjacentText(\"beforebegin\", \"before everything else\");\n    const { nodeType, data } = wrapper.previousSibling;\n    testing.expectEqual(nodeType, Node.TEXT_NODE);\n    testing.expectEqual(data, \"before everything else\");\n  }\n\n  // Insert \"afterend\".\n  {\n    wrapper.insertAdjacentText(\"afterend\", \"after end\");\n    const { nodeType, data } = wrapper.nextSibling;\n    testing.expectEqual(nodeType, Node.TEXT_NODE);\n    testing.expectEqual(data, \"after end\");\n  }\n\n  // Insert \"afterbegin\".\n  wrapper.insertAdjacentText(\"afterbegin\", \"after begin\");\n  const { nodeType, data } = wrapper.firstChild;\n  testing.expectEqual(nodeType, Node.TEXT_NODE);\n  testing.expectEqual(data, \"after begin\");\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/query_selector.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div class=\"test-class\">div1</div>\n<span class=\"test-class\">span1</span>\n<div class=\"multiple classes here\">multi1</div>\n<div class=\"classes\">multi2</div>\n<div class=\"\">empty-class</div>\n<div>no-class</div>\n\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<h3>Heading 3</h3>\n<h4>Heading 4</h4>\n<h5>Heading 5</h5>\n<h6>Heading 6</h6>\n<b>Bold text</b>\n<i>Italic text</i>\n<em>Emphasized text</em>\n<strong>Strong text</strong>\n<header>Header element</header>\n<nav>Navigation</nav>\n<main>Main content</main>\n\n<script id=byId name=\"test1\">\n  testing.expectEqual(1, document.querySelector.length);\n  testing.expectError(\"SyntaxError\", () => document.querySelector(''));\n  testing.withError((err) => {\n    testing.expectEqual(12, err.code);\n    testing.expectEqual(\"SyntaxError\", err.name);\n  }, () => document.querySelector(''));\n\n  testing.expectEqual('test1', document.querySelector('#byId').getAttribute('name'));\n</script>\n\n<script id=byClass>\n{\n  const result = document.querySelector('.test-class');\n  testing.expectEqual('div1', result.textContent);\n\n  testing.expectEqual('multi1', document.querySelector('.multiple').textContent);\n  testing.expectEqual('multi1', document.querySelector('.classes').textContent);\n  testing.expectEqual('multi1', document.querySelector('.here').textContent);\n\n  testing.expectEqual(null, document.querySelector('.nonexistent'));\n}\n</script>\n\n<script id=byTag>\n{\n  const result = document.querySelector('div');\n  testing.expectEqual('div1', result.textContent);\n\n  testing.expectEqual('span1', document.querySelector('span').textContent);\n\n  // Test that script elements can be found\n  const firstScript = document.querySelector('script');\n  testing.expectEqual('SCRIPT', firstScript.tagName);\n\n  testing.expectEqual(null, document.querySelector('article'));\n  // testing.expectEqual(null, document.querySelector('another'));\n}\n</script>\n\n<script id=byTagExtended>\n{\n  // Test headings (h1-h6)\n  testing.expectEqual('Heading 1', document.querySelector('h1').textContent);\n  testing.expectEqual('Heading 2', document.querySelector('h2').textContent);\n  testing.expectEqual('Heading 3', document.querySelector('h3').textContent);\n  testing.expectEqual('Heading 4', document.querySelector('h4').textContent);\n  testing.expectEqual('Heading 5', document.querySelector('h5').textContent);\n  testing.expectEqual('Heading 6', document.querySelector('h6').textContent);\n\n  // Test case insensitivity\n  testing.expectEqual('Heading 1', document.querySelector('H1').textContent);\n  testing.expectEqual('Heading 2', document.querySelector('H2').textContent);\n\n  // Test text formatting elements\n  testing.expectEqual('Bold text', document.querySelector('b').textContent);\n  testing.expectEqual('Italic text', document.querySelector('i').textContent);\n  testing.expectEqual('Emphasized text', document.querySelector('em').textContent);\n  testing.expectEqual('Strong text', document.querySelector('strong').textContent);\n\n  // Test structural elements\n  testing.expectEqual('Header element', document.querySelector('header').textContent);\n  testing.expectEqual('Navigation', document.querySelector('nav').textContent);\n  testing.expectEqual('Main content', document.querySelector('main').textContent);\n\n  // Test case insensitivity for these too\n  testing.expectEqual('Navigation', document.querySelector('NAV').textContent);\n  testing.expectEqual('Main content', document.querySelector('Main').textContent);\n}\n</script>\n\n<div id=\"compound-test\" class=\"container active\">Compound 1</div>\n<div class=\"container\">Compound 2</div>\n<span class=\"container active\">Compound 3</span>\n<p id=\"compound-test2\">Compound 4</p>\n\n<script id=compoundSelectors>\n{\n  testing.expectEqual('Compound 1', document.querySelector('div.container').textContent);\n  testing.expectEqual('Compound 1', document.querySelector('div.active').textContent);\n  testing.expectEqual('Compound 3', document.querySelector('span.active').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('div.container.active').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('div#compound-test').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('.container#compound-test').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('#compound-test.active').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('.container.active').textContent);\n  testing.expectEqual('Compound 1', document.querySelector('.active.container').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('  div.container').textContent);\n  testing.expectEqual('Compound 1', document.querySelector('  .active').textContent);\n\n  testing.expectEqual('Compound 1', document.querySelector('div.container  ').textContent);\n  testing.expectEqual('Compound 4', document.querySelector('#compound-test2  ').textContent);\n\n  testing.expectEqual('Compound 3', document.querySelector('  span.active  ').textContent);\n\n  testing.expectEqual(null, document.querySelector('span#compound-test'));\n  testing.expectEqual(null, document.querySelector('div.nonexistent'));\n  testing.expectEqual(null, document.querySelector('p.container'));\n}\n</script>\n\n<script id=universalSelector>\n{\n  const result = document.querySelector('*');\n  testing.expectEqual('HTML', result.tagName);\n\n  testing.expectEqual('div1', document.querySelector('*.test-class').textContent);\n\n  testing.expectEqual('test1', document.querySelector('*#byId').getAttribute('name'));\n\n  testing.expectEqual('Compound 1', document.querySelector('*.container.active').textContent);\n}\n</script>\n\n<div id=\"descendant-container\">\n  <p class=\"desc-text\">Direct child paragraph</p>\n  <div class=\"nested\">\n    <p class=\"desc-text\">Nested paragraph</p>\n    <span>\n      <p class=\"deep\">Deeply nested paragraph</p>\n    </span>\n  </div>\n  <article>\n    <p class=\"desc-text\">Article paragraph</p>\n  </article>\n</div>\n<p class=\"desc-text\">Outside paragraph</p>\n\n<div id=\"complex\">\n  <div class=\"outer\">\n    <div class=\"middle\">\n      <div class=\"inner\">\n        <span id=\"target\">Target span</span>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script id=descendantSelectors>\n{\n  testing.expectEqual('Direct child paragraph', document.querySelector('div p').textContent);\n  testing.expectEqual('Direct child paragraph', document.querySelector('#descendant-container p').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('div div p').textContent);\n  testing.expectEqual('target', document.querySelector('div div div span').id);\n  testing.expectEqual('Nested paragraph', document.querySelector('div.nested p.desc-text').textContent);\n  testing.expectEqual('target', document.querySelector('div #target').id);\n  testing.expectEqual('Nested paragraph', document.querySelector('.nested p').textContent);\n  testing.expectEqual('Deeply nested paragraph', document.querySelector('div span p').textContent);\n  testing.expectEqual(null, document.querySelector('article div p'));\n}\n</script>\n\n<script id=descendantWithWhitespace>\n{\n  testing.expectEqual('Direct child paragraph', document.querySelector('  div   p  ').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('  div.nested    p.desc-text  ').textContent);\n}\n</script>\n\n<div id=\"compound-id-test\" class=\"special\">\n  <p class=\"text\">Paragraph in compound ID div</p>\n  <div class=\"inner\">\n    <span id=\"nested-compound\" class=\"highlight\">Nested span</span>\n  </div>\n</div>\n\n<script id=compoundIdSelectors>\n{\n  testing.expectEqual('Paragraph in compound ID div', document.querySelector('div#compound-id-test p').textContent);\n  testing.expectEqual('Paragraph in compound ID div', document.querySelector('#compound-id-test.special p').textContent);\n  testing.expectEqual('Nested span', document.querySelector('div #nested-compound.highlight').textContent);\n  testing.expectEqual('Nested span', document.querySelector('#compound-id-test span#nested-compound').textContent);\n  testing.expectEqual('Nested span', document.querySelector('div.special #nested-compound.highlight').textContent);\n}\n</script>\n\n<script id=idOptimizationEdgeCases>\n{\n  testing.expectEqual(null, document.querySelector('article #compound-id-test p'));\n  testing.expectEqual(null, document.querySelector('#compound-id-test.wrong-class p'));\n  testing.expectEqual(null, document.querySelector('#compound-id-test.container p'));\n  testing.expectEqual(null, document.querySelector('#compound-id-test article'));\n  testing.expectEqual(null, document.querySelector('#compound-id-test nav'));\n  testing.expectEqual(null, document.querySelector('#nonexistent-id p'));\n  testing.expectEqual(null, document.querySelector('div #nonexistent-id'));\n  testing.expectEqual(null, document.querySelector('#nonexistent-id.some-class span'));\n  testing.expectEqual(null, document.querySelector('span#compound-id-test p'));\n  testing.expectEqual(null, document.querySelector('article#nested-compound'));\n}\n</script>\n\n<script id=universalDescendantSelectors>\n{\n  testing.expectEqual('div1', document.querySelector('* div').textContent);\n  testing.expectEqual('span1', document.querySelector('* span').textContent);\n  testing.expectEqual('Direct child paragraph', document.querySelector('div *').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('.nested *').textContent);\n  testing.expectEqual('div1', document.querySelector('* .test-class').textContent);\n  testing.expectEqual('multi1', document.querySelector('* .multiple').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('.nested *').textContent);\n  testing.expectEqual('Paragraph in compound ID div', document.querySelector('.special *').textContent);\n  testing.expectEqual('test1', document.querySelector('* #byId').getAttribute('name'));\n  testing.expectEqual('Compound 1', document.querySelector('* #compound-test').textContent);\n  testing.expectEqual('Direct child paragraph', document.querySelector('#descendant-container *').textContent);\n  testing.expectEqual('Paragraph in compound ID div', document.querySelector('#compound-id-test *').textContent);\n  testing.expectEqual('div1', document.querySelector('* * div').textContent);\n  testing.expectEqual(document.querySelector('p').textContent, document.querySelector('* * p').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('div * p').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('#descendant-container * p').textContent);\n  testing.expectEqual('Deeply nested paragraph', document.querySelector('.nested * p').textContent);\n  testing.expectEqual('target', document.querySelector('#complex * span').id);\n  testing.expectEqual('Nested span', document.querySelector('#compound-id-test * span').textContent);\n  testing.expectEqual('Nested paragraph', document.querySelector('* div.nested p').textContent);\n  testing.expectEqual('Paragraph in compound ID div', document.querySelector('* #compound-id-test.special p').textContent);\n}\n</script>\n\n<svg id=\"test-svg-selector\" width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">\n  <circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\"/>\n  <text x=\"10\" y=\"20\">SVG Text Content</text>\n  <g id=\"svg-group\">\n    <rect x=\"5\" y=\"5\" width=\"20\" height=\"20\"/>\n  </g>\n</svg>\n\n<script id=svgSelectors>\n{\n  testing.expectEqual('svg', document.querySelector('svg').tagName);\n  testing.expectEqual('circle', document.querySelector('circle').tagName);\n  testing.expectEqual('text', document.querySelector('text').tagName);\n\n  testing.expectEqual('circle', document.querySelector('svg circle').tagName);\n  testing.expectEqual('text', document.querySelector('svg text').tagName);\n  testing.expectEqual('rect', document.querySelector('svg rect').tagName);\n\n  testing.expectEqual('rect', document.querySelector('#svg-group rect').tagName);\n  testing.expectEqual('rect', document.querySelector('svg #svg-group rect').tagName);\n  testing.expectEqual('rect', document.querySelector('g rect').tagName);\n  testing.expectEqual('rect', document.querySelector('svg g rect').tagName);\n}\n</script>\n\n<script id=special>\n  testing.expectEqual(null, document.querySelector('\\\\'));\n\n  testing.expectEqual(null, document.querySelector('div\\\\'));\n  testing.expectEqual(null, document.querySelector('.test-class\\\\'));\n  testing.expectEqual(null, document.querySelector('#byId\\\\'));\n</script>\n\n<div class=\"café\">Non-ASCII class 1</div>\n<div class=\"日本語\">Non-ASCII class 2</div>\n<span id=\"niño\">Non-ASCII ID 1</span>\n<p id=\"🎨\">Non-ASCII ID 2</p>\n\n<script id=nonAsciiSelectors>\n  testing.expectEqual('Non-ASCII class 1', document.querySelector('.café').textContent);\n  testing.expectEqual('Non-ASCII class 2', document.querySelector('.日本語').textContent);\n\n  testing.expectEqual('Non-ASCII ID 1', document.querySelector('#niño').textContent);\n  testing.expectEqual('Non-ASCII ID 2', document.querySelector('#🎨').textContent);\n\n  testing.expectEqual('Non-ASCII class 1', document.querySelector('div.café').textContent);\n  testing.expectEqual('Non-ASCII ID 1', document.querySelector('span#niño').textContent);\n</script>\n\n<span id=\".,:!\">Punctuation test</span>\n\n<script id=escapedPunctuation>\n{\n  // Test escaped punctuation in ID selectors\n  testing.expectEqual('Punctuation test', document.querySelector('#\\\\.\\\\,\\\\:\\\\!').textContent);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/query_selector_all.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div class=\"test-class\">div1</div>\n<span class=\"test-class\">span1</span>\n<div class=\"multiple classes here\">multi1</div>\n<div class=\"classes\">multi2</div>\n<div class=\"\">empty-class</div>\n<div>no-class</div>\n\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<h3>Heading 3</h3>\n<h4>Heading 4</h4>\n<h5>Heading 5</h5>\n<h6>Heading 6</h6>\n<b>Bold text</b>\n<i>Italic text</i>\n<em>Emphasized text</em>\n<strong>Strong text</strong>\n<header>Header element</header>\n<nav>Navigation</nav>\n<main>Main content</main>\n\n<script>\n  function assertList(expected, result) {\n    testing.expectEqual(expected.length, result.length);\n    testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));\n    testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));\n\n    testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));\n    testing.expectEqual(expected.map((e, i) => i.toString()), Object.keys(result));\n  }\n</script>\n\n<script id=script1 name=\"test1\">\n  testing.expectError(\"SyntaxError\", () => document.querySelectorAll(''));\n  testing.withError((err) => {\n    testing.expectEqual(12, err.code);\n    testing.expectEqual(\"SyntaxError\", err.name);\n  }, () => document.querySelectorAll(''));\n</script>\n\n<script id=byId>\n{\n  const result = document.querySelectorAll('#script1');\n  testing.expectEqual(true, result instanceof NodeList);\n  testing.expectEqual(1, result.length);\n  testing.expectEqual('test1', result[0].getAttribute('name'));\n\n  assertList([], document.querySelectorAll('#nonexistent'));\n}\n</script>\n\n<script id=byClass>\n  assertList([], document.querySelectorAll('.nope'));\n  assertList(['div1', 'span1'], document.querySelectorAll('.test-class'));\n\n  assertList(['multi1', 'multi2'], document.querySelectorAll('.classes'));\n  assertList(['multi1'], document.querySelectorAll('.multiple'));\n  assertList(['multi1'], document.querySelectorAll('.here'));\n</script>\n\n<script id=byTag>\n{\n  const divs = document.querySelectorAll('div');\n  testing.expectEqual(true, divs.length >= 5);\n  testing.expectEqual('div1', divs[0].textContent);\n\n  const scripts = document.querySelectorAll('script');\n  testing.expectEqual(true, scripts.length >= 5);\n\n  assertList(['span1'], document.querySelectorAll('span'));\n\n  assertList([], document.querySelectorAll('article'));\n  assertList([], document.querySelectorAll('section'));\n\n  assertList(['Heading 1'], document.querySelectorAll('h1'));\n  assertList(['Heading 2'], document.querySelectorAll('h2'));\n  assertList(['Heading 3'], document.querySelectorAll('h3'));\n\n  assertList(['Heading 1'], document.querySelectorAll('H1'));\n  assertList(['Navigation'], document.querySelectorAll('NAV'));\n}\n</script>\n\n<div id=\"compound-test\" class=\"container active\">Compound 1</div>\n<div class=\"container\">Compound 2</div>\n<span class=\"container active\">Compound 3</span>\n<div class=\"active\">Compound 4</div>\n<p id=\"compound-test2\">Compound 5</p>\n\n<script id=compoundSelectors>\n{\n  assertList(['Compound 1', 'Compound 2'], document.querySelectorAll('div.container'));\n  assertList(['Compound 1', 'Compound 4'], document.querySelectorAll('div.active'));\n  assertList(['Compound 3'], document.querySelectorAll('span.active'));\n\n  assertList(['Compound 1', 'Compound 3'], document.querySelectorAll('.container.active'));\n  assertList(['Compound 1', 'Compound 3'], document.querySelectorAll('.active.container'));\n\n  assertList(['Compound 1'], document.querySelectorAll('div.container.active'));\n\n  assertList(['Compound 1'], document.querySelectorAll('div#compound-test'));\n\n  assertList(['Compound 1'], document.querySelectorAll('.container#compound-test'));\n  assertList(['Compound 1'], document.querySelectorAll('#compound-test.active'));\n\n  assertList(['Compound 1', 'Compound 2'], document.querySelectorAll('  div.container'));\n  assertList(['Compound 1', 'Compound 3', 'Compound 4'], document.querySelectorAll('  .active'));\n  assertList(['Compound 1', 'Compound 2'], document.querySelectorAll('div.container  '));\n  assertList(['Compound 5'], document.querySelectorAll('#compound-test2  '));\n  assertList(['Compound 3'], document.querySelectorAll('  span.active  '));\n\n  assertList([], document.querySelectorAll('span#compound-test'));\n  assertList([], document.querySelectorAll('div.nonexistent'));\n  assertList([], document.querySelectorAll('p.container'));\n}\n</script>\n\n<script id=universalSelector>\n{\n  const all = document.querySelectorAll('*');\n  testing.expectEqual(true, all.length > 20);\n  testing.expectEqual('HTML', all[0].tagName);\n\n  assertList(['div1', 'span1'], document.querySelectorAll('*.test-class'));\n\n  const scriptResult = document.querySelectorAll('*#script1');\n  testing.expectEqual(1, scriptResult.length);\n  testing.expectEqual('test1', scriptResult[0].getAttribute('name'));\n\n  assertList(['Compound 1', 'Compound 3'], document.querySelectorAll('*.container.active'));\n}\n</script>\n\n<script id=iteratorTests>\n{\n  const list = document.querySelectorAll('.test-class');\n\n  const entries = Array.from(list.entries());\n  testing.expectEqual(2, entries.length);\n  testing.expectEqual(0, entries[0][0]);\n  testing.expectEqual('div1', entries[0][1].textContent);\n  testing.expectEqual(1, entries[1][0]);\n  testing.expectEqual('span1', entries[1][1].textContent);\n\n  const keys = Array.from(list.keys());\n  testing.expectEqual([0, 1], keys);\n\n  const values = Array.from(list.values());\n  testing.expectEqual(['div1', 'span1'], values.map((e) => e.textContent));\n\n  const defaultIter = Array.from(list);\n  testing.expectEqual(['div1', 'span1'], defaultIter.map((e) => e.textContent));\n}\n</script>\n\n<script id=multipleIterators>\n{\n  const list = document.querySelectorAll('.test-class');\n\n  const keysIter = list.keys();\n  const valuesIter = list.values();\n\n  const key1 = keysIter.next();\n  testing.expectEqual(false, key1.done);\n  testing.expectEqual(0, key1.value);\n\n  const val1 = valuesIter.next();\n  testing.expectEqual(false, val1.done);\n  testing.expectEqual('div1', val1.value.textContent);\n\n  const key2 = keysIter.next();\n  testing.expectEqual(false, key2.done);\n  testing.expectEqual(1, key2.value);\n\n  const val2 = valuesIter.next();\n  testing.expectEqual(false, val2.done);\n  testing.expectEqual('span1', val2.value.textContent);\n\n  const key3 = keysIter.next();\n  testing.expectEqual(true, key3.done);\n\n  const val3 = valuesIter.next();\n  testing.expectEqual(true, val3.done);\n}\n</script>\n\n<script id=iteratorLifetime>\n{\n  let iter;\n  {\n    const list = document.querySelectorAll('.test-class');\n    iter = list.values();\n  }\n\n  const val1 = iter.next();\n  testing.expectEqual(false, val1.done);\n  testing.expectEqual('div1', val1.value.textContent);\n\n  const val2 = iter.next();\n  testing.expectEqual(false, val2.done);\n  testing.expectEqual('span1', val2.value.textContent);\n\n  const val3 = iter.next();\n  testing.expectEqual(true, val3.done);\n}\n</script>\n\n<div id=\"descendant-container\">\n  <p class=\"desc-text\">Direct child paragraph</p>\n  <div class=\"nested\">\n    <p class=\"desc-text\">Nested paragraph</p>\n    <span>\n      <p class=\"deep\">Deeply nested paragraph</p>\n    </span>\n  </div>\n  <article>\n    <p class=\"desc-text\">Article paragraph</p>\n  </article>\n</div>\n<p class=\"desc-text\">Outside paragraph</p>\n\n<div id=\"complex\">\n  <div class=\"outer\">\n    <div class=\"middle\">\n      <div class=\"inner\">\n        <span id=\"target\">Target span</span>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script>\n  function assertList(expected, result) {\n    testing.expectEqual(expected.length, result.length);\n    testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));\n  }\n</script>\n\n<script id=descendantSelectors>\n{\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], document.querySelectorAll('div p'));\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Article paragraph'], document.querySelectorAll('div .desc-text'));\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], document.querySelectorAll('#descendant-container p'));\n  assertList(['Nested paragraph', 'Deeply nested paragraph'], document.querySelectorAll('div div p'));\n  assertList(['Target span'], document.querySelectorAll('div div div span'));\n  assertList(['Nested paragraph'], document.querySelectorAll('div.nested p.desc-text'));\n  assertList([], document.querySelectorAll('article div p'));\n}\n</script>\n\n<script id=descendantWithWhitespace>\n{\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], document.querySelectorAll('  div   p  '));\n  assertList(['Nested paragraph'], document.querySelectorAll('  div.nested    p.desc-text  '));\n}\n</script>\n\n<script id=multiLevelDescendant>\n{\n  assertList(['Nested paragraph', 'Deeply nested paragraph'], document.querySelectorAll('body div div p'));\n  assertList(['Target span'], document.querySelectorAll('body div div div div span'));\n}\n</script>\n\n<script id=descendantCombinedWithCompound>\n{\n  assertList(['Nested paragraph', 'Deeply nested paragraph'], document.querySelectorAll('.nested p'));\n\n  const bodyPs = document.querySelectorAll('body p');\n  testing.expectEqual(true, bodyPs.length >= 5);\n\n  assertList(['Target span'], document.querySelectorAll('#complex span'));\n  assertList(['Target span'], document.querySelectorAll('div.outer span'));\n  assertList(['Target span'], document.querySelectorAll('.middle .inner span'));\n}\n</script>\n\n<div id=\"combinator-test\">\n  <h1>Title</h1>\n  <p class=\"intro\">First paragraph</p>\n  <p>Second paragraph</p>\n  <div class=\"content\">\n    <span>Span 1</span>\n    <span>Span 2</span>\n    <span>Span 3</span>\n  </div>\n  <p>Third paragraph</p>\n</div>\n\n<script id=childCombinator>\n{\n  assertList(['First paragraph', 'Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test > p'));\n  assertList(['Span 1', 'Span 2', 'Span 3'], document.querySelectorAll('div.content > span'));\n  assertList([], document.querySelectorAll('#combinator-test > span'));\n  assertList(['Title', 'First paragraph', 'Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test > *:not(div)'));\n}\n</script>\n\n<script id=adjacentSiblingCombinator>\n{\n  assertList(['First paragraph'], document.querySelectorAll('h1 + p'));\n  assertList(['Second paragraph'], document.querySelectorAll('p.intro + p'));\n  assertList([], document.querySelectorAll('#combinator-test div + h1'));\n}\n</script>\n\n<script id=generalSiblingCombinator>\n{\n  assertList(['First paragraph', 'Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test h1 ~ p'));\n  assertList(['Second paragraph', 'Third paragraph'], document.querySelectorAll('p.intro ~ p'));\n  assertList([], document.querySelectorAll('#combinator-test p ~ h1'));\n}\n</script>\n\n<script id=combinedCombinators>\n{\n  assertList(['Span 1'], document.querySelectorAll('#combinator-test > div > span:nth-child(1)'));\n  assertList(['Second paragraph'], document.querySelectorAll('#combinator-test h1 + p + p'));\n  assertList(['Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test h1 + p ~ p'));\n}\n</script>\n\n\n<div id=\"pseudo-test\">\n  <ul>\n    <li>Item 1</li>\n    <li>Item 2</li>\n    <li>Item 3</li>\n    <li>Item 4</li>\n    <li>Item 5</li>\n  </ul>\n  <div>\n    <p>Para 1</p>\n    <span>Span</span>\n    <p>Para 2</p>\n  </div>\n</div>\n\n<script id=firstChildPseudo>\n{\n  assertList(['Item 1'], document.querySelectorAll('#pseudo-test li:first-child'));\n  assertList(['Para 1'], document.querySelectorAll('#pseudo-test p:first-child'));\n  assertList([], document.querySelectorAll('#pseudo-test span:first-child'));\n}\n</script>\n\n<script id=lastChildPseudo>\n{\n  assertList(['Item 5'], document.querySelectorAll('#pseudo-test li:last-child'));\n  assertList(['Para 2'], document.querySelectorAll('#pseudo-test p:last-child'));\n}\n</script>\n\n<script id=nthChildPseudo>\n{\n  assertList(['Item 2'], document.querySelectorAll('#pseudo-test li:nth-child(2)'));\n  assertList(['Item 1', 'Item 3', 'Item 5'], document.querySelectorAll('#pseudo-test li:nth-child(odd)'));\n  assertList(['Item 2', 'Item 4'], document.querySelectorAll('#pseudo-test li:nth-child(even)'));\n  assertList(['Item 3', 'Item 5'], document.querySelectorAll('#pseudo-test li:nth-child(2n+3)'));\n}\n</script>\n\n<script id=firstOfTypePseudo>\n{\n  assertList(['Para 1'], document.querySelectorAll('#pseudo-test div > p:first-of-type'));\n  assertList(['Span'], document.querySelectorAll('#pseudo-test div > span:first-of-type'));\n}\n</script>\n\n<script id=lastOfTypePseudo>\n{\n  assertList(['Para 2'], document.querySelectorAll('#pseudo-test div > p:last-of-type'));\n  assertList(['Span'], document.querySelectorAll('#pseudo-test div > span:last-of-type'));\n}\n</script>\n\n<form id=\"form-validity-test\">\n  <input id=\"vi-required-empty\" type=\"text\" required>\n  <input id=\"vi-optional\" type=\"text\">\n  <input id=\"vi-hidden-required\" type=\"hidden\" required>\n  <fieldset id=\"vi-fieldset\">\n    <input id=\"vi-nested-required\" type=\"text\" required>\n    <select id=\"vi-select-required\" required>\n      <option value=\"\">Pick one</option>\n      <option value=\"a\">A</option>\n    </select>\n  </fieldset>\n</form>\n<input id=\"vi-checkbox\" type=\"checkbox\">\n\n<script id=invalidPseudo>\n{\n  // Inputs with required + empty value are :invalid\n  testing.expectEqual(true, document.getElementById('vi-required-empty').matches(':invalid'));\n  testing.expectEqual(false, document.getElementById('vi-required-empty').matches(':valid'));\n\n  // Inputs without required are :valid\n  testing.expectEqual(false, document.getElementById('vi-optional').matches(':invalid'));\n  testing.expectEqual(true, document.getElementById('vi-optional').matches(':valid'));\n\n  // hidden inputs are not candidates for constraint validation\n  testing.expectEqual(false, document.getElementById('vi-hidden-required').matches(':invalid'));\n  testing.expectEqual(false, document.getElementById('vi-hidden-required').matches(':valid'));\n\n  // select with required and empty selected value is :invalid\n  testing.expectEqual(true, document.getElementById('vi-select-required').matches(':invalid'));\n  testing.expectEqual(false, document.getElementById('vi-select-required').matches(':valid'));\n\n  // fieldset containing invalid controls is :invalid\n  testing.expectEqual(true, document.getElementById('vi-fieldset').matches(':invalid'));\n  testing.expectEqual(false, document.getElementById('vi-fieldset').matches(':valid'));\n\n  // form containing invalid controls is :invalid\n  testing.expectEqual(true, document.getElementById('form-validity-test').matches(':invalid'));\n  testing.expectEqual(false, document.getElementById('form-validity-test').matches(':valid'));\n}\n</script>\n\n<script id=validAfterValueSet>\n{\n  // After setting a value, a required input becomes :valid\n  const input = document.getElementById('vi-required-empty');\n  input.value = 'hello';\n  testing.expectEqual(false, input.matches(':invalid'));\n  testing.expectEqual(true, input.matches(':valid'));\n  input.value = '';\n}\n</script>\n\n<script id=indeterminatePseudo>\n{\n  const cb = document.getElementById('vi-checkbox');\n  testing.expectEqual(false, cb.matches(':indeterminate'));\n  cb.indeterminate = true;\n  testing.expectEqual(true, cb.matches(':indeterminate'));\n  cb.indeterminate = false;\n  testing.expectEqual(false, cb.matches(':indeterminate'));\n}\n</script>\n\n<script id=iterator_list_lifetime>\n  // This test is intended to ensure that a list remains alive as long as it\n  // must, i.e. as long as any iterator referencing the list is alive.\n  // This test depends on being able to force the v8 GC to cleanup, which\n  // we have no way of controlling. At worst, the test will pass without\n  // actually testing correct lifetime. But it was at least manually verified\n  // for me that this triggers plenty of GCs.\n  const expected = Array.from(document.querySelectorAll('*')).length;\n  {\n    let keys = [];\n\n    // Phase 1: Create many lists+iterators to fill up the arena pool\n    for (let i = 0; i < 1000; i++) {\n      let list = document.querySelectorAll('*');\n      keys.push(list.keys());\n\n      // Create an Event every iteration to compete for arenas\n      new Event('arena_compete');\n    }\n\n    for (let k of keys) {\n      const result = Array.from(k);\n      testing.expectEqual(expected, result.length);\n    }\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/query_selector_attributes.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\">\n  <div data-test=\"value1\">First</div>\n  <div data-test=\"value2\">Second</div>\n  <input type=\"text\" name=\"username\">\n  <input type=\"password\" name=\"password\">\n  <input type=\"checkbox\" checked>\n  <a href=\"https://example.com\">Link 1</a>\n  <a href=\"/relative\">Link 2</a>\n  <a href=\"https://example.com/page\">Link 3</a>\n  <button disabled>Disabled Button</button>\n  <button>Enabled Button</button>\n  <span class=\"foo bar\">Span 1</span>\n  <span class=\"foo\">Span 2</span>\n  <span class=\"bar\">Span 3</span>\n  <div lang=\"en\">English</div>\n  <div lang=\"en-US\">American English</div>\n  <div lang=\"en-GB\">British English</div>\n  <div lang=\"fr\">French</div>\n  <img src=\"image.png\" alt=\"Test image\">\n  <img src=\"photo.jpg\">\n</div>\n\n<script id=\"presence\">\n  testing.expectEqual(2, document.querySelectorAll('[data-test]').length);\n  testing.expectEqual(3, document.querySelectorAll('[type]').length);\n  testing.expectEqual(1, document.querySelectorAll('[checked]').length);\n  testing.expectEqual(1, document.querySelectorAll('[disabled]').length);\n  testing.expectEqual(1, document.querySelectorAll('[alt]').length);\n\n  const firstDataTest = document.querySelector('[data-test]');\n  testing.expectEqual('First', firstDataTest.innerText);\n</script>\n\n<script id=\"exact\">\n  testing.expectEqual(1, document.querySelectorAll('[data-test=\"value1\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[data-test=\"value2\"]').length);\n  testing.expectEqual(0, document.querySelectorAll('[data-test=\"value3\"]').length);\n\n  testing.expectEqual(1, document.querySelectorAll('[type=\"text\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[type=\"password\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[type=\"checkbox\"]').length);\n\n  const textInput = document.querySelector('[type=\"text\"]');\n  testing.expectEqual('username', textInput.getAttribute('name'));\n</script>\n\n<script id=\"word\">\n  testing.expectEqual(2, document.querySelectorAll('[class~=\"foo\"]').length);\n  testing.expectEqual(2, document.querySelectorAll('[class~=\"bar\"]').length);\n  testing.expectEqual(0, document.querySelectorAll('[class~=\"baz\"]').length);\n\n  const fooEl = document.querySelector('[class~=\"foo\"]');\n  testing.expectEqual('Span 1', fooEl.innerText);\n</script>\n\n<script id=\"prefixDash\">\n  testing.expectEqual(3, document.querySelectorAll('[lang|=\"en\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[lang|=\"fr\"]').length);\n  testing.expectEqual(0, document.querySelectorAll('[lang|=\"es\"]').length);\n\n  const enEl = document.querySelector('[lang|=\"en\"]');\n  testing.expectEqual('English', enEl.innerText);\n</script>\n\n<script id=\"startsWith\">\n  testing.expectEqual(2, document.querySelectorAll('[href^=\"https://\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[href^=\"/\"]').length);\n  testing.expectEqual(2, document.querySelectorAll('[href^=\"https://example.com\"]').length);\n\n  const httpsLink = document.querySelector('[href^=\"https://\"]');\n  testing.expectEqual('Link 1', httpsLink.innerText);\n</script>\n\n<script id=\"endsWith\">\n  testing.expectEqual(1, document.querySelectorAll('[href$=\".com\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[href$=\"/page\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[href$=\"relative\"]').length);\n\n  const comLink = document.querySelector('[href$=\".com\"]');\n  testing.expectEqual('https://example.com', comLink.getAttribute('href'));\n</script>\n\n<script id=\"substring\">\n  testing.expectEqual(2, document.querySelectorAll('[href*=\"example\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[href*=\"relative\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[src*=\"image\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('[src*=\"photo\"]').length);\n\n  const exampleLink = document.querySelector('[href*=\"example\"]');\n  testing.expectEqual('Link 1', exampleLink.innerText);\n</script>\n\n<script id=\"compound\">\n  testing.expectEqual(1, document.querySelectorAll('input[type=\"text\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('input[type=\"password\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('button[disabled]').length);\n  testing.expectEqual(1, document.querySelectorAll('div[lang=\"en\"]').length);\n  testing.expectEqual(1, document.querySelectorAll('div[data-test=\"value1\"]').length);\n\n  const compoundInput = document.querySelector('input[type=\"text\"][name=\"username\"]');\n  testing.expectEqual('username', compoundInput.getAttribute('name'));\n</script>\n\n<script id=\"descendant\">\n  testing.expectEqual(2, document.querySelectorAll('#container [data-test]').length);\n  testing.expectEqual(3, document.querySelectorAll('#container input[type]').length);\n\n  const containerDataTest = document.querySelector('#container [data-test]');\n  testing.expectEqual('First', containerDataTest.innerText);\n</script>\n\n<link rel=\"preload\" as=\"image\" imagesrcset=\"url1.png 1x, url2.png 2x\" id=\"preload-link\">\n\n<script id=\"commaInAttrValue\">\n  // Commas inside quoted attribute values must not be treated as selector separators\n  const el = document.querySelector('link[rel=\"preload\"][as=\"image\"][imagesrcset=\"url1.png 1x, url2.png 2x\"]');\n  testing.expectEqual('preload-link', el.id);\n\n  // Also test with single quotes inside selector\n  const el2 = document.querySelector(\"link[imagesrcset='url1.png 1x, url2.png 2x']\");\n  testing.expectEqual('preload-link', el2.id);\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/query_selector_edge_cases.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"root\">\n  <div id=\"a\" class=\"x\">\n    <p id=\"b\" class=\"y\">Para 1</p>\n    <div id=\"c\" class=\"x y\">\n      <span id=\"d\">Span 1</span>\n      <p id=\"e\" class=\"z\">Para 2</p>\n    </div>\n  </div>\n  <article id=\"f\">\n    <div id=\"g\">\n      <p id=\"h\">Para 3</p>\n    </div>\n  </article>\n</div>\n\n<script>\n  function assertList(expected, result) {\n    testing.expectEqual(expected.length, result.length);\n    testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));\n  }\n</script>\n\n<script id=redundantUniversal>\n{\n  // Universal with ID\n  assertList(['Para 1'], document.querySelectorAll('#b'));\n  assertList(['Span 1'], document.querySelectorAll('#d'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#c *'));\n\n  // Universal with combinators\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#c > *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#b + * *'));\n}\n</script>\n\n<script id=multipleClasses>\n{\n  // Multiple class combinations\n  const yClass = document.querySelectorAll('.y');\n  testing.expectEqual(true, yClass.length >= 1);\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('.x.y *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('.y.x *'));\n}\n</script>\n\n<script id=idEdgeCases>\n{\n  // ID with universal\n  assertList(['Para 1'], document.querySelectorAll('*#b'));\n  assertList(['Para 1'], document.querySelectorAll('#b'));\n\n  // ID with tag mismatch (should return empty)\n  assertList([], document.querySelectorAll('div#b'));\n  assertList([], document.querySelectorAll('span#a'));\n}\n</script>\n\n<script id=complexCombinatorChains>\n{\n  // Long combinator chains\n  assertList(['Span 1'], document.querySelectorAll('div > div > div > span'));\n\n  // Mixed combinators\n  assertList(['Para 2'], document.querySelectorAll('#b + div > p'));\n  assertList(['Para 2'], document.querySelectorAll('#b ~ * > p'));\n  assertList(['Para 2'], document.querySelectorAll('p + div > p'));\n}\n</script>\n\n<script id=descendantWithSiblings>\n{\n  // Descendant followed by sibling\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#a div *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#a p ~ div *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#a p + div *'));\n}\n</script>\n\n<script id=notEdgeCases>\n{\n  // :not() with different selectors\n  const notDivArticle = document.querySelectorAll('*:not(div):not(article)');\n  testing.expectEqual(true, notDivArticle.length >= 3);\n\n  assertList(['Para 3'], document.querySelectorAll('#root p:not(.y):not(.z)'));\n\n  // :not() with universal\n  const allButDiv = document.querySelectorAll('#root *:not(div)');\n  testing.expectEqual(true, allButDiv.length >= 3);\n\n  // :not() with ID\n  const allP = document.querySelectorAll('p:not(#nonexistent)');\n  testing.expectEqual(true, allP.length >= 3);\n  assertList(['Para 1', 'Para 3'], document.querySelectorAll('p:not(#e)'));\n\n  // :not() with class\n  const notClasses = document.querySelectorAll('#root *:not(.x):not(.y):not(.z)');\n  testing.expectEqual(true, notClasses.length >= 2);\n}\n</script>\n\n<script id=pseudoClassCombinations>\n{\n  // First/last child combinations\n  assertList(['Para 1'], document.querySelectorAll('#a > p:first-child'));\n  assertList(['Span 1'], document.querySelectorAll('#c > :first-child'));\n  assertList(['Para 2'], document.querySelectorAll('#c > :last-child'));\n\n  // nth-child with combinators\n  assertList(['Span 1'], document.querySelectorAll('#c > :nth-child(1)'));\n  assertList(['Para 2'], document.querySelectorAll('#c > :nth-child(2)'));\n  assertList(['Span 1'], document.querySelectorAll('#c > :nth-child(odd)'));\n  assertList(['Para 2'], document.querySelectorAll('#c > :nth-child(even)'));\n}\n</script>\n\n<script id=whitespaceMadness>\n{\n  // Excessive whitespace\n  assertList(['Para 2'], document.querySelectorAll('  #a   >   div   >   p  '));\n  assertList(['Para 2'], document.querySelectorAll('  p  +  div  >  p  '));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('   #b   ~   div   *   '));\n}\n</script>\n\n<script id=compoundSelectorMadness>\n{\n  // Very long compound selectors\n  assertList(['Para 1'], document.querySelectorAll('p#b.y'));\n  assertList(['Para 1'], document.querySelectorAll('#b.y'));\n  assertList(['Para 1'], document.querySelectorAll('.y#b'));\n  assertList(['Para 1'], document.querySelectorAll('p.y#b'));\n  assertList(['Para 1'], document.querySelectorAll('*.y#b'));\n  assertList(['Para 1'], document.querySelectorAll('*#b.y'));\n  assertList(['Para 1'], document.querySelectorAll('#b.y'));\n\n  // Multiple classes in different orders\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('.x.y *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('.y.x *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('div.x.y *'));\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('div.y.x *'));\n}\n</script>\n\n<script id=rootBoundaries>\n{\n  // Root itself can match in ancestor searches\n  const a = document.getElementById('a');\n  const divChildren = a.querySelectorAll('div *');\n  testing.expectEqual(true, divChildren.length >= 2);\n\n  const c = document.getElementById('c');\n  const cDivChildren = c.querySelectorAll('div *');\n  // c itself is a div, so its children match\n  testing.expectEqual(2, cDivChildren.length);\n}\n</script>\n\n<script id=siblingEdgeCases>\n{\n  // Sibling selector with descendants\n  assertList(['Span 1', 'Para 2'], document.querySelectorAll('#b + div *'));\n\n  // Multiple sibling hops\n  const root = document.getElementById('root');\n  assertList(['Para 3'], root.querySelectorAll('div ~ article p'));\n  assertList(['Para 3'], root.querySelectorAll('div + article p'));\n}\n</script>\n\n<script id=nonExistentCombinations>\n{\n  // Valid selectors that don't match anything\n  assertList([], document.querySelectorAll('p > div'));\n  assertList([], document.querySelectorAll('span > p'));\n  assertList([], document.querySelectorAll('#a + #b'));\n  assertList([], document.querySelectorAll('#b + #a'));\n  assertList([], document.querySelectorAll('article > article'));\n  assertList([], document.querySelectorAll('span.x'));\n  assertList([], document.querySelectorAll('p#a'));\n  assertList([], document.querySelectorAll('.nonexistent'));\n  assertList([], document.querySelectorAll('#nonexistent'));\n  assertList([], document.querySelectorAll('video'));\n}\n</script>\n\n<script id=universalCombinations>\n{\n  // Universal in different positions\n  const universalDesc = document.querySelectorAll('* * * p');\n  testing.expectEqual(true, universalDesc.length >= 2);\n\n  const universalChild = document.querySelectorAll('* > * > * > *');\n  testing.expectEqual(true, universalChild.length >= 1);\n\n  const mixedUniversal = document.querySelectorAll('* > * > p');\n  testing.expectEqual(true, mixedUniversal.length >= 1);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/query_selector_not.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"root\">\n  <div id=\"container\" class=\"parent\">\n    <p id=\"p1\" class=\"text\">Paragraph 1</p>\n    <p id=\"p2\" class=\"text highlight\">Paragraph 2</p>\n    <div id=\"nested\">\n      <span id=\"s1\" class=\"item\">Span 1</span>\n      <span id=\"s2\" class=\"item active\">Span 2</span>\n      <a id=\"link1\" href=\"#\">Link 1</a>\n    </div>\n    <article id=\"article1\">\n      <p id=\"p3\">Article paragraph</p>\n      <span id=\"s3\">Article span</span>\n    </article>\n  </div>\n</div>\n\n<script id=basicNot>\n{\n  const notDiv = document.querySelectorAll('#root *:not(div)');\n  testing.expectEqual(true, notDiv.length >= 7);\n\n  const notClass = document.querySelectorAll('#container *:not(.text)');\n  testing.expectEqual(true, notClass.length >= 5);\n\n  const notId = document.querySelectorAll('#container p:not(#p2)');\n  testing.expectEqual(2, notId.length);\n  testing.expectEqual('p1', notId[0].id);\n  testing.expectEqual('p3', notId[1].id);\n}\n</script>\n\n<script id=notWithCombinators>\n{\n  const notDirectChild = document.querySelectorAll('span:not(#nested > span)');\n  testing.expectEqual(1, notDirectChild.length);\n  testing.expectEqual('s3', notDirectChild[0].id);\n\n  const notDescendant = document.querySelectorAll('#container > *:not(div)');\n  testing.expectEqual(3, notDescendant.length);\n}\n</script>\n\n<script id=notWithMultipleSelectors>\n{\n  const notDivOrP = document.querySelectorAll('#container *:not(div, p)');\n  testing.expectEqual(5, notDivOrP.length);\n\n  const notTextOrItem = document.querySelectorAll('#container *:not(.text, .item)');\n  testing.expectEqual(true, notTextOrItem.length >= 3);\n\n  const notIdOrClass = document.querySelectorAll('p:not(#p1, .highlight)');\n  testing.expectEqual(1, notIdOrClass.length);\n  testing.expectEqual('p3', notIdOrClass[0].id);\n}\n</script>\n\n<script id=notWithAttributes>\n{\n  const notWithHref = document.querySelectorAll('#nested *:not([href])');\n  testing.expectEqual(2, notWithHref.length);\n\n  const notLinkWithHref = document.querySelectorAll('#nested *:not(a[href])');\n  testing.expectEqual(2, notLinkWithHref.length);\n}\n</script>\n\n<script id=notWithPseudoClasses>\n{\n  const notFirstChild = document.querySelectorAll('#nested *:not(:first-child)');\n  testing.expectEqual(2, notFirstChild.length);\n\n  const notLastChild = document.querySelectorAll('#nested *:not(:last-child)');\n  testing.expectEqual(2, notLastChild.length);\n}\n</script>\n\n<script id=nestedNot>\n{\n  // :not(:not(...)) matches elements that DO match the inner selector\n  const items = document.querySelectorAll('.item:not(:not(.active))');\n  testing.expectEqual(1, items.length);\n  testing.expectEqual('s2', items[0].id);\n}\n</script>\n\n<script id=complexCombinations>\n{\n  const complex1 = document.querySelectorAll('p:not(#nested p, .highlight)');\n  testing.expectEqual(2, complex1.length);\n  testing.expectEqual('p1', complex1[0].id);\n  testing.expectEqual('p3', complex1[1].id);\n\n  const complex2 = document.querySelectorAll('#container > *:not(div)');\n  testing.expectEqual(3, complex2.length);\n}\n</script>\n\n<script id=notWithUniversal>\n{\n  const notUniversal = document.querySelectorAll('#nested :not(*)');\n  testing.expectEqual(0, notUniversal.length);\n}\n</script>\n\n<script id=edgeCases>\n{\n  const multiNot = document.querySelectorAll('*:not(div):not(p):not(article)');\n  testing.expectEqual(true, multiNot.length >= 3);\n\n  const notAtEnd = document.querySelectorAll('#container p:not(.highlight)');\n  testing.expectEqual(2, notAtEnd.length);\n\n  const notAtStart = document.querySelectorAll(':not(div) > span');\n  testing.expectEqual(1, notAtStart.length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/replace_children.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<head>\n  <title>document.replaceChildren Tests</title>\n</head>\n<body>\n  <div id=\"test\">Original content</div>\n</body>\n\n<script id=error_multiple_elements>\n  {\n    // Test that we cannot have more than one Element child\n    const doc = new Document();\n    const div1 = doc.createElement('div');\n    const div2 = doc.createElement('div');\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.replaceChildren(div1, div2);\n    });\n  }\n</script>\n\n<script id=error_multiple_elements_via_fragment>\n  {\n    // Test that we cannot have more than one Element child via DocumentFragment\n    const doc = new Document();\n    const fragment = doc.createDocumentFragment();\n    fragment.appendChild(doc.createElement('div'));\n    fragment.appendChild(doc.createElement('span'));\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.replaceChildren(fragment);\n    });\n  }\n</script>\n\n<script id=error_multiple_doctypes>\n  {\n    // Test that we cannot have more than one DocumentType child\n    const doc = new Document();\n    const doctype1 = doc.implementation.createDocumentType('html', '', '');\n    const doctype2 = doc.implementation.createDocumentType('html', '', '');\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.replaceChildren(doctype1, doctype2);\n    });\n  }\n</script>\n\n<script id=error_text_node>\n  {\n    // Test that we cannot insert Text nodes directly into Document\n    const doc = new Document();\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.replaceChildren('Just text');\n    });\n  }\n</script>\n\n<script id=error_text_with_element>\n  {\n    // Test that we cannot insert Text nodes even with valid Element\n    const doc = new Document();\n    const html = doc.createElement('html');\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.replaceChildren('Text 1', html, 'Text 2');\n    });\n  }\n</script>\n\n<script id=error_append_multiple_elements>\n  {\n    // Test that append also validates\n    const doc = new Document();\n    doc.append(doc.createElement('html'));\n\n    const div = doc.createElement('div');\n    testing.expectError('HierarchyRequest', () => {\n      doc.append(div);\n    });\n  }\n</script>\n\n<script id=error_prepend_multiple_elements>\n  {\n    // Test that prepend also validates\n    const doc = new Document();\n    doc.prepend(doc.createElement('html'));\n\n    const div = doc.createElement('div');\n    testing.expectError('HierarchyRequest', () => {\n      doc.prepend(div);\n    });\n  }\n</script>\n\n<script id=error_append_text>\n  {\n    // Test that append rejects text nodes\n    const doc = new Document();\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.append('text');\n    });\n  }\n</script>\n\n<script id=error_prepend_text>\n  {\n    // Test that prepend rejects text nodes\n    const doc = new Document();\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.prepend('text');\n    });\n  }\n</script>\n\n<script id=replace_with_single_element>\n  {\n    const doc = new Document();\n    const html = doc.createElement('html');\n    html.id = 'replaced';\n    html.textContent = 'New content';\n\n    doc.replaceChildren(html);\n\n    testing.expectEqual(1, doc.childNodes.length);\n    testing.expectEqual(html, doc.firstChild);\n    testing.expectEqual('replaced', doc.firstChild.id);\n  }\n</script>\n\n<script id=replace_with_comments>\n  {\n    const doc = new Document();\n    const comment1 = doc.createComment('Comment 1');\n    const html = doc.createElement('html');\n    const comment2 = doc.createComment('Comment 2');\n\n    doc.replaceChildren(comment1, html, comment2);\n\n    testing.expectEqual(3, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.firstChild.nodeName);\n    testing.expectEqual('Comment 1', doc.firstChild.textContent);\n    testing.expectEqual('html', doc.childNodes[1].nodeName);\n    testing.expectEqual('#comment', doc.lastChild.nodeName);\n    testing.expectEqual('Comment 2', doc.lastChild.textContent);\n  }\n</script>\n\n<script id=replace_with_empty>\n  {\n    const doc = new Document();\n    // First add some content\n    const div = doc.createElement('div');\n    doc.replaceChildren(div);\n    testing.expectEqual(1, doc.childNodes.length);\n\n    // Now replace with nothing\n    doc.replaceChildren();\n\n    testing.expectEqual(0, doc.childNodes.length);\n    testing.expectEqual(null, doc.firstChild);\n    testing.expectEqual(null, doc.lastChild);\n  }\n</script>\n\n<script id=replace_removes_old_children>\n  {\n    const doc = new Document();\n    const comment1 = doc.createComment('old');\n\n    doc.replaceChildren(comment1);\n    testing.expectEqual(1, doc.childNodes.length);\n    testing.expectEqual(doc, comment1.parentNode);\n\n    const html = doc.createElement('html');\n    html.id = 'new';\n\n    doc.replaceChildren(html);\n\n    // Old child should be removed\n    testing.expectEqual(null, comment1.parentNode);\n    testing.expectEqual(1, doc.childNodes.length);\n    testing.expectEqual('new', doc.firstChild.id);\n  }\n</script>\n\n<script id=replace_with_document_fragment_valid>\n  {\n    const doc = new Document();\n    const fragment = doc.createDocumentFragment();\n    const html = doc.createElement('html');\n    const comment = doc.createComment('comment');\n\n    fragment.appendChild(comment);\n    fragment.appendChild(html);\n\n    doc.replaceChildren(fragment);\n\n    // Fragment contents should be moved\n    testing.expectEqual(2, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.firstChild.nodeName);\n    testing.expectEqual('html', doc.lastChild.nodeName);\n\n    // Fragment should be empty now\n    testing.expectEqual(0, fragment.childNodes.length);\n  }\n</script>\n\n<script id=replace_maintains_child_order>\n  {\n    const doc = new Document();\n    const nodes = [];\n\n    // Document can have: comment, processing instruction, doctype, element\n    nodes.push(doc.createComment('comment'));\n    nodes.push(doc.createElement('html'));\n\n    doc.replaceChildren(...nodes);\n\n    testing.expectEqual(2, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.childNodes[0].nodeName);\n    testing.expectEqual('html', doc.childNodes[1].nodeName);\n  }\n</script>\n\n<script id=replace_with_nested_structure>\n  {\n    const doc = new Document();\n    const outer = doc.createElement('html');\n    outer.id = 'outer';\n    const middle = doc.createElement('body');\n    middle.id = 'middle';\n    const inner = doc.createElement('span');\n    inner.id = 'inner';\n    inner.textContent = 'Nested';\n\n    middle.appendChild(inner);\n    outer.appendChild(middle);\n\n    doc.replaceChildren(outer);\n\n    testing.expectEqual(1, doc.childNodes.length);\n    testing.expectEqual('outer', doc.firstChild.id);\n\n    const foundInner = doc.getElementById('inner');\n    testing.expectEqual(inner, foundInner);\n    testing.expectEqual('Nested', foundInner.textContent);\n  }\n</script>\n\n<script id=consecutive_replaces>\n  {\n    const doc = new Document();\n    const html1 = doc.createElement('html');\n    html1.id = 'first-replace';\n    doc.replaceChildren(html1);\n    testing.expectEqual('first-replace', doc.firstChild.id);\n\n    // Replace element with comments\n    const comment = doc.createComment('in between');\n    doc.replaceChildren(comment);\n    testing.expectEqual(1, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.firstChild.nodeName);\n\n    // Replace comments with new element\n    const html2 = doc.createElement('html');\n    html2.id = 'second-replace';\n    doc.replaceChildren(html2);\n    testing.expectEqual('second-replace', doc.firstChild.id);\n    testing.expectEqual(1, doc.childNodes.length);\n\n    // First element should no longer be in document\n    testing.expectEqual(null, html1.parentNode);\n    testing.expectEqual(null, comment.parentNode);\n  }\n</script>\n\n<script id=replace_with_comments_only>\n  {\n    const doc = new Document();\n    const comment1 = doc.createComment('First');\n    const comment2 = doc.createComment('Second');\n\n    doc.replaceChildren(comment1, comment2);\n\n    testing.expectEqual(2, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.firstChild.nodeName);\n    testing.expectEqual('First', doc.firstChild.textContent);\n    testing.expectEqual('#comment', doc.lastChild.nodeName);\n    testing.expectEqual('Second', doc.lastChild.textContent);\n  }\n</script>\n\n<script id=error_fragment_with_text>\n  {\n    // DocumentFragment with text should fail when inserted into Document\n    const doc = new Document();\n    const fragment = doc.createDocumentFragment();\n    fragment.appendChild(doc.createTextNode('text'));\n    fragment.appendChild(doc.createElement('html'));\n\n    testing.expectError('HierarchyRequest', () => {\n      doc.replaceChildren(fragment);\n    });\n  }\n</script>\n\n<script id=append_valid_nodes>\n  {\n    const doc = new Document();\n    const comment = doc.createComment('test');\n    const html = doc.createElement('html');\n\n    doc.append(comment);\n    testing.expectEqual(1, doc.childNodes.length);\n\n    doc.append(html);\n    testing.expectEqual(2, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.firstChild.nodeName);\n    testing.expectEqual('html', doc.lastChild.nodeName);\n  }\n</script>\n\n<script id=prepend_valid_nodes>\n  {\n    const doc = new Document();\n    const html = doc.createElement('html');\n    const comment = doc.createComment('test');\n\n    doc.prepend(html);\n    testing.expectEqual(1, doc.childNodes.length);\n\n    doc.prepend(comment);\n    testing.expectEqual(2, doc.childNodes.length);\n    testing.expectEqual('#comment', doc.firstChild.nodeName);\n    testing.expectEqual('html', doc.lastChild.nodeName);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/document/write.html",
    "content": "<!DOCTYPE html>\n<head>\n  <title>document.write Tests</title>\n  <script src=\"../testing.js\"></script>\n</head>\n\n<body>\n\n<!-- Phase 1 Tests: Basic HTML (no scripts) -->\n\n<script id=basic_write_and_verify>\n  document.write('<h1 id=\"written\">Hello</h1>');\n  // Add a simple assertion so the test framework doesn't complain\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_basic>\n  const written = document.getElementById('written');\n  testing.expectEqual('Hello', written.textContent);\n  testing.expectEqual('H1', written.tagName);\n</script>\n\n<script id=multiple_writes>\n  document.write('<div id=\"a\">A</div>');\n  document.write('<div id=\"b\">B</div>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_multiple>\n  const a = document.getElementById('a');\n  const b = document.getElementById('b');\n  testing.expectEqual('A', a.textContent);\n  testing.expectEqual('B', b.textContent);\n\n  // Verify they're siblings in the correct order\n  testing.expectEqual(b, a.nextElementSibling);\n</script>\n\n<script id=write_with_attributes>\n  document.write('<div id=\"styled\" class=\"foo bar\" data-value=\"123\">Content</div>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_attributes>\n  const el = document.getElementById('styled');\n  testing.expectEqual('foo bar', el.className);\n  testing.expectEqual('123', el.getAttribute('data-value'));\n  testing.expectEqual('Content', el.textContent);\n</script>\n\n<script id=write_multiple_elements>\n  document.write('<p id=\"p1\">First</p><p id=\"p2\">Second</p>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_multiple_elements>\n  const p1 = document.getElementById('p1');\n  const p2 = document.getElementById('p2');\n  testing.expectEqual('First', p1.textContent);\n  testing.expectEqual('Second', p2.textContent);\n  testing.expectEqual(p2, p1.nextElementSibling);\n</script>\n\n<script id=write_nested_elements>\n  document.write('<div id=\"outer\"><span id=\"inner\">Nested</span></div>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_nested>\n  const outer = document.getElementById('outer');\n  const inner = document.getElementById('inner');\n  testing.expectEqual(outer, inner.parentElement);\n  testing.expectEqual('Nested', inner.textContent);\n</script>\n\n<!-- Phase 2 Tests: Script execution -->\n\n<script id=write_script>\n  document.write('<script id=\"written_script\">window.executed = true; testing.expectEqual(true, true);<\\/script>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_script_executed>\n  testing.expectEqual(true, window.executed);\n  testing.expectEqual(document.getElementById('written_script').tagName, 'SCRIPT');\n</script>\n\n<script id=written_script_can_write>\n  document.write('<script id=\"nested_writer\">document.write(\"<div id=\\\\\"nested\\\\\">OK</div>\"); testing.expectEqual(true, true);<\\/script>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_nested_write>\n  const nested = document.getElementById('nested');\n  testing.expectEqual('OK', nested.textContent);\n</script>\n\n<div id=\"before_script\">Before</div>\n<script id=written_script_sees_dom>\n  document.write('<script id=\"dom_accessor\">document.getElementById(\"before_script\").setAttribute(\"data-modified\", \"yes\"); testing.expectEqual(true, true);<\\/script>');\n  testing.expectEqual(true, true);\n</script>\n\n<script id=verify_dom_modification>\n  const beforeScript = document.getElementById('before_script');\n  testing.expectEqual('yes', beforeScript.getAttribute('data-modified'));\n</script>\n\n<!-- Phase 3 Tests: document.open/close would go here -->\n<!-- Note: Testing document.open/close requires async/setTimeout which doesn't -->\n<!-- work well with the test isolation. The implementation is tested manually. -->\n\n<script id=final_assertion>\n  // Just verify the methods exist\n  testing.expectEqual('function', typeof document.open);\n  testing.expectEqual('function', typeof document.close);\n  testing.expectEqual('function', typeof document.write);\n</script>\n\n<!-- Phase 3 Tests: document.open/close (post-parsing with setTimeout) -->\n\n<div id=\"will_be_removed\">This will be removed by document.open()</div>\n\n<script id=test_open_close_async>\n  // Mark that we saw the element before\n  const sawBefore = document.getElementById('will_be_removed') !== null;\n  testing.expectEqual(true, sawBefore);\n\n  // Use setTimeout to ensure we're after parsing completes\n  setTimeout(() => {\n    document.open();\n  }, 5);\n\n  testing.eventually(() => {\n    // The element should be gone now\n    const afterOpen = document.getElementById('will_be_removed');\n    testing.expectEqual(null, afterOpen);\n\n    // Write new content\n    document.write('<html><body>');\n    document.write('<h1 id=\"new_content\">Replaced</h1>');\n    document.write('</body></html>');\n\n    // Close the document\n    document.close();\n\n    // Verify new content exists\n    const newContent = document.getElementById('new_content');\n    testing.expectEqual('Replaced', newContent.textContent);\n  })\n</script>\n\n<script>\n  // doing this after test_open_close_async used to crash, so we keep it\n  // to make sure it doesn't\n  setTimeout(() => {\n    document.open();\n    document.close();\n  }, 20);\n</script>\n</body>\n"
  },
  {
    "path": "src/browser/tests/document_fragment/document_fragment.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<body></body>\n\n<script id=document_fragment>\n  {\n    const df = new DocumentFragment();\n    testing.expectEqual(\"DocumentFragment\", df.constructor.name);\n    testing.expectEqual(\"#document-fragment\", df.nodeName);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    testing.expectEqual(\"DocumentFragment\", df.constructor.name);\n    testing.expectEqual(\"#document-fragment\", df.nodeName);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    const div1 = document.createElement(\"div\");\n    div1.textContent = \"First\";\n    const div2 = document.createElement(\"div\");\n    div2.textContent = \"Second\";\n\n    df.appendChild(div1);\n    df.appendChild(div2);\n\n    testing.expectEqual(2, df.childElementCount);\n    testing.expectEqual(div1, df.firstElementChild);\n    testing.expectEqual(div2, df.lastElementChild);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    df.append(\"text1\", document.createElement(\"span\"), \"text2\");\n    testing.expectEqual(3, df.childNodes.length);\n    testing.expectEqual(1, df.childElementCount);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    df.append(\"last\");\n    df.prepend(\"first\");\n    testing.expectEqual(\"#text\", df.firstChild.nodeName);\n    testing.expectEqual(\"first\", df.firstChild.textContent);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    const p = document.createElement(\"p\");\n    p.className = \"test\";\n    p.textContent = \"Hello\";\n    df.appendChild(p);\n\n    const found = df.querySelector(\".test\");\n    testing.expectEqual(p, found);\n    testing.expectEqual(\"Hello\", found.textContent);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    df.appendChild(document.createElement(\"div\"));\n    df.appendChild(document.createElement(\"div\"));\n    testing.expectEqual(2, df.childElementCount);\n\n    df.replaceChildren(document.createElement(\"span\"));\n    testing.expectEqual(1, df.childElementCount);\n    testing.expectEqual(\"SPAN\", df.firstElementChild.tagName);\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    const outer = document.createElement(\"div\");\n    outer.id = \"outer\";\n    const inner = document.createElement(\"div\");\n    inner.id = \"inner\";\n    outer.appendChild(inner);\n    df.appendChild(outer);\n\n    testing.expectEqual(outer, df.getElementById(\"outer\"));\n    testing.expectEqual(inner, df.getElementById(\"inner\"));\n    testing.expectEqual(null, df.getElementById(\"notfound\"));\n  }\n\n  {\n    const df = document.createDocumentFragment();\n    const test1 = document.createElement(\"div\");\n    test1.id = \"test1\";\n    const test2 = document.createElement(\"div\");\n    test2.id = \"test2\";\n\n    df.appendChild(test1);\n    df.appendChild(test2);\n    testing.expectEqual(2, df.childElementCount);\n\n    document.body.appendChild(df);\n    testing.expectEqual(0, df.childElementCount);\n    testing.expectEqual(test1, document.getElementById(\"test1\"));\n    testing.expectEqual(test2, document.getElementById(\"test2\"));\n  }\n</script>\n\n<script id=cloneNode_shallow>\n{\n  const df = document.createDocumentFragment();\n  const div1 = document.createElement(\"div\");\n  div1.textContent = \"Content 1\";\n  const div2 = document.createElement(\"div\");\n  div2.textContent = \"Content 2\";\n  df.appendChild(div1);\n  df.appendChild(div2);\n\n  const clone = df.cloneNode(false);\n  testing.expectEqual(\"DocumentFragment\", clone.constructor.name);\n  testing.expectEqual(0, clone.childElementCount);\n  testing.expectEqual(2, df.childElementCount);\n}\n</script>\n\n<script id=cloneNode_deep>\n{\n  const df = document.createDocumentFragment();\n  const div1 = document.createElement(\"div\");\n  div1.id = \"original1\";\n  div1.className = \"test\";\n  div1.textContent = \"Content 1\";\n  const div2 = document.createElement(\"div\");\n  div2.id = \"original2\";\n  div2.textContent = \"Content 2\";\n  df.appendChild(div1);\n  df.appendChild(div2);\n\n  const clone = df.cloneNode(true);\n  testing.expectEqual(\"DocumentFragment\", clone.constructor.name);\n  testing.expectEqual(2, clone.childElementCount);\n\n  const clonedDiv1 = clone.firstElementChild;\n  testing.expectEqual(\"DIV\", clonedDiv1.tagName);\n  testing.expectEqual(\"original1\", clonedDiv1.id);\n  testing.expectEqual(\"test\", clonedDiv1.className);\n  testing.expectEqual(\"Content 1\", clonedDiv1.textContent);\n\n  const clonedDiv2 = clone.lastElementChild;\n  testing.expectEqual(\"original2\", clonedDiv2.id);\n  testing.expectEqual(\"Content 2\", clonedDiv2.textContent);\n\n  testing.expectFalse(div1.isSameNode(clonedDiv1));\n  testing.expectFalse(div2.isSameNode(clonedDiv2));\n\n  testing.expectEqual(2, df.childElementCount);\n}\n</script>\n\n<script id=cloneNode_nested>\n{\n  const df = document.createDocumentFragment();\n  const outer = document.createElement(\"div\");\n  outer.id = \"outer\";\n  const inner = document.createElement(\"span\");\n  inner.id = \"inner\";\n  inner.textContent = \"Nested content\";\n  outer.appendChild(inner);\n  df.appendChild(outer);\n\n  const clone = df.cloneNode(true);\n  testing.expectEqual(1, clone.childElementCount);\n\n  const clonedOuter = clone.firstElementChild;\n  testing.expectEqual(\"outer\", clonedOuter.id);\n  testing.expectEqual(1, clonedOuter.childElementCount);\n\n  const clonedInner = clonedOuter.firstElementChild;\n  testing.expectEqual(\"SPAN\", clonedInner.tagName);\n  testing.expectEqual(\"inner\", clonedInner.id);\n  testing.expectEqual(\"Nested content\", clonedInner.textContent);\n}\n</script>\n\n<script id=textContent>\n{\n  const df = document.createDocumentFragment();\n  df.appendChild(document.createElement(\"div\"));\n  testing.expectEqual(1, df.childNodes.length);\n  df.textContent = '';\n  testing.expectEqual(0, df.childNodes.length);\n\n  df.textContent = ' ';\n  testing.expectEqual(1, df.childNodes.length);\n}\n</script>\n\n<script id=isEqualNode>\n  testing.expectEqual('DocumentFragment', new DocumentFragment().constructor.name);\n\n  const dc1 = new DocumentFragment();\n  testing.expectEqual(true, dc1.isEqualNode(dc1))\n\n  const dc2 = new DocumentFragment();\n  testing.expectEqual(true, dc1.isEqualNode(dc2))\n</script>\n"
  },
  {
    "path": "src/browser/tests/document_fragment/insertion.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\"></div>\n\n<script id=\"appendChildBasic\">\n{\n  // DocumentFragment children should be moved, fragment itself should not appear\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const fragment = document.createDocumentFragment();\n  const span1 = document.createElement('span');\n  span1.textContent = 'A';\n  const span2 = document.createElement('span');\n  span2.textContent = 'B';\n\n  fragment.appendChild(span1);\n  fragment.appendChild(span2);\n\n  // Fragment should have 2 children before insertion\n  testing.expectEqual(2, fragment.childNodes.length);\n\n  container.appendChild(fragment);\n\n  // After insertion:\n  // 1. Container should have 2 children (the spans), not 1 (the fragment)\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('SPAN', container.childNodes[0].tagName);\n  testing.expectEqual('SPAN', container.childNodes[1].tagName);\n  testing.expectEqual('A', container.childNodes[0].textContent);\n  testing.expectEqual('B', container.childNodes[1].textContent);\n\n  // 2. Fragment should be empty after insertion\n  testing.expectEqual(0, fragment.childNodes.length);\n\n  // 3. No DocumentFragment should appear in the tree\n  testing.expectEqual('SPAN', container.firstChild.tagName);\n  testing.expectEqual('SPAN', container.firstChild.nodeName);\n}\n</script>\n\n<script id=\"insertBeforeBasic\">\n{\n  // insertBefore with DocumentFragment\n  const container = $('#container');\n  container.innerHTML = '<div id=\"ref\">REF</div>';\n\n  const fragment = document.createDocumentFragment();\n  const span1 = document.createElement('span');\n  span1.textContent = 'A';\n  const span2 = document.createElement('span');\n  span2.textContent = 'B';\n\n  fragment.appendChild(span1);\n  fragment.appendChild(span2);\n\n  const ref = $('#ref');\n  container.insertBefore(fragment, ref);\n\n  // Container should have: span1, span2, ref (3 children)\n  testing.expectEqual(3, container.childNodes.length);\n  testing.expectEqual('SPAN', container.childNodes[0].tagName);\n  testing.expectEqual('A', container.childNodes[0].textContent);\n  testing.expectEqual('SPAN', container.childNodes[1].tagName);\n  testing.expectEqual('B', container.childNodes[1].textContent);\n  testing.expectEqual('DIV', container.childNodes[2].tagName);\n  testing.expectEqual('REF', container.childNodes[2].textContent);\n\n  // Fragment should be empty\n  testing.expectEqual(0, fragment.childNodes.length);\n}\n</script>\n\n<script id=\"insertBeforeNull\">\n{\n  // insertBefore with null ref node should behave like appendChild\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const fragment = document.createDocumentFragment();\n  const span = document.createElement('span');\n  span.textContent = 'TEST';\n  fragment.appendChild(span);\n\n  container.insertBefore(fragment, null);\n\n  testing.expectEqual(1, container.childNodes.length);\n  testing.expectEqual('SPAN', container.childNodes[0].tagName);\n  testing.expectEqual(0, fragment.childNodes.length);\n}\n</script>\n\n<script id=\"replaceChildWithFragment\">\n{\n  // replaceChild with DocumentFragment\n  const container = $('#container');\n  container.innerHTML = '<div id=\"old\">OLD</div>';\n\n  const fragment = document.createDocumentFragment();\n  const span1 = document.createElement('span');\n  span1.textContent = 'A';\n  const span2 = document.createElement('span');\n  span2.textContent = 'B';\n  fragment.appendChild(span1);\n  fragment.appendChild(span2);\n\n  const old = $('#old');\n  container.replaceChild(fragment, old);\n\n  // Container should have 2 children (the fragment's children)\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('SPAN', container.childNodes[0].tagName);\n  testing.expectEqual('A', container.childNodes[0].textContent);\n  testing.expectEqual('SPAN', container.childNodes[1].tagName);\n  testing.expectEqual('B', container.childNodes[1].textContent);\n\n  // Old node should not be in container\n  testing.expectEqual(null, old.parentNode);\n}\n</script>\n\n<script id=\"emptyFragment\">\n{\n  // Empty DocumentFragment should not add any nodes\n  const container = $('#container');\n  container.innerHTML = '<span>TEST</span>';\n\n  const fragment = document.createDocumentFragment();\n  container.appendChild(fragment);\n\n  // Should still have just 1 child\n  testing.expectEqual(1, container.childNodes.length);\n  testing.expectEqual('SPAN', container.childNodes[0].tagName);\n}\n</script>\n\n<script id=\"fragmentReuse\">\n{\n  // DocumentFragment can be reused after its children are moved\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const fragment = document.createDocumentFragment();\n  const span1 = document.createElement('span');\n  span1.textContent = 'A';\n  fragment.appendChild(span1);\n\n  container.appendChild(fragment);\n  testing.expectEqual(1, container.childNodes.length);\n  testing.expectEqual(0, fragment.childNodes.length);\n\n  // Reuse the same fragment\n  const span2 = document.createElement('span');\n  span2.textContent = 'B';\n  fragment.appendChild(span2);\n\n  container.appendChild(fragment);\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual('A', container.childNodes[0].textContent);\n  testing.expectEqual('B', container.childNodes[1].textContent);\n}\n</script>\n\n<script id=\"nestedFragments\">\n{\n  // DocumentFragment containing another DocumentFragment\n  // (though this is unusual, the inner fragment should also be unwrapped)\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const outer = document.createDocumentFragment();\n  const inner = document.createDocumentFragment();\n\n  const span = document.createElement('span');\n  span.textContent = 'TEST';\n  inner.appendChild(span);\n\n  // Appending inner fragment to outer should move span to outer\n  outer.appendChild(inner);\n  testing.expectEqual(1, outer.childNodes.length);\n  testing.expectEqual('SPAN', outer.childNodes[0].tagName);\n  testing.expectEqual(0, inner.childNodes.length);\n\n  // Now append outer to container\n  container.appendChild(outer);\n  testing.expectEqual(1, container.childNodes.length);\n  testing.expectEqual('SPAN', container.childNodes[0].tagName);\n  testing.expectEqual(0, outer.childNodes.length);\n}\n</script>\n\n<script id=\"fragmentWithMixedContent\">\n{\n  // DocumentFragment with text nodes, comments, and elements\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const fragment = document.createDocumentFragment();\n  fragment.appendChild(document.createTextNode('Text1'));\n  fragment.appendChild(document.createComment('comment'));\n  const div = document.createElement('div');\n  div.textContent = 'Div';\n  fragment.appendChild(div);\n  fragment.appendChild(document.createTextNode('Text2'));\n\n  testing.expectEqual(4, fragment.childNodes.length);\n\n  container.appendChild(fragment);\n\n  // All 4 nodes should be in container\n  testing.expectEqual(4, container.childNodes.length);\n  testing.expectEqual(3, container.childNodes[0].nodeType); // TEXT_NODE\n  testing.expectEqual(8, container.childNodes[1].nodeType); // COMMENT_NODE\n  testing.expectEqual(1, container.childNodes[2].nodeType); // ELEMENT_NODE\n  testing.expectEqual(3, container.childNodes[3].nodeType); // TEXT_NODE\n\n  testing.expectEqual(0, fragment.childNodes.length);\n}\n</script>\n\n<script id=\"innerHTML\">\n{\n  // After a DocumentFragment is inserted, innerHTML should not show it\n  const container = $('#container');\n  container.innerHTML = '';\n\n  const fragment = document.createDocumentFragment();\n  const comment = document.createComment('test');\n  fragment.appendChild(comment);\n\n  container.appendChild(fragment);\n\n  // Should only see the comment, not a DocumentFragment wrapper\n  const html = container.innerHTML;\n  testing.expectEqual('<!--test-->', html);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/document_head_body.html",
    "content": "<head id=\"the_head\">\n  <script src=\"testing.js\"></script>\n</head>\n<body id=\"the_body\">\n  <script id=\"test-document-head-body\">\n    testing.expectEqual(document.getElementById('the_head'), document.head);\n    testing.expectEqual(document.getElementById('the_body'), document.body);\n  </script>\n</body>"
  },
  {
    "path": "src/browser/tests/domexception.html",
    "content": "<!DOCTYPE html>\n<head>\n  <title>DOMException Test</title>\n  <script src=\"testing.js\"></script>\n</head>\n\n<body>\n</body>\n\n<script id=constructor_no_args>\n  {\n    const ex = new DOMException();\n    testing.expectEqual('Error', ex.toString());\n    testing.expectEqual('Error', ex.name);\n    testing.expectEqual('', ex.message);\n    testing.expectEqual(0, ex.code);\n  }\n</script>\n\n<script id=constructor_with_message>\n  {\n    const ex = new DOMException('Something went wrong');\n    testing.expectEqual('Error', ex.name);\n    testing.expectEqual('Something went wrong', ex.message);\n    testing.expectEqual(0, ex.code);\n  }\n</script>\n\n<script id=constructor_with_message_and_name>\n  {\n    const ex = new DOMException('Custom error message', 'NotFoundError');\n    testing.expectEqual('NotFoundError', ex.name);\n    testing.expectEqual('Custom error message', ex.message);\n    testing.expectEqual(8, ex.code);\n  }\n</script>\n\n<script id=legacy_error_codes>\n  {\n    // Test standard errors with legacy codes\n    const errors = [\n      { name: 'IndexSizeError', code: 1 },\n      { name: 'HierarchyRequestError', code: 3 },\n      { name: 'WrongDocumentError', code: 4 },\n      { name: 'InvalidCharacterError', code: 5 },\n      { name: 'NoModificationAllowedError', code: 7 },\n      { name: 'NotFoundError', code: 8 },\n      { name: 'NotSupportedError', code: 9 },\n      { name: 'InUseAttributeError', code: 10 },\n      { name: 'InvalidStateError', code: 11 },\n      { name: 'SyntaxError', code: 12 },\n      { name: 'InvalidModificationError', code: 13 },\n      { name: 'NamespaceError', code: 14 },\n      { name: 'InvalidAccessError', code: 15 },\n      { name: 'SecurityError', code: 18 },\n      { name: 'NetworkError', code: 19 },\n      { name: 'AbortError', code: 20 },\n      { name: 'URLMismatchError', code: 21 },\n      { name: 'QuotaExceededError', code: 22 },\n      { name: 'TimeoutError', code: 23 },\n      { name: 'InvalidNodeTypeError', code: 24 },\n      { name: 'DataCloneError', code: 25 },\n    ];\n\n    for (const { name, code } of errors) {\n      const ex = new DOMException('test', name);\n      testing.expectEqual(name, ex.name);\n      testing.expectEqual(code, ex.code);\n      testing.expectEqual('test', ex.message);\n    }\n  }\n</script>\n\n<script id=custom_error_name>\n  {\n    // Non-standard error names should have code 0\n    const ex = new DOMException('Custom message', 'MyCustomError');\n    testing.expectEqual('MyCustomError', ex.name);\n    testing.expectEqual('Custom message', ex.message);\n    testing.expectEqual(0, ex.code);\n  }\n</script>\n\n<script id=modern_errors_no_code>\n  {\n    // Modern errors that don't have legacy codes\n    const modernErrors = [\n      'EncodingError',\n      'NotReadableError',\n      'UnknownError',\n      'ConstraintError',\n      'DataError',\n      'TransactionInactiveError',\n      'ReadOnlyError',\n      'VersionError',\n      'OperationError',\n      'NotAllowedError'\n    ];\n\n    for (const name of modernErrors) {\n      const ex = new DOMException('test', name);\n      testing.expectEqual(name, ex.name);\n      testing.expectEqual(0, ex.code);\n    }\n  }\n</script>\n\n<script id=thrown_exception>\n  {\n    try {\n      throw new DOMException('Operation failed', 'InvalidStateError');\n    } catch (e) {\n      testing.expectEqual('InvalidStateError', e.name);\n      testing.expectEqual('Operation failed', e.message);\n      testing.expectEqual(11, e.code);\n    }\n  }\n</script>\n\n<div id=\"content\">\n  <a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n</div>\n\n<script id=hierarchy_error>\n  let link = $('#link');\n  let content = $('#content');\n\n  testing.withError((err) => {\n    testing.expectEqual(3, err.code);\n    testing.expectEqual('HierarchyRequestError', err.name);\n    testing.expectEqual(true, err instanceof DOMException);\n    testing.expectEqual(true, err instanceof Error);\n  }, () => link.appendChild(content));\n</script>\n"
  },
  {
    "path": "src/browser/tests/domimplementation.html",
    "content": "<!DOCTYPE html>\n<head>\n  <title>DOMImplementation Test</title>\n  <script src=\"testing.js\"></script>\n</head>\n\n<body>\n</body>\n\n<script id=implementation>\n  {\n    const impl = document.implementation;\n    testing.expectEqual('[object DOMImplementation]', impl.toString());\n  }\n</script>\n\n<script id=hasFeature>\n  {\n    const impl = document.implementation;\n    testing.expectEqual(true, impl.hasFeature('XML', '1.0'));\n    testing.expectEqual(true, impl.hasFeature('HTML', '2.0'));\n    testing.expectEqual(true, impl.hasFeature('', null));\n  }\n</script>\n\n<script id=createDocumentType>\n  {\n    const impl = document.implementation;\n    const doctype = impl.createDocumentType('html', '', '');\n    testing.expectEqual(10, doctype.nodeType);\n    testing.expectEqual('html', doctype.nodeName);\n    testing.expectEqual('html', doctype.name);\n    testing.expectEqual('', doctype.publicId);\n    testing.expectEqual('', doctype.systemId);\n    testing.expectEqual('[object DocumentType]', doctype.toString());\n  }\n</script>\n\n<script id=createDocumentTypeWithIds>\n  {\n    const impl = document.implementation;\n    const doctype = impl.createDocumentType(\n      'svg',\n      '-//W3C//DTD SVG 1.1//EN',\n      'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'\n    );\n    testing.expectEqual('svg', doctype.name);\n    testing.expectEqual('-//W3C//DTD SVG 1.1//EN', doctype.publicId);\n    testing.expectEqual('http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd', doctype.systemId);\n  }\n</script>\n\n<script id=createDocumentTypeNullIds>\n  {\n    const impl = document.implementation;\n    const doctype = impl.createDocumentType('html', null, null);\n    testing.expectEqual('html', doctype.name);\n    testing.expectEqual('null', doctype.publicId);\n    testing.expectEqual('null', doctype.systemId);\n  }\n</script>\n\n<script id=createHTMLDocument_no_title>\n  {\n    const impl = document.implementation;\n    const doc = impl.createHTMLDocument();\n\n    testing.expectEqual(true, doc instanceof HTMLDocument);\n    testing.expectEqual(true, doc instanceof Document);\n    testing.expectEqual(9, doc.nodeType);\n    testing.expectEqual('complete', doc.readyState);\n\n    // Should have DOCTYPE\n    testing.expectEqual(true, doc.firstChild instanceof DocumentType);\n    testing.expectEqual('html', doc.firstChild.name);\n\n    // Should have html element\n    const html = doc.documentElement;\n    testing.expectEqual(true, html !== null);\n    testing.expectEqual('HTML', html.tagName);\n\n    // Should have head\n    const head = doc.head;\n    testing.expectEqual(true, head !== null);\n    testing.expectEqual('HEAD', head.tagName);\n\n    // Should have body\n    const body = doc.body;\n    testing.expectEqual(true, body !== null);\n    testing.expectEqual('BODY', body.tagName);\n\n    // Title should be empty when not provided\n    testing.expectEqual('', doc.title);\n  }\n</script>\n\n<script id=createHTMLDocument_with_title>\n  {\n    const impl = document.implementation;\n    const doc = impl.createHTMLDocument('Test Document');\n\n    testing.expectEqual('Test Document', doc.title);\n\n    // Should have title element in head\n    const titleElement = doc.head.querySelector('title');\n    testing.expectEqual(true, titleElement !== null);\n    testing.expectEqual('Test Document', titleElement.textContent);\n  }\n</script>\n\n<script id=createHTMLDocument_nulll_title>\n  {\n    const impl = document.implementation;\n    const doc = impl.createHTMLDocument(null);\n\n    testing.expectEqual('null', doc.title);\n\n    // Should have title element in head\n    const titleElement = doc.head.querySelector('title');\n    testing.expectEqual(true, titleElement !== null);\n    testing.expectEqual('null', titleElement.textContent);\n  }\n</script>\n\n<script id=createHTMLDocument_structure>\n  {\n    const impl = document.implementation;\n    const doc = impl.createHTMLDocument('My Doc');\n\n    // Verify complete structure: DOCTYPE -> html -> (head, body)\n    testing.expectEqual(2, doc.childNodes.length); // DOCTYPE and html\n\n    const html = doc.documentElement;\n    testing.expectEqual(2, html.childNodes.length); // head and body\n\n    testing.expectEqual(doc.head, html.firstChild);\n    testing.expectEqual(doc.body, html.lastChild);\n\n    // Head should contain only title when title is provided\n    testing.expectEqual(1, doc.head.childNodes.length);\n    testing.expectEqual('TITLE', doc.head.firstChild.tagName);\n  }\n</script>\n\n<script id=createHTMLDocument_manipulation>\n  {\n    const impl = document.implementation;\n    const doc = impl.createHTMLDocument();\n\n    // Should be able to manipulate the created document\n    const div = doc.createElement('div');\n    div.textContent = 'Hello World';\n    doc.body.appendChild(div);\n\n    testing.expectEqual(1, doc.body.childNodes.length);\n    testing.expectEqual('Hello World', doc.body.firstChild.textContent);\n  }\n</script>\n\n<script id=createDocument_minimal>\n  {\n    const impl = document.implementation;\n    const doc = impl.createDocument(null, null, null);\n\n    testing.expectEqual(true, doc instanceof Document);\n    testing.expectEqual(9, doc.nodeType);\n    testing.expectEqual('[object XMLDocument]', doc.toString());\n\n    // Should be empty - no doctype, no root element\n    testing.expectEqual(0, doc.childNodes.length);\n    testing.expectEqual(null, doc.documentElement);\n  }\n</script>\n\n<script id=createDocument_with_root>\n  {\n    const impl = document.implementation;\n    const doc = impl.createDocument(null, 'root', null);\n\n    testing.expectEqual(1, doc.childNodes.length);\n\n    const root = doc.documentElement;\n    testing.expectEqual(true, root !== null);\n    // TODO: XML documents should preserve case, but we currently uppercase\n    testing.expectEqual('root', root.tagName);\n  }\n</script>\n\n<script id=createDocument_with_namespace>\n  {\n    const impl = document.implementation;\n    const doc = impl.createDocument('http://www.w3.org/2000/svg', 'svg', null);\n\n    const root = doc.documentElement;\n    testing.expectEqual('svg', root.nodeName);\n    testing.expectEqual('http://www.w3.org/2000/svg', root.namespaceURI);\n  }\n</script>\n\n<script id=createDocument_with_doctype>\n  {\n    const impl = document.implementation;\n    const doctype = impl.createDocumentType('svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd');\n    const doc = impl.createDocument('http://www.w3.org/2000/svg', 'svg', doctype);\n\n    testing.expectEqual(2, doc.childNodes.length);\n\n    // First child should be doctype\n    testing.expectEqual(true, doc.firstChild instanceof DocumentType);\n    testing.expectEqual('svg', doc.firstChild.name);\n\n    // Second child should be root element\n    testing.expectEqual('svg', doc.documentElement.nodeName);\n  }\n</script>\n\n<script id=createDocument_qualified_name>\n  {\n    const impl = document.implementation;\n    const doc = impl.createDocument('http://example.com', 'prefix:localName', null);\n\n    const root = doc.documentElement;\n    testing.expectEqual('prefix:localName', root.tagName);\n    // TODO: Custom namespaces are being replaced with an empty value\n    testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI);\n  }\n</script>\n\n<script id=createDocument_manipulation>\n  {\n    const impl = document.implementation;\n    const doc = impl.createDocument(null, 'root', null);\n\n    // Should be able to manipulate the created document\n    const child = doc.createElement('child');\n    child.textContent = 'Test';\n    doc.documentElement.appendChild(child);\n\n    testing.expectEqual(1, doc.documentElement.childNodes.length);\n    testing.expectEqual('child', doc.documentElement.firstChild.tagName);\n    testing.expectEqual('Test', doc.documentElement.firstChild.textContent);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/domparser.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<body></body>\n\n<script id=basic>\n{\n  {\n    const parser = new DOMParser();\n    testing.expectEqual('object', typeof parser);\n    testing.expectEqual('function', typeof parser.parseFromString);\n  }\n\n  {\n    const parser = new DOMParser();\n    let d = parser.parseFromString('', 'text/xml');\n    testing.expectEqual('<parsererror>error</parsererror>', new XMLSerializer().serializeToString(d));\n  }\n}\n</script>\n\n<script id=parseSimpleHTML>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div>Hello World</div>', 'text/html');\n\n  testing.expectEqual('object', typeof doc);\n  testing.expectEqual('[object HTMLDocument]', doc.toString());\n\n  const div = doc.querySelector('div');\n  testing.expectEqual('DIV', div.tagName);\n  testing.expectEqual('Hello World', div.textContent);\n}\n</script>\n\n<script id=parseWithAttributes>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"test\" class=\"foo\">Content</div>', 'text/html');\n\n  const div = doc.querySelector('div');\n  testing.expectEqual('test', div.id);\n  testing.expectEqual('foo', div.className);\n  testing.expectEqual('Content', div.textContent);\n}\n</script>\n\n<script id=parseMultipleElements>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div>First</div><span>Second</span>', 'text/html');\n\n  const div = doc.querySelector('div');\n  const span = doc.querySelector('span');\n\n  testing.expectEqual('DIV', div.tagName);\n  testing.expectEqual('First', div.textContent);\n  testing.expectEqual('SPAN', span.tagName);\n  testing.expectEqual('Second', span.textContent);\n}\n</script>\n\n<script id=parseNestedElements>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div><p><span>Nested</span></p></div>', 'text/html');\n\n  const div = doc.querySelector('div');\n  const p = doc.querySelector('p');\n  const span = doc.querySelector('span');\n\n  testing.expectEqual('DIV', div.tagName);\n  testing.expectEqual('P', p.tagName);\n  testing.expectEqual('SPAN', span.tagName);\n  testing.expectEqual('Nested', span.textContent);\n  testing.expectEqual(p, div.firstChild);\n  testing.expectEqual(span, p.firstChild);\n}\n</script>\n\n<script id=parseEmptyString>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('', 'text/html');\n\n  testing.expectEqual('object', typeof doc);\n  testing.expectEqual('[object HTMLDocument]', doc.toString());\n}\n</script>\n\n<script id=parsedDocumentIsIndependent>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"parsed\">Parsed</div>', 'text/html');\n\n  // The parsed document should be independent from the current document\n  const currentDiv = document.querySelector('div');\n  const parsedDiv = doc.querySelector('div');\n\n  testing.expectEqual(null, currentDiv);\n  testing.expectEqual('parsed', parsedDiv.id);\n  testing.expectEqual('Parsed', parsedDiv.textContent);\n}\n</script>\n\n<script id=malformedHTMLDoesNotThrow>\n{\n  const parser = new DOMParser();\n\n  // HTML parsers should be forgiving and not throw on malformed HTML\n  const doc1 = parser.parseFromString('<div><p>unclosed', 'text/html');\n  testing.expectEqual('object', typeof doc1);\n\n  const doc2 = parser.parseFromString('<<<invalid>>>', 'text/html');\n  testing.expectEqual('object', typeof doc2);\n}\n</script>\n\n<script id=getElementById>\n{\n  const doc = new DOMParser().parseFromString('<div id=\"new-node\">new-node</div>', 'text/html');\n  testing.expectEqual('new-node', doc.getElementById('new-node').textContent);\n}\n</script>\n\n<script id=getElementById_isolationBetweenDocuments>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"shared-id\">From parsed doc</div>', 'text/html');\n\n  // Create element with same ID in main document\n  const mainEl = document.createElement('div');\n  mainEl.id = 'shared-id';\n  mainEl.textContent = 'From main doc';\n  document.body.appendChild(mainEl);\n\n  // Each document should find its own element\n  const mainFound = document.getElementById('shared-id');\n  testing.expectEqual('From main doc', mainFound.textContent);\n\n  const parsedFound = doc.getElementById('shared-id');\n  testing.expectEqual('From parsed doc', parsedFound.textContent);\n\n  // Clean up\n  mainEl.remove();\n}\n</script>\n\n<script id=getElementById_afterSettingId>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div>No ID initially</div>', 'text/html');\n\n  const div = doc.querySelector('div');\n\n  // Should not find it yet\n  testing.expectEqual(null, doc.getElementById('new-id'));\n\n  // Set ID via JavaScript\n  div.id = 'new-id';\n\n  // Should now find it\n  const found = doc.getElementById('new-id');\n  testing.expectEqual(div, found);\n}\n</script>\n\n<script id=getElementById_afterRemovingId>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"remove-me\">Content</div>', 'text/html');\n\n  // Should find it initially\n  const div = doc.getElementById('remove-me');\n  testing.expectEqual('Content', div.textContent);\n\n  // Remove the ID\n  div.removeAttribute('id');\n\n  // Should not find it anymore\n  testing.expectEqual(null, doc.getElementById('remove-me'));\n}\n</script>\n\n<script id=getElementById_afterChangingId>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"old-id\">Content</div>', 'text/html');\n\n  const div = doc.querySelector('div');\n\n  // Change the ID\n  div.id = 'new-id';\n\n  // Should not find old ID\n  testing.expectEqual(null, doc.getElementById('old-id'));\n\n  // Should find new ID\n  const found = doc.getElementById('new-id');\n  testing.expectEqual(div, found);\n}\n</script>\n\n<script id=getElementById_detachedFromParsedDoc>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"will-detach\">Content</div>', 'text/html');\n\n  const div = doc.querySelector('div');\n\n  // Should find it while connected\n  testing.expectEqual(div, doc.getElementById('will-detach'));\n\n  // Remove it from the document\n  div.remove();\n\n  // Should not find it after removal\n  testing.expectEqual(null, doc.getElementById('will-detach'));\n}\n</script>\n\n<script id=getElementById_multipleDocuments>\n{\n  const parser = new DOMParser();\n  const doc1 = parser.parseFromString('<div id=\"doc1-el\">Doc 1</div>', 'text/html');\n  const doc2 = parser.parseFromString('<div id=\"doc2-el\">Doc 2</div>', 'text/html');\n\n  // Each document should only find its own element\n  testing.expectEqual('Doc 1', doc1.getElementById('doc1-el').textContent);\n  testing.expectEqual(null, doc1.getElementById('doc2-el'));\n\n  testing.expectEqual('Doc 2', doc2.getElementById('doc2-el').textContent);\n  testing.expectEqual(null, doc2.getElementById('doc1-el'));\n}\n</script>\n\n<script id=documentElement>\n  testing.expectEqual('', new DOMParser().parseFromString('', \"text/html\").documentElement.textContent);\n  testing.expectEqual('spice', new DOMParser().parseFromString('spice', \"text/html\").documentElement.textContent);\n  testing.expectEqual('<html><head></head><body>spice</body></html>', new DOMParser().parseFromString('spice', \"text/html\").documentElement.outerHTML);\n  testing.expectEqual('<html><head></head><body></body></html>', new DOMParser().parseFromString('<html></html>', \"text/html\").documentElement.outerHTML);\n</script>\n\n<script id=parse-xml>\n{\n  const sampleXML = `<?xml version=\"1.0\"?>\n    <catalog>\n       <book id=\"bk101\">\n          <author>Gambardella, Matthew</author>\n          <title>XML Developer's Guide</title>\n          <genre>Computer</genre>\n          <price>44.95</price>\n          <publish_date>2000-10-01</publish_date>\n          <description>An in-depth look at creating applications\n          with XML.</description>\n       </book>\n       <book id=\"bk102\">\n          <author>Ralls, Kim</author>\n          <title>Midnight Rain</title>\n          <genre>Fantasy</genre>\n          <price>5.95</price>\n          <publish_date>2000-12-16</publish_date>\n          <description>A former architect battles corporate zombies,\n          an evil sorceress, and her own childhood to become queen\n          of the world.</description>\n       </book>\n       <book id=\"bk103\">\n          <author>Corets, Eva</author>\n          <title>Maeve Ascendant</title>\n          <genre>Fantasy</genre>\n          <price>5.95</price>\n          <publish_date>2000-11-17</publish_date>\n          <description>After the collapse of a nanotechnology\n          society in England, the young survivors lay the\n          foundation for a new society.</description>\n       </book>\n       <book id=\"bk104\">\n          <author>Corets, Eva</author>\n          <title>Oberon's Legacy</title>\n          <genre>Fantasy</genre>\n          <price>5.95</price>\n          <publish_date>2001-03-10</publish_date>\n          <description>In post-apocalypse England, the mysterious\n          agent known only as Oberon helps to create a new life\n          for the inhabitants of London. Sequel to Maeve\n          Ascendant.</description>\n       </book>\n       <book id=\"bk105\">\n          <author>Corets, Eva</author>\n          <title>The Sundered Grail</title>\n          <genre>Fantasy</genre>\n          <price>5.95</price>\n          <publish_date>2001-09-10</publish_date>\n          <description>The two daughters of Maeve, half-sisters,\n          battle one another for control of England. Sequel to\n          Oberon's Legacy.</description>\n       </book>\n       <book id=\"bk106\">\n          <author>Randall, Cynthia</author>\n          <title>Lover Birds</title>\n          <genre>Romance</genre>\n          <price>4.95</price>\n          <publish_date>2000-09-02</publish_date>\n          <description>When Carla meets Paul at an ornithology\n          conference, tempers fly as feathers get ruffled.</description>\n       </book>\n       <book id=\"bk107\">\n          <author>Thurman, Paula</author>\n          <title>Splish Splash</title>\n          <genre>Romance</genre>\n          <price>4.95</price>\n          <publish_date>2000-11-02</publish_date>\n          <description>A deep sea diver finds true love twenty\n          thousand leagues beneath the sea.</description>\n       </book>\n       <book id=\"bk108\">\n          <author>Knorr, Stefan</author>\n          <title>Creepy Crawlies</title>\n          <genre>Horror</genre>\n          <price>4.95</price>\n          <publish_date>2000-12-06</publish_date>\n          <description>An anthology of horror stories about roaches,\n          centipedes, scorpions  and other insects.</description>\n       </book>\n       <book id=\"bk109\">\n          <author>Kress, Peter</author>\n          <title>Paradox Lost</title>\n          <genre>Science Fiction</genre>\n          <price>6.95</price>\n          <publish_date>2000-11-02</publish_date>\n          <description>After an inadvertant trip through a Heisenberg\n          Uncertainty Device, James Salway discovers the problems\n          of being quantum.</description>\n       </book>\n       <book id=\"bk110\">\n          <author>O'Brien, Tim</author>\n          <title>Microsoft .NET: The Programming Bible</title>\n          <genre>Computer</genre>\n          <price>36.95</price>\n          <publish_date>2000-12-09</publish_date>\n          <description>Microsoft's .NET initiative is explored in\n          detail in this deep programmer's reference.</description>\n       </book>\n       <book id=\"bk111\">\n          <author>O'Brien, Tim</author>\n          <title>MSXML3: A Comprehensive Guide</title>\n          <genre>Computer</genre>\n          <price>36.95</price>\n          <publish_date>2000-12-01</publish_date>\n          <description>The Microsoft MSXML3 parser is covered in\n          detail, with attention to XML DOM interfaces, XSLT processing,\n          SAX and more.</description>\n       </book>\n       <book id=\"bk112\">\n          <author>Galos, Mike</author>\n          <title>Visual Studio 7: A Comprehensive Guide</title>\n          <genre>Computer</genre>\n          <price>49.95</price>\n          <publish_date>2001-04-16</publish_date>\n          <description>Microsoft Visual Studio 7 is explored in depth,\n          looking at how Visual Basic, Visual C++, C#, and ASP+ are\n          integrated into a comprehensive development\n          environment.</description>\n       </book>\n    </catalog>`;\n\n  const parser = new DOMParser();\n  const mimes = [\n    \"text/xml\",\n    \"application/xml\",\n    \"application/xhtml+xml\",\n    \"image/svg+xml\",\n  ];\n\n  for (const mime of mimes) {\n    const doc = parser.parseFromString(sampleXML, mime);\n    const { firstChild: { childNodes, children: collection, tagName }, children } = doc;\n    // doc.\n    testing.expectEqual(true, doc instanceof XMLDocument);\n    testing.expectEqual(1, children.length);\n    // firstChild.\n    // TODO: Modern browsers expect this in lowercase.\n    testing.expectEqual(\"catalog\", tagName);\n    testing.expectEqual(25, childNodes.length);\n    testing.expectEqual(12, collection.length);\n    // Check children of first child.\n    for (let i = 0; i < collection.length; i++) {\n      const {children: elements, id} = collection.item(i);\n      testing.expectEqual(\"bk\" + (100 + i + 1), id);\n      // TODO: Modern browsers expect these in lowercase.\n      testing.expectEqual(\"author\", elements.item(0).tagName);\n      testing.expectEqual(\"title\", elements.item(1).tagName);\n      testing.expectEqual(\"genre\", elements.item(2).tagName);\n      testing.expectEqual(\"price\", elements.item(3).tagName);\n      testing.expectEqual(\"publish_date\", elements.item(4).tagName);\n      testing.expectEqual(\"description\", elements.item(5).tagName);\n    }\n  }\n}\n</script>\n\n<script id=getElementsByTagName-xml>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<layout><row><col>A</col><col>B</col></row></layout>', 'text/xml');\n\n  // Test getElementsByTagName on document\n  const rows = doc.getElementsByTagName('row');\n  testing.expectEqual(1, rows.length);\n\n  // Test getElementsByTagName on element\n  const row = rows[0];\n  const cols = row.getElementsByTagName('col');\n  testing.expectEqual(2, cols.length);\n  testing.expectEqual('A', cols[0].textContent);\n  testing.expectEqual('B', cols[1].textContent);\n\n  // Test getElementsByTagName('*') on element\n  const allElements = row.getElementsByTagName('*');\n  testing.expectEqual(2, allElements.length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/append.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"test-container\"></div>\n<script id=\"append-prepend-tests\">\n    const container = $('#test-container');\n\n    // Test append\n    const p1 = document.createElement('p');\n    p1.textContent = 'p1';\n    container.append(p1);\n    testing.expectEqual('<p>p1</p>', container.innerHTML, \"append single element\");\n\n    const p2 = document.createElement('p');\n    p2.textContent = 'p2';\n    container.append(p2, ' some text');\n    testing.expectEqual('<p>p1</p><p>p2</p> some text', container.innerHTML, \"append multiple nodes\");\n\n    // Test prepend\n    container.innerHTML = '';\n    const p3 = document.createElement('p');\n    p3.textContent = 'p3';\n    container.prepend(p3);\n    testing.expectEqual('<p>p3</p>', container.innerHTML, \"prepend single element\");\n\n    const p4 = document.createElement('p');\n    p4.textContent = 'p4';\n    container.prepend(p4, 'some text ');\n    testing.expectEqual('<p>p4</p>some text <p>p3</p>', container.innerHTML, \"prepend multiple nodes\");\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/attributes.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=attr1 ClasS=\"sHow\"></div>\n\n<script id=attributes>\n  const el1 = $('#attr1');\n  testing.expectEqual(null, el1.getAttribute('other'));\n\n  testing.expectEqual('attr1', el1.id);\n  testing.expectEqual('attr1', el1.getAttribute('id'));\n  testing.expectEqual('attr1', el1.getAttribute('Id'));\n  testing.expectEqual('sHow', el1.getAttribute('CLASS'));\n  testing.expectEqual('sHow', el1.getAttribute('class'));\n  testing.expectEqual(['id', 'class'], el1.getAttributeNames());\n\n  el1.setAttribute('other', 'attr-1')\n  testing.expectEqual('attr-1', el1.getAttribute('other'));\n  testing.expectEqual(['id', 'class', 'other'], el1.getAttributeNames());\n  el1.removeAttribute('other');\n  testing.expectEqual(null, el1.getAttribute('other'));\n  testing.expectEqual(['id', 'class'], el1.getAttributeNames());\n\n  testing.expectEqual(null, el1.getAttributeNode('unknown'));\n\n  let an1 = el1.getAttributeNode('class');\n  testing.expectEqual(an1 , el1.setAttributeNode(an1));\n  testing.expectEqual(el1, an1.ownerElement);\n  testing.expectEqual('class', an1.name);\n  testing.expectEqual('class', an1.localName);\n  testing.expectEqual('sHow', an1.value);\n  testing.expectEqual(el1.getAttributeNode('class'), an1);\n  testing.expectEqual(null, an1.namespaceURI);\n\n  const script_id_node = $('#attributes').getAttributeNode('id')\n  testing.withError((err) => {\n    testing.expectEqual(8, err.code);\n    testing.expectEqual(\"NotFoundError\", err.name);\n  }, () => el1.removeAttributeNode(script_id_node));\n\n  testing.expectEqual(an1, el1.removeAttributeNode(an1));\n  testing.expectEqual(null, an1.ownerElement);\n  testing.expectEqual(null, el1.getAttributeNode('class'));\n\n  testing.expectEqual(null, el1.setAttributeNode(an1))\n  testing.expectEqual(el1, an1.ownerElement);\n</script>\n\n<script id=namednodemap>\n  testing.expectEqual(true, el1.attributes instanceof NamedNodeMap);\n  testing.expectEqual(el1.attributes, el1.attributes);\n  function assertAttributes(expected) {\n    const attributes = el1.attributes;\n    testing.expectEqual(expected.length, attributes.length);\n\n    for (let i = 0; i < expected.length; i++) {\n      const ex = expected[i];\n      let attribute = attributes[ex.name];\n      testing.expectEqual('[object Attr]', attribute.toString());\n      testing.expectEqual(ex.name, attribute.name)\n      testing.expectEqual(ex.value, attribute.value)\n\n      attribute = attributes.getNamedItem(ex.name);\n      testing.expectEqual(ex.name, attribute.name)\n      testing.expectEqual(ex.value, attribute.value)\n\n      attribute = attributes.item(i);\n      testing.expectEqual(ex.name, attribute.name)\n      testing.expectEqual(ex.value, attribute.value)\n    }\n    testing.expectEqual(undefined, attributes[\"nope\"]);\n    testing.expectEqual('attr1', attributes.id.value);\n    testing.expectEqual(null, attributes.getNamedItem(\"nope\"));\n    testing.expectEqual(null, attributes.item(100));\n    testing.expectEqual(null, attributes.item(-3));\n\n    let acc = [];\n    for (let attribute of attributes) {\n      acc.push({name: attribute.name, value: attribute.value});\n    }\n    testing.expectEqual(expected, acc);\n  }\n\n  assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]);\n</script>\n\n<script id=hasAttribute>\n{\n  const el1 = $('#attr1');\n\n  testing.expectEqual(true, el1.hasAttribute('id'));\n  testing.expectEqual(true, el1.hasAttribute('ID'));\n  testing.expectEqual(true, el1.hasAttribute('class'));\n  testing.expectEqual(true, el1.hasAttribute('CLASS'));\n  testing.expectEqual(false, el1.hasAttribute('other'));\n  testing.expectEqual(false, el1.hasAttribute('nope'));\n\n  el1.setAttribute('data-test', 'value');\n  testing.expectEqual(true, el1.hasAttribute('data-test'));\n\n  el1.removeAttribute('data-test');\n  testing.expectEqual(false, el1.hasAttribute('data-test'));\n}\n</script>\n\n<script id=toggleAttribute>\n{\n  const el1 = $('#attr1');\n\n  testing.expectEqual(false, el1.hasAttribute('toggle-test'));\n  testing.expectEqual(true, el1.toggleAttribute('toggle-test'));\n  testing.expectEqual(true, el1.hasAttribute('toggle-test'));\n  testing.expectEqual('', el1.getAttribute('toggle-test'));\n\n  testing.expectEqual(false, el1.toggleAttribute('toggle-test'));\n  testing.expectEqual(false, el1.hasAttribute('toggle-test'));\n\n  testing.expectEqual(false, el1.hasAttribute('toggle-test'));\n  testing.expectEqual(true, el1.toggleAttribute('toggle-test', true));\n  testing.expectEqual(true, el1.hasAttribute('toggle-test'));\n\n  testing.expectEqual(true, el1.toggleAttribute('toggle-test', true));\n  testing.expectEqual(true, el1.hasAttribute('toggle-test'));\n\n  testing.expectEqual(false, el1.toggleAttribute('toggle-test', false));\n  testing.expectEqual(false, el1.hasAttribute('toggle-test'));\n\n  testing.expectEqual(false, el1.toggleAttribute('toggle-test', false));\n  testing.expectEqual(false, el1.hasAttribute('toggle-test'));\n}\n</script>\n\n<script id=hasAttributes>\n{\n  const el1 = $('#attr1');\n\n  // Element with attributes should return true\n  testing.expectEqual(true, el1.hasAttributes());\n\n  // Create element with no attributes\n  const div = document.createElement('div');\n  testing.expectEqual(false, div.hasAttributes());\n\n  // Add an attribute\n  div.setAttribute('test', 'value');\n  testing.expectEqual(true, div.hasAttributes());\n\n  // Remove the attribute\n  div.removeAttribute('test');\n  testing.expectEqual(false, div.hasAttributes());\n\n  // Add multiple attributes\n  div.setAttribute('a', '1');\n  div.setAttribute('b', '2');\n  div.setAttribute('c', '3');\n  testing.expectEqual(true, div.hasAttributes());\n\n  // Remove all but one\n  div.removeAttribute('a');\n  div.removeAttribute('b');\n  testing.expectEqual(true, div.hasAttributes());\n\n  // Remove the last one\n  div.removeAttribute('c');\n  testing.expectEqual(false, div.hasAttributes());\n}\n</script>\n\n<script id=invalidAttributeNames>\n{\n  const div = document.createElement('div');\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('0abc', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('123', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('-invalid', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('.foo', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('my attr', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('my@attr', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('attr!', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('~', 'value'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.setAttribute('', 'value'));\n\n  div.setAttribute('valid-name', 'value1');\n  testing.expectEqual('value1', div.getAttribute('valid-name'));\n\n  div.setAttribute('valid_name', 'value2');\n  testing.expectEqual('value2', div.getAttribute('valid_name'));\n\n  div.setAttribute('valid.name', 'value3');\n  testing.expectEqual('value3', div.getAttribute('valid.name'));\n\n  div.setAttribute('a123', 'value4');\n  testing.expectEqual('value4', div.getAttribute('a123'));\n\n  div.setAttribute('data-test-123', 'value5');\n  testing.expectEqual('value5', div.getAttribute('data-test-123'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.toggleAttribute('.invalid'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.toggleAttribute('0invalid'));\n\n  testing.withError((err) => {\n    testing.expectEqual(5, err.code);\n    testing.expectEqual(\"InvalidCharacterError\", err.name);\n  }, () => div.toggleAttribute('-invalid'));\n}\n</script>\n\n<a id=legacy></a>\n<script id=legacy>\n  {\n    let a = document.getElementById('legacy').attributes;\n    testing.expectEqual(1, a.length);\n    testing.expectEqual('[object Attr]', a.item(0).toString());\n    testing.expectEqual(null, a.item(1));\n    testing.expectEqual('[object Attr]', a.getNamedItem('id').toString());\n    testing.expectEqual(null, a.getNamedItem('foo'));\n    testing.expectEqual('[object Attr]', a.setNamedItem(a.getNamedItem('id')).toString());\n\n    testing.expectEqual('id', a['id'].name);\n    testing.expectEqual('legacy', a['id'].value);\n    testing.expectEqual(undefined, a['other']);\n    a[0].value = 'abc123';\n    testing.expectEqual('abc123', a[0].value);\n  }\n</script>\n\n<div id=\"nsa\"></div>\n<script id=non-string-attr>\n  {\n    let nsa = document.getElementById('nsa');\n\n    nsa.setAttribute('int', 1);\n    testing.expectEqual('1', nsa.getAttribute('int'));\n\n    nsa.setAttribute('obj', {});\n    testing.expectEqual('[object Object]', nsa.getAttribute('obj'));\n\n    nsa.setAttribute('arr', []);\n    testing.expectEqual('', nsa.getAttribute('arr'));\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/bounding_rect.html",
    "content": "<!DOCTYPE html>\n<head>\n  <title>Lightpanda Browser demo</title>\n  <meta charset=\"UTF-8\">\n</head>\n<body>\n  <h1>Lightpanda Browser Demo</h1>\n\n  <h2><a href=\"campfire-commerce/\">Campfire Commerce</a></h2>\n  <a href=\"campfire-commerce/\"><img src=\"campfire-commerce/images/logo.jpg\"></a>\n  <p>\n    Demo of an e-commerce product offer page with data loaded from XHR request.\n    <br>\n    All images and texts have been generated with AI.\n    Template by <a href=\"https://codepen.io/Sunil_Pradhan/pen/qBqgLxK\">Sunil Pradhan</a>\n  </p>\n\n  <h2><a href=\"amiibo/\">Amiibo characters</a></h2>\n  <a href=\"amiibo/\"><img src=\"https://raw.githubusercontent.com/N3evin/AmiiboAPI/master/images/icon_04380001-03000502.png\"></a>\n  <p>\n    Pages of Amiibo characters generated from\n    <a href=\"https://www.amiiboapi.com\">Amiibo API</a>.\n  </p>\n</body>\n<script src=\"../testing.js\"></script>\n<script id=\"rect\">\n  const a1 = document.querySelector('[href=\"campfire-commerce/\"]');\n  const rect1 = a1.getBoundingClientRect();\n  console.log(\"a1:\", { x: rect1.x, y: rect1.y });\n\n  const a2 = document.querySelector('[href=\"amiibo/\"]');\n  const rect2 = a2.getBoundingClientRect();\n\n  testing.expectTrue(rect1.x != rect2.x)\n  testing.expectTrue(rect1.y != rect2.y);\n  testing.expectEqual(a1, document.elementFromPoint(rect1.x, rect1.y))\n  testing.expectEqual(a2, document.elementFromPoint(rect2.x, rect2.y))\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/class_list.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=test1></div>\n<div id=test2 class=\"foo bar\"></div>\n\n<script id=basic>\n{\n  const div = $('#test1');\n\n  testing.expectEqual(0, div.classList.length);\n\n  div.classList.add('foo');\n  testing.expectEqual('foo', div.className);\n  testing.expectEqual(1, div.classList.length);\n\n  div.classList.add('bar', 'baz');\n  testing.expectEqual('foo bar baz', div.className);\n  testing.expectEqual(3, div.classList.length);\n\n  div.classList.add('bar');\n  testing.expectEqual('foo bar baz', div.className);\n  testing.expectEqual(3, div.classList.length);\n}\n</script>\n\n<script id=contains>\n{\n  const div = $('#test2');\n\n  testing.expectEqual(false, div.classList.contains(''));\n  testing.expectEqual(true, div.classList.contains('foo'));\n  testing.expectEqual(true, div.classList.contains('bar'));\n  testing.expectEqual(false, div.classList.contains('baz'));\n}\n</script>\n\n<script id=remove>\n{\n  const div = $('#test2');\n\n  div.classList.remove('foo');\n  testing.expectEqual('bar', div.className);\n  testing.expectEqual(1, div.classList.length);\n\n  div.classList.remove('nonexistent');\n  testing.expectEqual('bar', div.className);\n\n  div.classList.remove('bar');\n  testing.expectEqual('', div.className);\n  testing.expectEqual(0, div.classList.length);\n}\n</script>\n\n<script id=toggle>\n{\n  const div = document.createElement('div');\n\n  let result = div.classList.toggle('foo');\n  testing.expectEqual(true, result);\n  testing.expectEqual('foo', div.className);\n\n  result = div.classList.toggle('foo');\n  testing.expectEqual(false, result);\n  testing.expectEqual('', div.className);\n\n  result = div.classList.toggle('bar', true);\n  testing.expectEqual(true, result);\n  testing.expectEqual('bar', div.className);\n\n  result = div.classList.toggle('bar', true);\n  testing.expectEqual(true, result);\n  testing.expectEqual('bar', div.className);\n\n  result = div.classList.toggle('baz', false);\n  testing.expectEqual(false, result);\n  testing.expectEqual('bar', div.className);\n}\n</script>\n\n<script id=replace>\n{\n  const div = document.createElement('div');\n  div.className = 'foo bar baz';\n\n  let result = div.classList.replace('bar', 'qux');\n  testing.expectEqual(true, result);\n  testing.expectEqual('foo qux baz', div.className);\n\n  result = div.classList.replace('nonexistent', 'other');\n  testing.expectEqual(false, result);\n  testing.expectEqual('foo qux baz', div.className);\n}\n</script>\n\n<script id=replace_errors>\n{\n  const div = document.createElement('div');\n  div.className = 'foo bar';\n\n  testing.withError((err) => {\n    testing.expectEqual('SyntaxError', err.name);\n  }, () => div.classList.replace('', 'baz'));\n\n  testing.withError((err) => {\n    testing.expectEqual('SyntaxError', err.name);\n  }, () => div.classList.replace('foo', ''));\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => div.classList.replace('foo bar', 'baz'));\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => div.classList.replace('foo', 'bar baz'));\n}\n</script>\n\n<script id=item>\n{\n  const div = document.createElement('div');\n  div.className = 'alpha beta gamma';\n\n  testing.expectEqual('alpha', div.classList.item(0));\n  testing.expectEqual('beta', div.classList.item(1));\n  testing.expectEqual('gamma', div.classList.item(2));\n  testing.expectEqual(null, div.classList.item(3));\n  testing.expectEqual(null, div.classList.item(-1));\n\n  testing.expectEqual('alpha', div.classList[0]);\n  testing.expectEqual('beta', div.classList[1]);\n  testing.expectEqual('gamma', div.classList[2]);\n  testing.expectEqual(undefined, div.classList[3]);\n}\n</script>\n\n<script id=iteration>\n{\n  const div = document.createElement('div');\n  div.className = 'one two three';\n\n  const result = [];\n  for (const cls of div.classList) {\n    result.push(cls);\n  }\n  testing.expectEqual(['one', 'two', 'three'], result);\n}\n</script>\n\n<script id=sync>\n{\n  const div = document.createElement('div');\n\n  div.setAttribute('class', 'alpha beta');\n  testing.expectEqual(2, div.classList.length);\n  testing.expectEqual(true, div.classList.contains('alpha'));\n  testing.expectEqual(true, div.classList.contains('beta'));\n\n  div.className = 'gamma delta';\n  testing.expectEqual(2, div.classList.length);\n  testing.expectEqual(true, div.classList.contains('gamma'));\n  testing.expectEqual(false, div.classList.contains('alpha'));\n\n  div.classList.add('epsilon');\n  testing.expectEqual('gamma delta epsilon', div.className);\n}\n</script>\n\n<script id=whitespace>\n{\n  const div = document.createElement('div');\n  div.className = '  foo   bar  \\t\\n  baz  ';\n\n  testing.expectEqual(3, div.classList.length);\n  testing.expectEqual('foo', div.classList[0]);\n  testing.expectEqual('bar', div.classList[1]);\n  testing.expectEqual('baz', div.classList[2]);\n}\n</script>\n\n<script id=value>\n{\n  const div = document.createElement('div');\n  div.classList.value = 'test1 test2';\n\n  testing.expectEqual('test1 test2', div.classList.value);\n  testing.expectEqual('test1 test2', div.className);\n  testing.expectEqual(2, div.classList.length);\n}\n</script>\n\n<script id=classList_assignment>\n{\n  const div = document.createElement('div');\n\n  // Direct assignment should work (equivalent to classList.value = ...)\n  div.classList = 'foo bar baz';\n  testing.expectEqual('foo bar baz', div.className);\n  testing.expectEqual(3, div.classList.length);\n  testing.expectEqual(true, div.classList.contains('foo'));\n\n  // Assigning again should replace\n  div.classList = 'qux';\n  testing.expectEqual('qux', div.className);\n  testing.expectEqual(1, div.classList.length);\n  testing.expectEqual(false, div.classList.contains('foo'));\n\n  // Empty assignment\n  div.classList = '';\n  testing.expectEqual('', div.className);\n  testing.expectEqual(0, div.classList.length);\n}\n</script>\n\n<script id=errors>\n{\n  const div = document.createElement('div');\n\n  testing.withError((err) => {\n    testing.expectEqual('SyntaxError', err.name);\n  }, () => div.classList.add(''));\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => div.classList.add('foo bar'));\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => div.classList.add('foo\\tbar'));\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => div.classList.add('foo\\nbar'));\n\n  testing.withError((err) => {\n    testing.expectEqual('SyntaxError', err.name);\n  }, () => div.classList.toggle(''));\n\n\n  testing.withError((err) => {\n    testing.expectEqual('InvalidCharacterError', err.name);\n  }, () => div.classList.remove('has space'));\n}\n</script>\n\n<script id=identity>\n{\n  const div = document.createElement('div');\n\n  testing.expectEqual(div.classList, div.classList);\n  const classList = div.classList;\n  div.className = 'changed';\n  testing.expectEqual(classList, div.classList);\n}\n</script>\n\n<script id=attribute_lifecycle>\n{\n  const div = document.createElement('div');\n\n  testing.expectEqual(null, div.getAttribute('class'));\n  testing.expectEqual(0, div.classList.length);\n\n  div.classList.add('foo');\n  testing.expectEqual('foo', div.getAttribute('class'));\n\n  div.classList.remove('foo');\n  testing.expectEqual('', div.getAttribute('class'));\n  testing.expectEqual(0, div.classList.length);\n\n  div.className = 'bar';\n  testing.expectEqual('bar', div.getAttribute('class'));\n\n  div.className = '';\n  testing.expectEqual('', div.getAttribute('class'));\n}\n</script>\n\n<script id=duplicates>\n{\n  const div = document.createElement('div');\n  div.className = 'foo foo bar';\n\n  testing.expectEqual(2, div.classList.length);\n  testing.expectEqual('foo', div.classList[0]);\n  testing.expectEqual('bar', div.classList[1]);\n\n  div.classList.add('foo');\n  testing.expectEqual('foo bar', div.className);\n\n  div.classList.remove('foo');\n  testing.expectEqual('bar', div.className);\n}\n</script>\n\n<script id=replace_edge_cases>\n{\n  const div = document.createElement('div');\n  div.className = 'foo bar';\n\n  let result = div.classList.replace('foo', 'foo');\n  testing.expectEqual(true, result);\n  testing.expectEqual('foo bar', div.className);\n\n  result = div.classList.replace('foo', 'bar');\n  testing.expectEqual(true, result);\n  testing.expectEqual('bar', div.className);\n\n  div.className = 'alpha beta gamma';\n  result = div.classList.replace('beta', 'gamma');\n  testing.expectEqual(true, result);\n  testing.expectEqual('alpha gamma', div.className);\n}\n</script>\n\n<script id=empty_calls>\n{\n  const div = document.createElement('div');\n  div.className = 'foo bar';\n\n  div.classList.add();\n  testing.expectEqual('foo bar', div.className);\n\n  div.classList.remove();\n  testing.expectEqual('foo bar', div.className);\n}\n</script>\n\n<script id=case_sensitivity>\n{\n  const div = document.createElement('div');\n\n  div.classList.add('Foo');\n  div.classList.add('foo');\n  div.classList.add('FOO');\n\n  testing.expectEqual('Foo foo FOO', div.className);\n  testing.expectEqual(3, div.classList.length);\n  testing.expectEqual(true, div.classList.contains('Foo'));\n  testing.expectEqual(true, div.classList.contains('foo'));\n  testing.expectEqual(true, div.classList.contains('FOO'));\n  testing.expectEqual(false, div.classList.contains('fOO'));\n}\n</script>\n\n<script id=special_characters>\n{\n  const div = document.createElement('div');\n\n  div.classList.add('class-name');\n  div.classList.add('class_name');\n  div.classList.add('class123');\n  div.classList.add('123class');\n  div.classList.add('über');\n  div.classList.add('日本語');\n\n  testing.expectEqual(6, div.classList.length);\n  testing.expectEqual(true, div.classList.contains('class-name'));\n  testing.expectEqual(true, div.classList.contains('class_name'));\n  testing.expectEqual(true, div.classList.contains('class123'));\n  testing.expectEqual(true, div.classList.contains('123class'));\n  testing.expectEqual(true, div.classList.contains('über'));\n  testing.expectEqual(true, div.classList.contains('日本語'));\n}\n</script>\n\n<script id=multiple_operations>\n{\n  const div = document.createElement('div');\n\n  div.classList.add('a', 'b', 'c', 'd', 'e');\n  testing.expectEqual('a b c d e', div.className);\n\n  div.classList.remove('b', 'd');\n  testing.expectEqual('a c e', div.className);\n\n  div.classList.add('b', 'c', 'f');\n  testing.expectEqual('a c e b f', div.className);\n}\n</script>\n\n<script id=toString>\n{\n  const div = document.createElement('div');\n  div.className = 'foo bar baz';\n\n  testing.expectEqual('foo bar baz', div.classList.toString());\n\n  div.className = '';\n  testing.expectEqual('', div.classList.toString());\n\n  div.className = '  alpha   beta  ';\n  testing.expectEqual('  alpha   beta  ', div.classList.toString());\n}\n</script>\n\n<script id=keys_iterator>\n{\n  const div = document.createElement('div');\n  div.className = 'one two three';\n\n  const keys = [];\n  const iter = div.classList.keys();\n\n  let result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual(0, result.value);\n  keys.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual(1, result.value);\n  keys.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual(2, result.value);\n  keys.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(true, result.done);\n  testing.expectEqual(undefined, result.value);\n\n  testing.expectEqual([0, 1, 2], keys);\n}\n</script>\n\n<script id=keys_for_of>\n{\n  const div = document.createElement('div');\n  div.className = 'alpha beta gamma';\n\n  const keys = [];\n  for (const key of div.classList.keys()) {\n    keys.push(key);\n  }\n  testing.expectEqual([0, 1, 2], keys);\n}\n</script>\n\n<script id=values_iterator>\n{\n  const div = document.createElement('div');\n  div.className = 'red green blue';\n\n  const values = [];\n  const iter = div.classList.values();\n\n  let result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual('red', result.value);\n  values.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual('green', result.value);\n  values.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual('blue', result.value);\n  values.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(true, result.done);\n  testing.expectEqual(undefined, result.value);\n\n  testing.expectEqual(['red', 'green', 'blue'], values);\n}\n</script>\n\n<script id=values_for_of>\n{\n  const div = document.createElement('div');\n  div.className = 'x y z';\n\n  const values = [];\n  for (const value of div.classList.values()) {\n    values.push(value);\n  }\n  testing.expectEqual(['x', 'y', 'z'], values);\n}\n</script>\n\n<script id=entries_iterator>\n{\n  const div = document.createElement('div');\n  div.className = 'first second third';\n\n  const entries = [];\n  const iter = div.classList.entries();\n\n  let result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual([0, 'first'], result.value);\n  entries.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual([1, 'second'], result.value);\n  entries.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(false, result.done);\n  testing.expectEqual([2, 'third'], result.value);\n  entries.push(result.value);\n\n  result = iter.next();\n  testing.expectEqual(true, result.done);\n  testing.expectEqual(undefined, result.value);\n\n  testing.expectEqual([[0, 'first'], [1, 'second'], [2, 'third']], entries);\n}\n</script>\n\n<script id=entries_for_of>\n{\n  const div = document.createElement('div');\n  div.className = 'a b c';\n\n  const entries = [];\n  for (const entry of div.classList.entries()) {\n    entries.push(entry);\n  }\n  testing.expectEqual([[0, 'a'], [1, 'b'], [2, 'c']], entries);\n}\n</script>\n\n<script id=forEach_basic>\n{\n  const div = document.createElement('div');\n  div.className = 'red green blue';\n\n  const values = [];\n  const indices = [];\n  const lists = [];\n\n  div.classList.forEach((value, index, list) => {\n    values.push(value);\n    indices.push(index);\n    lists.push(list);\n  });\n\n  testing.expectEqual(['red', 'green', 'blue'], values);\n  testing.expectEqual([0, 1, 2], indices);\n  testing.expectEqual(3, lists.length);\n  testing.expectEqual(div.classList, lists[0]);\n  testing.expectEqual(div.classList, lists[1]);\n  testing.expectEqual(div.classList, lists[2]);\n}\n</script>\n\n<script id=forEach_with_this>\n{\n  const div = document.createElement('div');\n  div.className = 'one two';\n\n  const context = { count: 0 };\n\n  div.classList.forEach(function(value) {\n    this.count++;\n  }, context);\n\n  testing.expectEqual(2, context.count);\n}\n</script>\n\n<script id=forEach_empty_list>\n{\n  const div = document.createElement('div');\n  div.className = '';\n\n  let callCount = 0;\n  div.classList.forEach(() => {\n    callCount++;\n  });\n\n  testing.expectEqual(0, callCount);\n}\n</script>\n\n<script id=forEach_duplicates>\n{\n  const div = document.createElement('div');\n  div.className = 'foo foo bar';\n\n  const values = [];\n  div.classList.forEach((value) => {\n    values.push(value);\n  });\n\n  testing.expectEqual(['foo', 'bar'], values);\n}\n</script>\n\n<script id=iterators_empty_list>\n{\n  const div = document.createElement('div');\n  div.className = '';\n\n  const keys = [...div.classList.keys()];\n  testing.expectEqual([], keys);\n\n  const values = [...div.classList.values()];\n  testing.expectEqual([], values);\n\n  const entries = [...div.classList.entries()];\n  testing.expectEqual([], entries);\n}\n</script>\n\n<script id=iterators_with_duplicates>\n{\n  const div = document.createElement('div');\n  div.className = 'alpha alpha beta alpha';\n\n  const values = [...div.classList.values()];\n  testing.expectEqual(['alpha', 'beta'], values);\n\n  const entries = [...div.classList.entries()];\n  testing.expectEqual([[0, 'alpha'], [1, 'beta']], entries);\n}\n</script>\n\n<script id=iterator_live_behavior>\n{\n  const div = document.createElement('div');\n  div.className = 'one two three';\n\n  const iter = div.classList.values();\n\n  let result = iter.next();\n  testing.expectEqual('one', result.value);\n\n  div.classList.add('four');\n\n  result = iter.next();\n  testing.expectEqual('two', result.value);\n\n  result = iter.next();\n  testing.expectEqual('three', result.value);\n\n  result = iter.next();\n  testing.expectEqual('four', result.value);\n\n  result = iter.next();\n  testing.expectEqual(true, result.done);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/closest.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<div id=\"outer\" class=\"container\">\n  <div id=\"middle\" class=\"wrapper\">\n    <div id=\"inner\" class=\"box\">\n      <p id=\"paragraph\" class=\"text\">Content</p>\n    </div>\n  </div>\n</div>\n\n<script id=matchesSelf>\n{\n  const para = document.getElementById('paragraph');\n\n  testing.expectEqual(para, para.closest('p'));\n  testing.expectEqual(para, para.closest('.text'));\n  testing.expectEqual(para, para.closest('#paragraph'));\n  testing.expectEqual(para, para.closest('p.text'));\n  testing.expectEqual(para, para.closest('p#paragraph.text'));\n}\n</script>\n\n<script id=findsAncestors>\n{\n  const para = document.getElementById('paragraph');\n\n  const inner = document.getElementById('inner');\n  testing.expectEqual(inner, para.closest('.box'));\n  testing.expectEqual(inner, para.closest('#inner'));\n  testing.expectEqual(inner, para.closest('div.box'));\n\n  const middle = document.getElementById('middle');\n  testing.expectEqual(middle, para.closest('.wrapper'));\n  testing.expectEqual(middle, para.closest('#middle'));\n\n  const outer = document.getElementById('outer');\n  testing.expectEqual(outer, para.closest('.container'));\n  testing.expectEqual(outer, para.closest('#outer'));\n}\n</script>\n\n<script id=returnsNull>\n{\n  const para = document.getElementById('paragraph');\n\n  testing.expectEqual(null, para.closest('.nonexistent'));\n  testing.expectEqual(null, para.closest('#missing'));\n  testing.expectEqual(null, para.closest('table'));\n  testing.expectEqual(null, para.closest('span'));\n}\n</script>\n\n<script id=complexSelectors>\n{\n  const para = document.getElementById('paragraph');\n\n  testing.expectEqual(document.getElementById('inner'), para.closest('div.box'));\n  testing.expectEqual(document.getElementById('middle'), para.closest('div.wrapper'));\n  testing.expectEqual(document.getElementById('outer'), para.closest('div.container'));\n\n  const inner = document.getElementById('inner');\n  inner.classList.add('active');\n  testing.expectEqual(inner, para.closest('.box.active'));\n}\n</script>\n\n<script id=attributeSelectors>\n{\n  const para = document.getElementById('paragraph');\n  const inner = document.getElementById('inner');\n  inner.setAttribute('data-test', 'value');\n\n  testing.expectEqual(inner, para.closest('[data-test]'));\n  testing.expectEqual(inner, para.closest('[data-test=\"value\"]'));\n  testing.expectEqual(null, para.closest('[data-other]'));\n}\n</script>\n\n<script id=errorHandling>\n{\n  const para = document.getElementById('paragraph');\n\n  let caught = false;\n  try {\n    para.closest('');\n  } catch (e) {\n    caught = true;\n    testing.expectEqual('SyntaxError', e.name);\n  }\n  testing.expectEqual(true, caught);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/css_style_properties.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"test-div\"></div>\n\n<script id=\"camelCaseAccess\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  testing.expectEqual('', div.style.backgroundColor);\n  testing.expectEqual('', div.style.fontSize);\n  testing.expectEqual('', div.style.marginTop);\n\n  div.style.setProperty('background-color', 'blue');\n  div.style.setProperty('font-size', '14px');\n  div.style.setProperty('margin-top', '10px');\n\n  testing.expectEqual('blue', div.style.backgroundColor);\n  testing.expectEqual('14px', div.style.fontSize);\n  testing.expectEqual('10px', div.style.marginTop);\n}\n</script>\n\n<script id=\"dashCaseAccess\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('background-color', 'red');\n  div.style.setProperty('font-size', '16px');\n\n  testing.expectEqual('red', div.style['background-color']);\n  testing.expectEqual('16px', div.style['font-size']);\n}\n</script>\n\n<script id=\"mixedAccess\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('color', 'green');\n\n  testing.expectEqual('green', div.style.color);\n  testing.expectEqual('green', div.style['color']);\n  testing.expectEqual('green', div.style.getPropertyValue('color'));\n}\n</script>\n\n<script id=\"cssFloat\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('float', 'left');\n  testing.expectEqual('left', div.style.cssFloat);\n  testing.expectEqual('left', div.style['float']);\n  testing.expectEqual('left', div.style.getPropertyValue('float'));\n\n  div.style.setProperty('float', 'right');\n  testing.expectEqual('right', div.style.cssFloat);\n}\n</script>\n\n<script id=\"vendorPrefixes\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('-webkit-transform', 'rotate(45deg)');\n  div.style.setProperty('-moz-user-select', 'none');\n  div.style.setProperty('-ms-filter', 'blur(5px)');\n  div.style.setProperty('-o-transition', 'all');\n\n  testing.expectEqual('rotate(45deg)', div.style.webkitTransform);\n  testing.expectEqual(undefined, div.style.mozUserSelect);\n  testing.expectEqual(undefined, div.style.msFilter);\n  testing.expectEqual(undefined, div.style.oTransition);\n\n  testing.expectEqual('rotate(45deg)', div.style['-webkit-transform']);\n  testing.expectEqual('none', div.style['-moz-user-select']);\n}\n</script>\n\n<script id=\"nonExistentProperties\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  testing.expectEqual(undefined, div.style.notARealProperty);\n  testing.expectEqual(undefined, div.style['not-a-real-property']);\n  testing.expectEqual(undefined, div.style.somethingTotallyMadeUp);\n}\n</script>\n\n\n<script id=\"edgeCaseCamelConversion\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('border-top-left-radius', '5px');\n  testing.expectEqual('5px', div.style.borderTopLeftRadius);\n\n  div.style.setProperty('z-index', '100');\n  testing.expectEqual('100', div.style.zIndex);\n}\n</script>\n\n<script id=\"vendorPrefixEdgeCases\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('-webkit-border-radius', '5px');\n  testing.expectEqual('5px', div.style.webkitBorderRadius);\n\n  testing.expectEqual(undefined, div.style.webkitNotSet);\n  testing.expectEqual(undefined, div.style.mozNotSet);\n}\n</script>\n\n<script id=\"propertyAssignment\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  // camelCase assignment\n  div.style.opacity = '0.5';\n  testing.expectEqual('0.5', div.style.opacity);\n\n  // bracket notation assignment\n  div.style['filter'] = 'blur(5px)';\n  testing.expectEqual('blur(5px)', div.style.filter);\n\n  // numeric value coerced to string\n  div.style.opacity = 1;\n  testing.expectEqual('1', div.style.opacity);\n\n  // assigning method names should be ignored (not intercepted)\n  div.style.setProperty('color', 'blue');\n  testing.expectEqual('blue', div.style.color);\n}\n</script>\n\n<script id=\"prototypeChainCheck\">\n{\n  const div = $('#test-div');\n\n  testing.expectEqual(true, typeof div.style.getPropertyValue === 'function');\n  testing.expectEqual(true, typeof div.style.setProperty === 'function');\n  testing.expectEqual(true, typeof div.style.removeProperty === 'function');\n  testing.expectEqual(true, typeof div.style.getPropertyPriority === 'function');\n}\n</script>\n\n\n<div id=crash1 style=\"background-position: 5% .1em\"></div>\n<script id=\"crash_case_1\">\n{\n  testing.expectEqual('5% .1em', $('#crash1').style.backgroundPosition);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/dataset.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"test\" data-foo=\"bar\" data-hello-world=\"test\"></div>\n<div id=\"test-write\"></div>\n<div id=\"test-bracket\"></div>\n\n<script id=basic>\n{\n  const el = document.getElementById('test');\n  testing.expectEqual('object', typeof el.dataset);\n}\n</script>\n\n<script id=readExistingAttributes>\n{\n  const el = document.getElementById('test');\n  testing.expectEqual('bar', el.dataset.foo);\n  testing.expectEqual('test', el.dataset.helloWorld);\n}\n</script>\n\n<script id=camelCaseConversion>\n{\n  const el = document.getElementById('test');\n\n  // Reading kebab-case as camelCase\n  testing.expectEqual('test', el.dataset.helloWorld);\n}\n</script>\n\n<script id=writeNewAttribute>\n{\n  const el = document.getElementById('test-write');\n  el.dataset.newAttr = 'value';\n\n  testing.expectEqual('value', el.dataset.newAttr);\n  testing.expectEqual('value', el.getAttribute('data-new-attr'));\n}\n</script>\n\n<script id=writeExistingAttribute>\n{\n  const el = document.getElementById('test-write');\n  el.setAttribute('data-foo', 'original');\n  el.dataset.foo = 'updated';\n\n  testing.expectEqual('updated', el.dataset.foo);\n  testing.expectEqual('updated', el.getAttribute('data-foo'));\n}\n</script>\n\n<script id=writeCamelCaseConversion>\n{\n  const el = document.getElementById('test-write');\n\n  // Writing camelCase creates kebab-case attribute\n  el.dataset.fooBarBaz = 'qux';\n  testing.expectEqual('qux', el.getAttribute('data-foo-bar-baz'));\n}\n</script>\n\n<script id=undefinedForNonExistent>\n{\n  const el = document.getElementById('test');\n  testing.expectEqual(undefined, el.dataset.nonExistent);\n}\n</script>\n\n<script id=bracketNotation>\n{\n  const el = document.getElementById('test-bracket');\n  el.setAttribute('data-foo', 'bar');\n  el.setAttribute('data-hello-world', 'test');\n\n  // Bracket notation should work the same as dot notation\n  testing.expectEqual('bar', el.dataset['foo']);\n  testing.expectEqual('test', el.dataset['helloWorld']);\n\n  // Non-existent should return undefined\n  testing.expectEqual(undefined, el.dataset['nonExistent']);\n}\n</script>\n\n<script id=edgeCases>\n{\n  const el = document.createElement('div');\n  el.setAttribute('data-x', 'single-letter');\n  el.setAttribute('data-foo-bar-baz', 'multiple-dashes');\n\n  // Single letter key\n  testing.expectEqual('single-letter', el.dataset['x']);\n  testing.expectEqual('single-letter', el.dataset.x);\n\n  // Multiple dashes\n  testing.expectEqual('multiple-dashes', el.dataset['fooBarBaz']);\n  testing.expectEqual('multiple-dashes', el.dataset.fooBarBaz);\n\n  // Empty string key (data- attribute) - should be accessible as empty string\n  el.setAttribute('data-', 'empty');\n  testing.expectEqual('empty', el.dataset['']);\n}\n</script>\n\n<script id=identityCheck>\n{\n  const el = document.getElementById('test');\n  const ds1 = el.dataset;\n  const ds2 = el.dataset;\n\n  // Should return the same object instance\n  testing.expectEqual(true, ds1 === ds2);\n}\n</script>\n\n<script id=deleteProperty>\n{\n  const el = document.createElement('div');\n  el.setAttribute('data-foo', 'bar');\n  el.setAttribute('data-hello-world', 'test');\n\n  testing.expectEqual('bar', el.dataset.foo);\n  testing.expectEqual('test', el.dataset.helloWorld);\n\n  // Delete using dot notation\n  delete el.dataset.foo;\n  testing.expectEqual(undefined, el.dataset.foo);\n  testing.expectEqual(null, el.getAttribute('data-foo'));\n\n  // Delete using bracket notation\n  delete el.dataset['helloWorld'];\n  testing.expectEqual(undefined, el.dataset.helloWorld);\n  testing.expectEqual(null, el.getAttribute('data-hello-world'));\n}\n</script>\n\n<script id=independentInstances>\n{\n  const el1 = document.getElementById('test');\n  const el2 = document.createElement('div');\n\n  el1.dataset.test1 = 'value1';\n  el2.dataset.test2 = 'value2';\n\n  testing.expectEqual('value1', el1.dataset.test1);\n  testing.expectEqual(undefined, el1.dataset.test2);\n  testing.expectEqual(undefined, el2.dataset.test1);\n  testing.expectEqual('value2', el2.dataset.test2);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/duplicate_ids.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"test\">first</div>\n<div id=\"test\">second</div>\n\n<script id=duplicateIds>\n  const first = document.getElementById('test');\n  testing.expectEqual('first', first.textContent);\n\n  first.remove();\n\n  const second = document.getElementById('test');\n  testing.expectEqual('second', second.textContent);\n\n  // second.remove();\n\n  // testing.expectEqual(null, document.getElementById('test'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/element.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\">\n    <!-- comment -->\n    <p id=\"p1\">Paragraph 1</p>\n    <div id=\"div1\">Div 1</div>\n    <!-- comment -->\n    <span id=\"span1\">Span 1</span>\n    <p id=\"p2\">Paragraph 2</p>\n</div>\n<div id=\"empty\" dir=ltr></div>\n\n<script id=\"relatedElements\">\n    const container = $('#container');\n    const p1 = $('#p1');\n    const div1 = $('#div1');\n    const span1 = $('#span1');\n    const p2 = $('#p2');\n    const empty = $('#empty');\n\n    testing.expectEqual(p1, container.firstElementChild);\n    testing.expectEqual(p2, container.lastElementChild);\n\n    testing.expectEqual(div1, p1.nextElementSibling);\n    testing.expectEqual(span1, div1.nextElementSibling);\n    testing.expectEqual(p2, span1.nextElementSibling);\n    testing.expectEqual(null, p2.nextElementSibling);\n\n    testing.expectEqual(span1, p2.previousElementSibling);\n    testing.expectEqual(div1, span1.previousElementSibling);\n    testing.expectEqual(p1, div1.previousElementSibling);\n    testing.expectEqual(null, p1.previousElementSibling);\n\n    testing.expectEqual(null, empty.firstElementChild);\n    testing.expectEqual(null, empty.lastElementChild);\n</script>\n\n<script id=children>\n    const cc = container.children;\n    testing.expectEqual(4, cc.length);\n    testing.expectEqual(p1, cc[0]);\n    testing.expectEqual(p1, cc.item(0));\n    testing.expectEqual(div1, cc[1]);\n    testing.expectEqual(div1, cc.item(1));\n    testing.expectEqual(span1, cc[2]);\n    testing.expectEqual(span1, cc.item(2));\n    testing.expectEqual(p2, cc[3]);\n    testing.expectEqual(p2, cc.item(3));\n\n    const ec = empty.children;\n    testing.expectEqual(0, ec.length);\n    testing.expectEqual(undefined, ec[0]);\n</script>\n\n<script id=nonBreakingSpace>\n  // Test non-breaking space encoding (critical for React hydration)\n  const div = document.createElement('div');\n  div.innerHTML = 'hello\\xa0world';\n  testing.expectEqual('hello\\xa0world', div.textContent);\n  testing.expectEqual('hello&nbsp;world', div.innerHTML);\n\n  // Test that outerHTML also encodes non-breaking spaces correctly\n  const p = document.createElement('p');\n  p.textContent = 'XAnge\\xa0Privacy';\n  testing.expectEqual('<p>XAnge&nbsp;Privacy</p>', p.outerHTML);\n</script>\n\n<script id=element>\n  {\n    const empty = $('#empty');\n    testing.expectEqual('empty', empty.id);\n    testing.expectEqual('ltr', empty.dir);\n\n    // good enough that it doesn't throw\n    empty.scrollIntoViewIfNeeded();\n    empty.scrollIntoViewIfNeeded(false);\n  }\n</script>\n\n<script id=before>\n{\n  const parent = document.createElement('div');\n  const existing = document.createElement('span');\n  parent.appendChild(existing);\n\n  const div1 = document.createElement('div');\n  const div2 = document.createElement('div');\n  existing.before(div1, div2);\n\n  testing.expectEqual(3, parent.childNodes.length);\n  testing.expectEqual(div1, parent.childNodes[0]);\n  testing.expectEqual(div2, parent.childNodes[1]);\n  testing.expectEqual(existing, parent.childNodes[2]);\n\n  existing.before('text node');\n  testing.expectEqual(4, parent.childNodes.length);\n  testing.expectEqual(3, parent.childNodes[2].nodeType);\n  testing.expectEqual('text node', parent.childNodes[2].textContent);\n  testing.expectEqual(existing, parent.childNodes[3]);\n\n  const detached = document.createElement('div');\n  detached.before(document.createElement('p'));\n  testing.expectEqual(null, detached.parentNode);\n}\n</script>\n\n<script id=after>\n{\n  const parent = document.createElement('div');\n  const existing = document.createElement('span');\n  parent.appendChild(existing);\n\n  const div1 = document.createElement('div');\n  const div2 = document.createElement('div');\n  existing.after(div1, div2);\n\n  testing.expectEqual(3, parent.childNodes.length);\n  testing.expectEqual(existing, parent.childNodes[0]);\n  testing.expectEqual(div1, parent.childNodes[1]);\n  testing.expectEqual(div2, parent.childNodes[2]);\n\n  existing.after('text node');\n  testing.expectEqual(4, parent.childNodes.length);\n  testing.expectEqual(existing, parent.childNodes[0]);\n  testing.expectEqual(3, parent.childNodes[1].nodeType);\n  testing.expectEqual('text node', parent.childNodes[1].textContent);\n\n  const detached = document.createElement('div');\n  detached.after(document.createElement('p'));\n  testing.expectEqual(null, detached.parentNode);\n}\n</script>\n\n<script id=beforeAfterOrdering>\n{\n  const parent = document.createElement('div');\n  const a = document.createElement('a');\n  const b = document.createElement('b');\n  const c = document.createElement('c');\n  const d = document.createElement('d');\n\n  parent.appendChild(b);\n  parent.appendChild(c);\n\n  b.before(a);\n  c.after(d);\n\n  testing.expectEqual(4, parent.childNodes.length);\n  testing.expectEqual(a, parent.childNodes[0]);\n  testing.expectEqual(b, parent.childNodes[1]);\n  testing.expectEqual(c, parent.childNodes[2]);\n  testing.expectEqual(d, parent.childNodes[3]);\n}\n</script>\n\n<script id=textContent>\n{\n  const parent = document.createElement('div');\n  parent.appendChild(document.createElement('div'));\n  testing.expectEqual(1, parent.childNodes.length);\n  parent.textContent = '';\n  testing.expectEqual(0, parent.childNodes.length);\n\n  parent.textContent = ' ';\n  testing.expectEqual(1, parent.childNodes.length);\n}\n</script>\n\n<script id=click>\n{\n  let clicked = 0;\n  window.addEventListener('click', (e) => {\n    testing.expectEqual(0, e.clientX);\n    testing.expectEqual(0, e.clientY);\n    clicked += 1;\n  });\n  $('#container').click();\n  testing.expectEqual(1, clicked);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/get_elements_by_class_name.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\" class=\"container-class\">\n  <div class=\"foo\" id=\"foo1\">foo1</div>\n  <span class=\"foo\" id=\"foo2\">foo2</span>\n  <p class=\"foo\" id=\"foo3\">foo3</p>\n  <div class=\"bar\" id=\"bar1\">bar1</div>\n  <div class=\"baz\" id=\"baz1\">baz1</div>\n</div>\n\n<div id=\"nested\" class=\"nested-root\">\n  <div class=\"outer\" id=\"outer\">\n    <div class=\"inner\" id=\"inner1\">inner1</div>\n    <div class=\"inner\" id=\"inner2\">inner2</div>\n    <p class=\"inner\" id=\"inner3\">inner3</p>\n  </div>\n</div>\n\n<div id=\"empty\"></div>\n\n<div id=\"multi\" class=\"multi class names\">\n  <div class=\"multi\" id=\"multi1\">multi1</div>\n  <span class=\"class\" id=\"multi2\">multi2</span>\n  <p class=\"names\" id=\"multi3\">multi3</p>\n</div>\n\n<div id=\"classMatch\" class=\"test-class\">\n  <span class=\"other\">span1</span>\n  <p class=\"other\">p1</p>\n</div>\n\n<script id=basic>\n{\n  const container = $('#container');\n\n  testing.expectEqual(0, container.getElementsByClassName('nonexistent').length);\n  testing.expectEqual(0, container.getElementsByClassName('unknown').length);\n\n  const foos = container.getElementsByClassName('foo');\n  testing.expectEqual(true, foos instanceof HTMLCollection);\n  testing.expectEqual(3, foos.length);\n  testing.expectEqual(3, foos.length); // cache test\n  testing.expectEqual('foo1', foos[0].id);\n  testing.expectEqual('foo2', foos[1].id);\n  testing.expectEqual('foo3', foos[2].id);\n  testing.expectEqual('foo1', foos[0].id); // cache test\n  testing.expectEqual('foo3', foos[2].id);\n  testing.expectEqual('foo2', foos[1].id);\n  testing.expectEqual(undefined, foos[-1]);\n  testing.expectEqual(undefined, foos[3]);\n\n  const bars = container.getElementsByClassName('bar');\n  testing.expectEqual(1, bars.length);\n  testing.expectEqual('bar1', bars[0].id);\n\n  const bazs = container.getElementsByClassName('baz');\n  testing.expectEqual(1, bazs.length);\n  testing.expectEqual('baz1', bazs[0].id);\n}\n</script>\n\n<script id=nestedSearch>\n{\n  const nested = $('#nested');\n  const inners = nested.getElementsByClassName('inner');\n  testing.expectEqual(3, inners.length);\n  testing.expectEqual('inner1', inners[0].id);\n  testing.expectEqual('inner2', inners[1].id);\n  testing.expectEqual('inner3', inners[2].id);\n\n  const outer = $('#outer');\n  const outerInners = outer.getElementsByClassName('inner');\n  testing.expectEqual(3, outerInners.length);\n  testing.expectEqual('inner1', outerInners[0].id);\n  testing.expectEqual('inner2', outerInners[1].id);\n  testing.expectEqual('inner3', outerInners[2].id);\n}\n</script>\n\n<script id=emptyResult>\n{\n  const empty = $('#empty');\n  testing.expectEqual(0, empty.getElementsByClassName('foo').length);\n  testing.expectEqual(0, empty.getElementsByClassName('bar').length);\n  testing.expectEqual(0, empty.getElementsByClassName('').length);\n}\n</script>\n\n<script id=excludeSelf>\n{\n  // Element itself should NOT be included in results, even if it matches\n  const container = $('#container');\n  const containerClass = container.getElementsByClassName('container-class');\n  testing.expectEqual(0, containerClass.length); // container has 'container-class' but should not be included\n\n  const classMatch = $('#classMatch');\n  const testClass = classMatch.getElementsByClassName('test-class');\n  testing.expectEqual(0, testClass.length); // classMatch itself has 'test-class' but should not be included\n\n  const others = classMatch.getElementsByClassName('other');\n  testing.expectEqual(2, others.length); // Only the child elements\n}\n</script>\n\n<script id=item>\n{\n  const container = $('#container');\n  const foos = container.getElementsByClassName('foo');\n\n  testing.expectEqual('foo1', foos.item(0).id);\n  testing.expectEqual('foo2', foos.item(1).id);\n  testing.expectEqual('foo3', foos.item(2).id);\n  testing.expectEqual(null, foos.item(3));\n  testing.expectEqual(null, foos.item(-1));\n  testing.expectEqual(null, foos.item(100));\n}\n</script>\n\n<script id=namedItem>\n{\n  const container = $('#container');\n  const foos = container.getElementsByClassName('foo');\n\n  testing.expectEqual('foo1', foos.namedItem('foo1').id);\n  testing.expectEqual('foo2', foos.namedItem('foo2').id);\n  testing.expectEqual('foo3', foos.namedItem('foo3').id);\n  testing.expectEqual(null, foos.namedItem('foo4'));\n  testing.expectEqual(null, foos.namedItem('container'));\n\n  testing.expectEqual('foo1', foos['foo1'].id);\n  testing.expectEqual('foo2', foos['foo2'].id);\n}\n</script>\n\n<script id=iterator>\n{\n  const container = $('#container');\n  const foos = container.getElementsByClassName('foo');\n\n  let acc = [];\n  for (let el of foos) {\n    acc.push(el.id);\n  }\n  testing.expectEqual(['foo1', 'foo2', 'foo3'], acc);\n}\n</script>\n\n<script id=multipleClasses>\n{\n  const multi = $('#multi');\n\n  const multiClass = multi.getElementsByClassName('multi');\n  testing.expectEqual(1, multiClass.length);\n  testing.expectEqual('multi1', multiClass[0].id);\n\n  const classClass = multi.getElementsByClassName('class');\n  testing.expectEqual(1, classClass.length);\n  testing.expectEqual('multi2', classClass[0].id);\n\n  const namesClass = multi.getElementsByClassName('names');\n  testing.expectEqual(1, namesClass.length);\n  testing.expectEqual('multi3', namesClass[0].id);\n}\n</script>\n\n<script id=arrayIndexing>\n{\n  const container = $('#container');\n  const foos = container.getElementsByClassName('foo');\n\n  testing.expectEqual('foo1', foos[0].id);\n  testing.expectEqual('foo2', foos[1].id);\n  testing.expectEqual('foo3', foos[2].id);\n  testing.expectEqual(undefined, foos[3]);\n  testing.expectEqual(undefined, foos[-1]);\n  testing.expectEqual(undefined, foos[100]);\n}\n</script>\n\n<script id=emptyString>\n{\n  const container = $('#container');\n  const empty = container.getElementsByClassName('');\n  testing.expectEqual(0, empty.length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/get_elements_by_tag_name.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\">\n  <div id=\"div1\">div1</div>\n  <p id=\"p1\">p1</p>\n  <div id=\"div2\">div2</div>\n  <span id=\"span1\">span1</span>\n  <p id=\"p2\">p2</p>\n</div>\n\n<div id=\"nested\">\n  <div id=\"outer\">\n    <div id=\"inner1\">inner1</div>\n    <p id=\"innerP\">innerP</p>\n    <div id=\"inner2\">inner2</div>\n  </div>\n</div>\n\n<div id=\"empty\"></div>\n\n<div id=\"divMatch\" class=\"test\">\n  <span>span1</span>\n  <p>p1</p>\n  <span>span2</span>\n</div>\n\n<h1>H1</h1>\n<h2>H2</h2>\n<h3>H3</h3>\n<section id=\"headings\">\n  <h1>Section H1</h1>\n  <h2>Section H2</h2>\n  <h3>Section H3</h3>\n  <h4>Section H4</h4>\n  <h5>Section H5</h5>\n  <h6>Section H6</h6>\n</section>\n\n<script id=basic>\n{\n  const container = $('#container');\n\n  testing.expectEqual(5, container.getElementsByTagName('*').length);\n  testing.expectEqual(0, container.getElementsByTagName('a').length);\n  testing.expectEqual(0, container.getElementsByTagName('unknown').length);\n\n  const divs = container.getElementsByTagName('div');\n  testing.expectEqual(true, divs instanceof HTMLCollection);\n  testing.expectEqual(2, divs.length);\n  testing.expectEqual('div1', divs[0].id);\n  testing.expectEqual('div2', divs[1].id);\n  testing.expectEqual('div1', divs[0].id); // cache test\n\n  const ps = container.getElementsByTagName('p');\n  testing.expectEqual(2, ps.length);\n  testing.expectEqual('p1', ps[0].id);\n  testing.expectEqual('p2', ps[1].id);\n\n  const spans = container.getElementsByTagName('span');\n  testing.expectEqual(1, spans.length);\n  testing.expectEqual('span1', spans[0].id);\n}\n</script>\n\n<script id=nestedSearch>\n{\n  const nested = $('#nested');\n  const nestedDivs = nested.getElementsByTagName('div');\n  testing.expectEqual(3, nestedDivs.length);\n  testing.expectEqual('outer', nestedDivs[0].id);\n  testing.expectEqual('inner1', nestedDivs[1].id);\n  testing.expectEqual('inner2', nestedDivs[2].id);\n\n  const outer = $('#outer');\n  const outerDivs = outer.getElementsByTagName('div');\n  testing.expectEqual(2, outerDivs.length);\n  testing.expectEqual('inner1', outerDivs[0].id);\n  testing.expectEqual('inner2', outerDivs[1].id);\n}\n</script>\n\n<script id=emptyResult>\n{\n  const empty = $('#empty');\n  testing.expectEqual(0, empty.getElementsByTagName('div').length);\n  testing.expectEqual(0, empty.getElementsByTagName('p').length);\n  testing.expectEqual(0, empty.getElementsByTagName('span').length);\n}\n</script>\n\n<script id=excludeSelf>\n{\n  // Element itself should NOT be included in results, even if it matches\n  const divMatch = $('#divMatch');\n  const divsInDiv = divMatch.getElementsByTagName('div');\n  testing.expectEqual(0, divsInDiv.length); // divMatch itself is a div but should not be included\n\n  const spansInDiv = divMatch.getElementsByTagName('span');\n  testing.expectEqual(2, spansInDiv.length); // Only the child spans\n}\n</script>\n\n<script id=caseInsensitive>\n{\n  const container = $('#container');\n  testing.expectEqual(2, container.getElementsByTagName('DIV').length);\n  testing.expectEqual(2, container.getElementsByTagName('Div').length);\n  testing.expectEqual(2, container.getElementsByTagName('div').length);\n  testing.expectEqual(2, container.getElementsByTagName('P').length);\n  testing.expectEqual(1, container.getElementsByTagName('SPAN').length);\n}\n</script>\n\n<script id=item>\n{\n  const container = $('#container');\n  const divs = container.getElementsByTagName('div');\n\n  testing.expectEqual('div1', divs.item(0).id);\n  testing.expectEqual('div2', divs.item(1).id);\n  testing.expectEqual(null, divs.item(2));\n  testing.expectEqual(null, divs.item(-1));\n  testing.expectEqual(null, divs.item(100));\n}\n</script>\n\n<script id=namedItem>\n{\n  const container = $('#container');\n  const divs = container.getElementsByTagName('div');\n\n  testing.expectEqual('div1', divs.namedItem('div1').id);\n  testing.expectEqual('div2', divs.namedItem('div2').id);\n  testing.expectEqual(null, divs.namedItem('div3'));\n  testing.expectEqual(null, divs.namedItem('container'));\n\n  testing.expectEqual('div1', divs['div1'].id);\n  testing.expectEqual('div2', divs['div2'].id);\n}\n</script>\n\n<script id=iterator>\n{\n  const container = $('#container');\n  const ps = container.getElementsByTagName('p');\n\n  let acc = [];\n  for (let p of ps) {\n    acc.push(p.id);\n  }\n  testing.expectEqual(['p1', 'p2'], acc);\n}\n</script>\n\n<script id=headings>\n{\n  const headings = $('#headings');\n\n  testing.expectEqual(1, headings.getElementsByTagName('h1').length);\n  testing.expectEqual('Section H1', headings.getElementsByTagName('h1')[0].textContent);\n  testing.expectEqual(1, headings.getElementsByTagName('h2').length);\n  testing.expectEqual('Section H2', headings.getElementsByTagName('h2')[0].textContent);\n  testing.expectEqual(1, headings.getElementsByTagName('h3').length);\n  testing.expectEqual(1, headings.getElementsByTagName('h4').length);\n  testing.expectEqual(1, headings.getElementsByTagName('h5').length);\n  testing.expectEqual(1, headings.getElementsByTagName('h6').length);\n\n  // Case insensitive\n  testing.expectEqual(1, headings.getElementsByTagName('H1').length);\n  testing.expectEqual('Section H1', headings.getElementsByTagName('H1')[0].textContent);\n}\n</script>\n\n<script id=arrayIndexing>\n{\n  const container = $('#container');\n  const divs = container.getElementsByTagName('div');\n\n  testing.expectEqual('div1', divs[0].id);\n  testing.expectEqual('div2', divs[1].id);\n  testing.expectEqual(undefined, divs[2]);\n  testing.expectEqual(undefined, divs[-1]);\n  testing.expectEqual(undefined, divs[100]);\n}\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/element/get_elements_by_tag_name_ns.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\" xmlns=\"http://www.w3.org/1999/xhtml\">\n  <div id=\"div1\">div1</div>\n  <p id=\"p1\">p1</p>\n  <div id=\"div2\">div2</div>\n</div>\n\n<svg id=\"svgContainer\" xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\">\n  <circle id=\"circle1\" cx=\"50\" cy=\"50\" r=\"40\"/>\n  <rect id=\"rect1\" x=\"10\" y=\"10\" width=\"30\" height=\"30\"/>\n  <circle id=\"circle2\" cx=\"25\" cy=\"25\" r=\"10\"/>\n</svg>\n\n<div id=\"mixed\">\n  <div id=\"htmlDiv\" xmlns=\"http://www.w3.org/1999/xhtml\">HTML div</div>\n  <svg xmlns=\"http://www.w3.org/2000/svg\">\n    <circle id=\"svgCircle\" cx=\"10\" cy=\"10\" r=\"5\"/>\n  </svg>\n</div>\n\n<script id=basic>\n{\n  const htmlNS = \"http://www.w3.org/1999/xhtml\";\n  const svgNS = \"http://www.w3.org/2000/svg\";\n\n  // Test HTML namespace\n  const htmlDivs = document.getElementsByTagNameNS(htmlNS, 'div');\n  testing.expectEqual(true, htmlDivs instanceof HTMLCollection);\n  testing.expectEqual(5, htmlDivs.length); // container, div1, div2, mixed, htmlDiv\n\n  const htmlPs = document.getElementsByTagNameNS(htmlNS, 'p');\n  testing.expectEqual(1, htmlPs.length);\n  testing.expectEqual('p1', htmlPs[0].id);\n}\n</script>\n\n<script id=svgNamespace>\n{\n  const svgNS = \"http://www.w3.org/2000/svg\";\n\n  const circles = document.getElementsByTagNameNS(svgNS, 'circle');\n  testing.expectEqual(3, circles.length); // circle1, circle2, svgCircle\n  testing.expectEqual('circle1', circles[0].id);\n  testing.expectEqual('circle2', circles[1].id);\n  testing.expectEqual('svgCircle', circles[2].id);\n\n  const rects = document.getElementsByTagNameNS(svgNS, 'rect');\n  testing.expectEqual(1, rects.length);\n  testing.expectEqual('rect1', rects[0].id);\n}\n</script>\n\n<script id=nullNamespace>\n{\n  // Null namespace should match elements with null namespace\n  const nullNsElements = document.getElementsByTagNameNS(null, 'div');\n  testing.expectEqual(0, nullNsElements.length); // Our divs are in HTML namespace\n}\n</script>\n\n<script id=wildcardNamespace>\n{\n  // Wildcard namespace \"*\" should match all namespaces\n  const allDivs = document.getElementsByTagNameNS('*', 'div');\n  testing.expectEqual(5, allDivs.length); // All divs regardless of namespace\n}\n</script>\n\n<script id=wildcardLocalName>\n{\n  const htmlNS = \"http://www.w3.org/1999/xhtml\";\n\n  // Wildcard local name should match all elements in that namespace\n  const allHtmlElements = document.getElementsByTagNameNS(htmlNS, '*');\n  testing.expectEqual(true, allHtmlElements.length > 0);\n}\n</script>\n\n<script id=caseSensitive>\n{\n  const htmlNS = \"http://www.w3.org/1999/xhtml\";\n\n  // getElementsByTagNameNS is case-sensitive for local names\n  const lowerDivs = document.getElementsByTagNameNS(htmlNS, 'div');\n  const upperDivs = document.getElementsByTagNameNS(htmlNS, 'DIV');\n\n  testing.expectEqual(5, lowerDivs.length);\n  testing.expectEqual(0, upperDivs.length); // Should be 0 because it's case-sensitive\n}\n</script>\n\n<script id=unknownNamespace>\n{\n  // Unknown namespace should still work\n  const unknownNs = document.getElementsByTagNameNS('http://example.com/unknown', 'div');\n  testing.expectEqual(0, unknownNs.length);\n}\n</script>\n\n<script id=emptyResult>\n{\n  const htmlNS = \"http://www.w3.org/1999/xhtml\";\n  const svgNS = \"http://www.w3.org/2000/svg\";\n\n  testing.expectEqual(0, document.getElementsByTagNameNS(htmlNS, 'nonexistent').length);\n  testing.expectEqual(0, document.getElementsByTagNameNS(svgNS, 'nonexistent').length);\n}\n</script>\n\n<script id=elementMethod>\n{\n  const htmlNS = \"http://www.w3.org/1999/xhtml\";\n  const container = document.getElementById('container');\n\n  // getElementsByTagNameNS on element should only search descendants\n  const divsInContainer = container.getElementsByTagNameNS(htmlNS, 'div');\n  testing.expectEqual(2, divsInContainer.length); // div1, div2 (not container itself)\n  testing.expectEqual('div1', divsInContainer[0].id);\n  testing.expectEqual('div2', divsInContainer[1].id);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/anchor.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Test anchors with various href values -->\n<a id=a0></a>\n<a href=\"../anchor1.html\" id=a1></a>\n<a href=\"/hello/world/anchor2.html\" id=a2></a>\n<a href=\"https://www.openmymind.net/Elixirs-With-Statement/\" id=a3></a>\n<a id=link href=foo>OK</a>\n\n<script id=empty_href>\n  testing.expectEqual('', $('#a0').href);\n\n  testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);\n  testing.expectEqual(testing.ORIGIN + '/hello/world/anchor2.html', $('#a2').href);\n  testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);\n\n  testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);\n</script>\n\n<script id=dynamic_anchor_defaults>\n{\n  let a = document.createElement('a');\n\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.host);\n  testing.expectEqual('', a.hostname);\n  testing.expectEqual('', a.port);\n  testing.expectEqual('', a.pathname);\n  testing.expectEqual('', a.search);\n  testing.expectEqual('', a.hash);\n  testing.expectEqual('', a.origin);\n  testing.expectEqual('', a.target);\n  testing.expectEqual('', a.type);\n  testing.expectEqual('', a.text);\n}\n</script>\n\n<script id=dynamic_anchor_empty_href_behavior>\n{\n  let a = document.createElement('a');\n\n  a.search = 'q=test';\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.search);\n\n  a.hash = 'section';\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.hash);\n\n  a.port = '8080';\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.port);\n\n  a.hostname = 'example.com';\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.hostname);\n\n  a.host = 'example.com:9000';\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.host);\n}\n</script>\n\n<script id=dynamic_anchor_with_href>\n{\n  let a = document.createElement('a');\n  a.href = 'https://lightpanda.io/';\n\n  testing.expectEqual('https://lightpanda.io/', a.href);\n  testing.expectEqual('lightpanda.io', a.host);\n  testing.expectEqual('lightpanda.io', a.hostname);\n  testing.expectEqual('', a.port);\n  testing.expectEqual('/', a.pathname);\n  testing.expectEqual('', a.search);\n  testing.expectEqual('', a.hash);\n  testing.expectEqual('https://lightpanda.io', a.origin);\n}\n</script>\n\n<script id=anchor_url_manipulation>\n{\n  let link = document.createElement('a');\n  link.href = 'https://lightpanda.io/';\n\n  testing.expectEqual('', link.target);\n  link.target = '_blank';\n  testing.expectEqual('_blank', link.target);\n  link.target = '';\n  testing.expectEqual('', link.target);\n\n  testing.expectEqual('https://lightpanda.io', link.origin);\n\n  link.host = 'lightpanda.io:443';\n  testing.expectEqual('lightpanda.io', link.host);\n  testing.expectEqual('', link.port);\n  testing.expectEqual('lightpanda.io', link.hostname);\n  testing.expectEqual('https://lightpanda.io/', link.href);\n\n  link.host = 'lightpanda.io';\n  testing.expectEqual('lightpanda.io', link.host);\n  testing.expectEqual('', link.port);\n  testing.expectEqual('lightpanda.io', link.hostname);\n\n  link.hostname = 'foo.bar';\n  testing.expectEqual('foo.bar', link.host);\n  testing.expectEqual('foo.bar', link.hostname);\n  testing.expectEqual('https://foo.bar/', link.href);\n\n  testing.expectEqual('', link.search);\n  link.search = 'q=bar';\n  testing.expectEqual('?q=bar', link.search);\n  testing.expectEqual('https://foo.bar/?q=bar', link.href);\n\n  testing.expectEqual('', link.hash);\n  link.hash = 'frag';\n  testing.expectEqual('#frag', link.hash);\n  testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);\n\n  testing.expectEqual('', link.port);\n  link.port = '443';\n  testing.expectEqual('', link.port);\n  testing.expectEqual('foo.bar', link.host);\n  testing.expectEqual('foo.bar', link.hostname);\n  testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);\n\n  link.port = null;\n  testing.expectEqual('', link.port);\n  testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);\n\n  link.href = 'foo';\n  testing.expectEqual(testing.BASE_URL + 'element/html/foo', link.href);\n\n  testing.expectEqual('', link.type);\n  link.type = 'text/html';\n  testing.expectEqual('text/html', link.type);\n\n  testing.expectEqual('', link.text);\n  link.text = 'Click here';\n  testing.expectEqual('Click here', link.text);\n}\n</script>\n\n<script id=anchor_port_non_default>\n{\n  let a = document.createElement('a');\n  a.href = 'https://example.com:8443/path';\n\n  testing.expectEqual('example.com:8443', a.host);\n  testing.expectEqual('example.com', a.hostname);\n  testing.expectEqual('8443', a.port);\n  testing.expectEqual('https://example.com:8443/path', a.href);\n\n  a.href = 'http://example.com:8080/path';\n  testing.expectEqual('example.com:8080', a.host);\n  testing.expectEqual('8080', a.port);\n\n  a.href = 'http://example.com:80/path';\n  testing.expectEqual('example.com', a.host);\n  testing.expectEqual('', a.port);\n}\n</script>\n\n<script id=anchor_special_chars>\n{\n  let a = document.createElement('a');\n  a.href = 'https://example.com/';\n\n  a.search = '?test=1';\n  testing.expectEqual('?test=1', a.search);\n\n  a.search = 'test=2';\n  testing.expectEqual('?test=2', a.search);\n\n  a.hash = '#section';\n  testing.expectEqual('#section', a.hash);\n\n  a.hash = 'other';\n  testing.expectEqual('#other', a.hash);\n\n  a.search = '';\n  testing.expectEqual('', a.search);\n  testing.expectEqual('https://example.com/#other', a.href);\n\n  a.hash = '';\n  testing.expectEqual('', a.hash);\n  testing.expectEqual('https://example.com/', a.href);\n}\n</script>\n\n<script id=anchor_name_attribute>\n{\n  let a = document.createElement('a');\n  testing.expectEqual('', a.name);\n\n  a.name = 'myanchor';\n  testing.expectEqual('myanchor', a.name);\n\n  a.name = '';\n  testing.expectEqual('', a.name);\n}\n</script>\n\n<script id=anchor_pathname>\n{\n  let a = document.createElement('a');\n  a.href = 'https://example.com/path/to/page';\n\n  testing.expectEqual('/path/to/page', a.pathname);\n\n  a.pathname = '/new/path';\n  testing.expectEqual('/new/path', a.pathname);\n  testing.expectEqual('https://example.com/new/path', a.href);\n\n  a.pathname = 'another';\n  testing.expectEqual('/another', a.pathname);\n  testing.expectEqual('https://example.com/another', a.href);\n}\n</script>\n\n<script id=anchor_protocol>\n{\n  let a = document.createElement('a');\n  a.href = 'https://example.com/';\n\n  testing.expectEqual('https:', a.protocol);\n\n  a.protocol = 'http:';\n  testing.expectEqual('http:', a.protocol);\n  testing.expectEqual('http://example.com/', a.href);\n\n  a.protocol = 'https';\n  testing.expectEqual('https:', a.protocol);\n  testing.expectEqual('https://example.com/', a.href);\n}\n</script>\n\n<script id=toString>\n{\n  let a = document.createElement('a');\n  a.href = 'https://example.com/test';\n  testing.expectEqual('https://example.com/test', a.toString());\n\n  let b = document.createElement('a');\n  testing.expectEqual('', b.toString());\n}\n</script>\n\n<script id=url_encode>\n  {\n    let a = document.createElement('a');\n    a.href = 'over 9000!';\n    testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!', a.href);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/button.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Button elements -->\n<button id=\"button1\">Click me</button>\n<button id=\"button2\" disabled>Disabled button</button>\n\n<!-- Form association tests -->\n<form id=\"form1\">\n  <button id=\"button_in_form\">In form</button>\n</form>\n\n<form id=\"form2\"></form>\n<button id=\"button_with_form_attr\" form=\"form2\">With form attr</button>\n\n<button id=\"button_no_form\">No form</button>\n\n<form id=\"form3\">\n  <button id=\"button_invalid_form_attr\" form=\"nonexistent\">Invalid form</button>\n</form>\n\n<script id=\"disabled_initial\">\n  testing.expectEqual(false, $('#button1').disabled)\n  testing.expectEqual(true, $('#button2').disabled)\n</script>\n\n<script id=\"disabled_set\">\n  $('#button1').disabled = true\n  testing.expectEqual(true, $('#button1').disabled)\n\n  $('#button2').disabled = false\n  testing.expectEqual(false, $('#button2').disabled)\n</script>\n\n<script id=\"form_ancestor\">\n  const buttonInForm = $('#button_in_form')\n  testing.expectEqual('FORM', buttonInForm.form.tagName)\n  testing.expectEqual('form1', buttonInForm.form.id)\n</script>\n\n<script id=\"form_attribute\">\n  const buttonWithFormAttr = $('#button_with_form_attr')\n  testing.expectEqual('FORM', buttonWithFormAttr.form.tagName)\n  testing.expectEqual('form2', buttonWithFormAttr.form.id)\n</script>\n\n<script id=\"form_null\">\n  const buttonNoForm = $('#button_no_form')\n  testing.expectEqual(null, buttonNoForm.form)\n</script>\n\n<script id=\"form_invalid_attribute\">\n  const buttonInvalidFormAttr = $('#button_invalid_form_attr')\n  testing.expectEqual(null, buttonInvalidFormAttr.form)\n</script>\n\n<button id=\"named1\" name=\"submit-btn\"></button>\n<button id=\"named2\"></button>\n\n<button id=\"required1\" required></button>\n<button id=\"required2\"></button>\n\n<script id=\"name_initial\">\n  testing.expectEqual('submit-btn', $('#named1').name)\n  testing.expectEqual('', $('#named2').name)\n</script>\n\n<script id=\"name_set\">\n  {\n    const button = document.createElement('button')\n    testing.expectEqual('', button.name)\n\n    button.name = 'action'\n    testing.expectEqual('action', button.name)\n    testing.expectEqual('action', button.getAttribute('name'))\n\n    button.name = 'submit'\n    testing.expectEqual('submit', button.name)\n    testing.expectEqual('submit', button.getAttribute('name'))\n  }\n</script>\n\n<script id=\"name_reflects_to_attribute\">\n  {\n    const button = document.createElement('button')\n    testing.expectEqual(null, button.getAttribute('name'))\n\n    button.name = 'fieldname'\n    testing.expectEqual('fieldname', button.getAttribute('name'))\n    testing.expectTrue(button.outerHTML.includes('name=\"fieldname\"'))\n  }\n</script>\n\n<script id=\"required_initial\">\n  testing.expectEqual(true, $('#required1').required)\n  testing.expectEqual(false, $('#required2').required)\n</script>\n\n<script id=\"required_set\">\n  {\n    const button = document.createElement('button')\n    testing.expectEqual(false, button.required)\n\n    button.required = true\n    testing.expectEqual(true, button.required)\n    testing.expectEqual('', button.getAttribute('required'))\n\n    button.required = false\n    testing.expectEqual(false, button.required)\n    testing.expectEqual(null, button.getAttribute('required'))\n  }\n</script>\n\n<script id=\"required_reflects_to_attribute\">\n  {\n    const button = document.createElement('button')\n    testing.expectEqual(null, button.getAttribute('required'))\n    testing.expectFalse(button.outerHTML.includes('required'))\n\n    button.required = true\n    testing.expectEqual('', button.getAttribute('required'))\n    testing.expectTrue(button.outerHTML.includes('required'))\n\n    button.required = false\n    testing.expectEqual(null, button.getAttribute('required'))\n    testing.expectFalse(button.outerHTML.includes('required'))\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/details.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Details elements -->\n<details id=\"details1\">\n  <summary>Summary</summary>\n  Content\n</details>\n<details id=\"details2\" open>\n  <summary>Open Summary</summary>\n  Content\n</details>\n\n<script id=\"instanceof\">\n  {\n    const details = document.createElement('details')\n    testing.expectTrue(details instanceof HTMLDetailsElement)\n  }\n</script>\n\n<script id=\"open_initial\">\n  testing.expectEqual(false, $('#details1').open)\n  testing.expectEqual(true, $('#details2').open)\n</script>\n\n<script id=\"open_set\">\n  {\n    $('#details1').open = true\n    testing.expectEqual(true, $('#details1').open)\n\n    $('#details2').open = false\n    testing.expectEqual(false, $('#details2').open)\n  }\n</script>\n\n<script id=\"open_reflects_attribute\">\n  {\n    const details = document.createElement('details')\n    testing.expectEqual(null, details.getAttribute('open'))\n\n    details.open = true\n    testing.expectEqual('', details.getAttribute('open'))\n\n    details.open = false\n    testing.expectEqual(null, details.getAttribute('open'))\n  }\n</script>\n\n<script id=\"name_initial\">\n  {\n    const details = document.createElement('details')\n    testing.expectEqual('', details.name)\n  }\n</script>\n\n<script id=\"name_set\">\n  {\n    const details = document.createElement('details')\n    details.name = 'group1'\n    testing.expectEqual('group1', details.name)\n    testing.expectEqual('group1', details.getAttribute('name'))\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/dialog.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Dialog elements -->\n<dialog id=\"dialog1\">Dialog content</dialog>\n<dialog id=\"dialog2\" open>Open dialog</dialog>\n\n<script id=\"open_initial\">\n  testing.expectEqual(false, $('#dialog1').open)\n  testing.expectEqual(true, $('#dialog2').open)\n</script>\n\n<script id=\"open_set\">\n  {\n    $('#dialog1').open = true\n    testing.expectEqual(true, $('#dialog1').open)\n\n    $('#dialog2').open = false\n    testing.expectEqual(false, $('#dialog2').open)\n  }\n</script>\n\n<script id=\"open_reflects_to_attribute\">\n  {\n    const dialog = document.createElement('dialog')\n    testing.expectEqual(null, dialog.getAttribute('open'))\n    testing.expectFalse(dialog.outerHTML.includes('open'))\n\n    dialog.open = true\n    testing.expectEqual('', dialog.getAttribute('open'))\n    testing.expectTrue(dialog.outerHTML.includes('open'))\n\n    dialog.open = false\n    testing.expectEqual(null, dialog.getAttribute('open'))\n    testing.expectFalse(dialog.outerHTML.includes('open'))\n  }\n</script>\n\n<script id=\"returnValue_initial\">\n  {\n    const dialog = document.createElement('dialog')\n    testing.expectEqual('', dialog.returnValue)\n  }\n</script>\n\n<script id=\"returnValue_set\">\n  {\n    const dialog = document.createElement('dialog')\n    testing.expectEqual('', dialog.returnValue)\n\n    dialog.returnValue = 'success'\n    testing.expectEqual('success', dialog.returnValue)\n    testing.expectEqual('success', dialog.getAttribute('returnvalue'))\n\n    dialog.returnValue = 'cancel'\n    testing.expectEqual('cancel', dialog.returnValue)\n    testing.expectEqual('cancel', dialog.getAttribute('returnvalue'))\n  }\n</script>\n\n<script id=\"returnValue_reflects_to_attribute\">\n  {\n    const dialog = document.createElement('dialog')\n    testing.expectEqual(null, dialog.getAttribute('returnvalue'))\n\n    dialog.returnValue = 'confirmed'\n    testing.expectEqual('confirmed', dialog.getAttribute('returnvalue'))\n    testing.expectTrue(dialog.outerHTML.includes('returnvalue=\"confirmed\"'))\n  }\n</script>\n\n<script id=\"element_type\">\n  {\n    const dialog = document.createElement('dialog')\n    testing.expectEqual('DIALOG', dialog.tagName)\n    testing.expectTrue(dialog instanceof HTMLDialogElement)\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/event_listeners.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Test inline event listeners set via HTML attributes -->\n<div id=\"attr-click\" onclick=\"window.x = 1\"></div>\n<div id=\"attr-load\" onload=\"window.x = 1\"></div>\n<div id=\"attr-error\" onerror=\"window.x = 1\"></div>\n<div id=\"attr-focus\" onfocus=\"window.x = 1\"></div>\n<div id=\"attr-blur\" onblur=\"window.x = 1\"></div>\n<div id=\"attr-keydown\" onkeydown=\"window.x = 1\"></div>\n<div id=\"attr-mousedown\" onmousedown=\"window.x = 1\"></div>\n<div id=\"attr-submit\" onsubmit=\"window.x = 1\"></div>\n<div id=\"attr-wheel\" onwheel=\"window.x = 1\"></div>\n<div id=\"attr-scroll\" onscroll=\"window.x = 1\"></div>\n<div id=\"attr-contextmenu\" oncontextmenu=\"window.x = 1\"></div>\n<div id=\"no-listeners\"></div>\n\n<script id=\"attr_listener_returns_function\">\n{\n    // Inline listeners set via HTML attributes should return a function\n    testing.expectEqual('function', typeof $('#attr-click').onclick);\n    testing.expectEqual('function', typeof $('#attr-load').onload);\n    testing.expectEqual('function', typeof $('#attr-error').onerror);\n    testing.expectEqual('function', typeof $('#attr-focus').onfocus);\n    testing.expectEqual('function', typeof $('#attr-blur').onblur);\n    testing.expectEqual('function', typeof $('#attr-keydown').onkeydown);\n    testing.expectEqual('function', typeof $('#attr-mousedown').onmousedown);\n    testing.expectEqual('function', typeof $('#attr-submit').onsubmit);\n    testing.expectEqual('function', typeof $('#attr-wheel').onwheel);\n    testing.expectEqual('function', typeof $('#attr-scroll').onscroll);\n    testing.expectEqual('function', typeof $('#attr-contextmenu').oncontextmenu);\n}\n</script>\n\n<script id=\"no_attr_listener_returns_null\">\n{\n    // Elements without inline listeners should return null\n    const div = $('#no-listeners');\n    testing.expectEqual(null, div.onclick);\n    testing.expectEqual(null, div.onload);\n    testing.expectEqual(null, div.onerror);\n    testing.expectEqual(null, div.onfocus);\n    testing.expectEqual(null, div.onblur);\n    testing.expectEqual(null, div.onkeydown);\n    testing.expectEqual(null, div.onmousedown);\n    testing.expectEqual(null, div.onsubmit);\n    testing.expectEqual(null, div.onwheel);\n    testing.expectEqual(null, div.onscroll);\n    testing.expectEqual(null, div.oncontextmenu);\n}\n</script>\n\n<script id=\"js_setter_getter\">\n{\n    // Test setting and getting listeners via JavaScript property\n    const div = document.createElement('div');\n\n    // Initially null\n    testing.expectEqual(null, div.onclick);\n    testing.expectEqual(null, div.onload);\n    testing.expectEqual(null, div.onerror);\n\n    // Set listeners\n    const clickHandler = () => {};\n    const loadHandler = () => {};\n    const errorHandler = () => {};\n\n    div.onclick = clickHandler;\n    div.onload = loadHandler;\n    div.onerror = errorHandler;\n\n    // Verify they can be retrieved and are functions\n    testing.expectEqual('function', typeof div.onclick);\n    testing.expectEqual('function', typeof div.onload);\n    testing.expectEqual('function', typeof div.onerror);\n}\n</script>\n\n<script id=\"js_listener_invoke\">\n{\n    // Test that JS-set listeners can be invoked directly\n    const div = document.createElement('div');\n    window.jsInvokeResult = 0;\n\n    div.onclick = () => { window.jsInvokeResult = 100; };\n    div.onclick();\n    testing.expectEqual(100, window.jsInvokeResult);\n\n    div.onload = () => { window.jsInvokeResult = 200; };\n    div.onload();\n    testing.expectEqual(200, window.jsInvokeResult);\n\n    div.onfocus = () => { window.jsInvokeResult = 300; };\n    div.onfocus();\n    testing.expectEqual(300, window.jsInvokeResult);\n}\n</script>\n\n<script id=\"js_listener_invoke_with_return\">\n{\n    // Test that JS-set listeners return values when invoked\n    const div = document.createElement('div');\n\n    div.onclick = () => { return 'click-result'; };\n    testing.expectEqual('click-result', div.onclick());\n\n    div.onload = () => { return 42; };\n    testing.expectEqual(42, div.onload());\n\n    div.onfocus = () => { return { key: 'value' }; };\n    testing.expectEqual('value', div.onfocus().key);\n}\n</script>\n\n<script id=\"js_listener_invoke_with_args\">\n{\n    // Test that JS-set listeners can receive arguments when invoked\n    const div = document.createElement('div');\n\n    div.onclick = (a, b) => { return a + b; };\n    testing.expectEqual(15, div.onclick(10, 5));\n\n    div.onload = (msg) => { return 'Hello, ' + msg; };\n    testing.expectEqual('Hello, World', div.onload('World'));\n}\n</script>\n\n<script id=\"js_setter_override\">\n{\n    // Test that setting a new listener overrides the old one\n    const div = document.createElement('div');\n\n    const first = () => { return 1; };\n    const second = () => { return 2; };\n\n    div.onclick = first;\n    testing.expectEqual('function', typeof div.onclick);\n    testing.expectEqual(1, div.onclick());\n\n    div.onclick = second;\n    testing.expectEqual('function', typeof div.onclick);\n    testing.expectEqual(2, div.onclick());\n}\n</script>\n\n<script id=\"js_setter_null_clears_listener\">\n{\n    // Setting an event handler property to null must silently clear it (not throw).\n    // Browsers also accept undefined and non-function values without throwing.\n    const div = document.createElement('div');\n\n    div.onload = () => 42;\n    testing.expectEqual('function', typeof div.onload);\n\n    // Setting to null removes the listener; getter returns null\n    div.onload = null;\n    testing.expectEqual(null, div.onload);\n\n    div.onerror = () => {};\n    div.onerror = null;\n    testing.expectEqual(null, div.onerror);\n\n    div.onclick = () => {};\n    div.onclick = null;\n    testing.expectEqual(null, div.onclick);\n}\n</script>\n\n<script id=\"different_event_types_independent\">\n{\n    // Test that different event types are stored independently\n    const div = document.createElement('div');\n\n    const clickFn = () => {};\n    const focusFn = () => {};\n    const blurFn = () => {};\n\n    div.onclick = clickFn;\n    testing.expectEqual('function', typeof div.onclick);\n    testing.expectEqual(null, div.onfocus);\n    testing.expectEqual(null, div.onblur);\n\n    div.onfocus = focusFn;\n    testing.expectEqual('function', typeof div.onclick);\n    testing.expectEqual('function', typeof div.onfocus);\n    testing.expectEqual(null, div.onblur);\n\n    div.onblur = blurFn;\n    testing.expectEqual('function', typeof div.onclick);\n    testing.expectEqual('function', typeof div.onfocus);\n    testing.expectEqual('function', typeof div.onblur);\n}\n</script>\n\n<script id=\"keyboard_event_listeners\">\n{\n    // Test keyboard event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onkeydown);\n    testing.expectEqual(null, div.onkeyup);\n    testing.expectEqual(null, div.onkeypress);\n\n    div.onkeydown = () => {};\n    div.onkeyup = () => {};\n    div.onkeypress = () => {};\n\n    testing.expectEqual('function', typeof div.onkeydown);\n    testing.expectEqual('function', typeof div.onkeyup);\n    testing.expectEqual('function', typeof div.onkeypress);\n}\n</script>\n\n<script id=\"mouse_event_listeners\">\n{\n    // Test mouse event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onmousedown);\n    testing.expectEqual(null, div.onmouseup);\n    testing.expectEqual(null, div.onmousemove);\n    testing.expectEqual(null, div.onmouseover);\n    testing.expectEqual(null, div.onmouseout);\n    testing.expectEqual(null, div.ondblclick);\n\n    div.onmousedown = () => {};\n    div.onmouseup = () => {};\n    div.onmousemove = () => {};\n    div.onmouseover = () => {};\n    div.onmouseout = () => {};\n    div.ondblclick = () => {};\n\n    testing.expectEqual('function', typeof div.onmousedown);\n    testing.expectEqual('function', typeof div.onmouseup);\n    testing.expectEqual('function', typeof div.onmousemove);\n    testing.expectEqual('function', typeof div.onmouseover);\n    testing.expectEqual('function', typeof div.onmouseout);\n    testing.expectEqual('function', typeof div.ondblclick);\n}\n</script>\n\n<script id=\"pointer_event_listeners\">\n{\n    // Test pointer event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onpointerdown);\n    testing.expectEqual(null, div.onpointerup);\n    testing.expectEqual(null, div.onpointermove);\n    testing.expectEqual(null, div.onpointerover);\n    testing.expectEqual(null, div.onpointerout);\n    testing.expectEqual(null, div.onpointerenter);\n    testing.expectEqual(null, div.onpointerleave);\n    testing.expectEqual(null, div.onpointercancel);\n\n    div.onpointerdown = () => {};\n    div.onpointerup = () => {};\n    div.onpointermove = () => {};\n    div.onpointerover = () => {};\n    div.onpointerout = () => {};\n    div.onpointerenter = () => {};\n    div.onpointerleave = () => {};\n    div.onpointercancel = () => {};\n\n    testing.expectEqual('function', typeof div.onpointerdown);\n    testing.expectEqual('function', typeof div.onpointerup);\n    testing.expectEqual('function', typeof div.onpointermove);\n    testing.expectEqual('function', typeof div.onpointerover);\n    testing.expectEqual('function', typeof div.onpointerout);\n    testing.expectEqual('function', typeof div.onpointerenter);\n    testing.expectEqual('function', typeof div.onpointerleave);\n    testing.expectEqual('function', typeof div.onpointercancel);\n}\n</script>\n\n<script id=\"form_event_listeners\">\n{\n    // Test form event listener getters/setters\n    const form = document.createElement('form');\n\n    testing.expectEqual(null, form.onsubmit);\n    testing.expectEqual(null, form.onreset);\n    testing.expectEqual(null, form.onchange);\n    testing.expectEqual(null, form.oninput);\n    testing.expectEqual(null, form.oninvalid);\n\n    form.onsubmit = () => {};\n    form.onreset = () => {};\n    form.onchange = () => {};\n    form.oninput = () => {};\n    form.oninvalid = () => {};\n\n    testing.expectEqual('function', typeof form.onsubmit);\n    testing.expectEqual('function', typeof form.onreset);\n    testing.expectEqual('function', typeof form.onchange);\n    testing.expectEqual('function', typeof form.oninput);\n    testing.expectEqual('function', typeof form.oninvalid);\n}\n</script>\n\n<script id=\"drag_event_listeners\">\n{\n    // Test drag event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.ondrag);\n    testing.expectEqual(null, div.ondragstart);\n    testing.expectEqual(null, div.ondragend);\n    testing.expectEqual(null, div.ondragenter);\n    testing.expectEqual(null, div.ondragleave);\n    testing.expectEqual(null, div.ondragover);\n    testing.expectEqual(null, div.ondrop);\n\n    div.ondrag = () => {};\n    div.ondragstart = () => {};\n    div.ondragend = () => {};\n    div.ondragenter = () => {};\n    div.ondragleave = () => {};\n    div.ondragover = () => {};\n    div.ondrop = () => {};\n\n    testing.expectEqual('function', typeof div.ondrag);\n    testing.expectEqual('function', typeof div.ondragstart);\n    testing.expectEqual('function', typeof div.ondragend);\n    testing.expectEqual('function', typeof div.ondragenter);\n    testing.expectEqual('function', typeof div.ondragleave);\n    testing.expectEqual('function', typeof div.ondragover);\n    testing.expectEqual('function', typeof div.ondrop);\n}\n</script>\n\n<script id=\"clipboard_event_listeners\">\n{\n    // Test clipboard event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.oncopy);\n    testing.expectEqual(null, div.oncut);\n    testing.expectEqual(null, div.onpaste);\n\n    div.oncopy = () => {};\n    div.oncut = () => {};\n    div.onpaste = () => {};\n\n    testing.expectEqual('function', typeof div.oncopy);\n    testing.expectEqual('function', typeof div.oncut);\n    testing.expectEqual('function', typeof div.onpaste);\n}\n</script>\n\n<script id=\"scroll_event_listeners\">\n{\n    // Test scroll event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onscroll);\n    testing.expectEqual(null, div.onscrollend);\n    testing.expectEqual(null, div.onresize);\n\n    div.onscroll = () => {};\n    div.onscrollend = () => {};\n    div.onresize = () => {};\n\n    testing.expectEqual('function', typeof div.onscroll);\n    testing.expectEqual('function', typeof div.onscrollend);\n    testing.expectEqual('function', typeof div.onresize);\n}\n</script>\n\n<script id=\"animation_event_listeners\">\n{\n    // Test animation event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onanimationstart);\n    testing.expectEqual(null, div.onanimationend);\n    testing.expectEqual(null, div.onanimationiteration);\n    testing.expectEqual(null, div.onanimationcancel);\n\n    div.onanimationstart = () => {};\n    div.onanimationend = () => {};\n    div.onanimationiteration = () => {};\n    div.onanimationcancel = () => {};\n\n    testing.expectEqual('function', typeof div.onanimationstart);\n    testing.expectEqual('function', typeof div.onanimationend);\n    testing.expectEqual('function', typeof div.onanimationiteration);\n    testing.expectEqual('function', typeof div.onanimationcancel);\n}\n</script>\n\n<script id=\"transition_event_listeners\">\n{\n    // Test transition event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.ontransitionstart);\n    testing.expectEqual(null, div.ontransitionend);\n    testing.expectEqual(null, div.ontransitionrun);\n    testing.expectEqual(null, div.ontransitioncancel);\n\n    div.ontransitionstart = () => {};\n    div.ontransitionend = () => {};\n    div.ontransitionrun = () => {};\n    div.ontransitioncancel = () => {};\n\n    testing.expectEqual('function', typeof div.ontransitionstart);\n    testing.expectEqual('function', typeof div.ontransitionend);\n    testing.expectEqual('function', typeof div.ontransitionrun);\n    testing.expectEqual('function', typeof div.ontransitioncancel);\n}\n</script>\n\n<script id=\"misc_event_listeners\">\n{\n    // Test miscellaneous event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onwheel);\n    testing.expectEqual(null, div.ontoggle);\n    testing.expectEqual(null, div.oncontextmenu);\n    testing.expectEqual(null, div.onselect);\n    testing.expectEqual(null, div.onabort);\n    testing.expectEqual(null, div.oncancel);\n    testing.expectEqual(null, div.onclose);\n\n    div.onwheel = () => {};\n    div.ontoggle = () => {};\n    div.oncontextmenu = () => {};\n    div.onselect = () => {};\n    div.onabort = () => {};\n    div.oncancel = () => {};\n    div.onclose = () => {};\n\n    testing.expectEqual('function', typeof div.onwheel);\n    testing.expectEqual('function', typeof div.ontoggle);\n    testing.expectEqual('function', typeof div.oncontextmenu);\n    testing.expectEqual('function', typeof div.onselect);\n    testing.expectEqual('function', typeof div.onabort);\n    testing.expectEqual('function', typeof div.oncancel);\n    testing.expectEqual('function', typeof div.onclose);\n}\n</script>\n\n<script id=\"media_event_listeners\">\n{\n    // Test media event listener getters/setters\n    const div = document.createElement('div');\n\n    testing.expectEqual(null, div.onplay);\n    testing.expectEqual(null, div.onpause);\n    testing.expectEqual(null, div.onplaying);\n    testing.expectEqual(null, div.onended);\n    testing.expectEqual(null, div.onvolumechange);\n    testing.expectEqual(null, div.onwaiting);\n    testing.expectEqual(null, div.onseeking);\n    testing.expectEqual(null, div.onseeked);\n    testing.expectEqual(null, div.ontimeupdate);\n    testing.expectEqual(null, div.onloadstart);\n    testing.expectEqual(null, div.onprogress);\n    testing.expectEqual(null, div.onstalled);\n    testing.expectEqual(null, div.onsuspend);\n    testing.expectEqual(null, div.oncanplay);\n    testing.expectEqual(null, div.oncanplaythrough);\n    testing.expectEqual(null, div.ondurationchange);\n    testing.expectEqual(null, div.onemptied);\n    testing.expectEqual(null, div.onloadeddata);\n    testing.expectEqual(null, div.onloadedmetadata);\n    testing.expectEqual(null, div.onratechange);\n\n    div.onplay = () => {};\n    div.onpause = () => {};\n    div.onplaying = () => {};\n    div.onended = () => {};\n    div.onvolumechange = () => {};\n    div.onwaiting = () => {};\n    div.onseeking = () => {};\n    div.onseeked = () => {};\n    div.ontimeupdate = () => {};\n    div.onloadstart = () => {};\n    div.onprogress = () => {};\n    div.onstalled = () => {};\n    div.onsuspend = () => {};\n    div.oncanplay = () => {};\n    div.oncanplaythrough = () => {};\n    div.ondurationchange = () => {};\n    div.onemptied = () => {};\n    div.onloadeddata = () => {};\n    div.onloadedmetadata = () => {};\n    div.onratechange = () => {};\n\n    testing.expectEqual('function', typeof div.onplay);\n    testing.expectEqual('function', typeof div.onpause);\n    testing.expectEqual('function', typeof div.onplaying);\n    testing.expectEqual('function', typeof div.onended);\n    testing.expectEqual('function', typeof div.onvolumechange);\n    testing.expectEqual('function', typeof div.onwaiting);\n    testing.expectEqual('function', typeof div.onseeking);\n    testing.expectEqual('function', typeof div.onseeked);\n    testing.expectEqual('function', typeof div.ontimeupdate);\n    testing.expectEqual('function', typeof div.onloadstart);\n    testing.expectEqual('function', typeof div.onprogress);\n    testing.expectEqual('function', typeof div.onstalled);\n    testing.expectEqual('function', typeof div.onsuspend);\n    testing.expectEqual('function', typeof div.oncanplay);\n    testing.expectEqual('function', typeof div.oncanplaythrough);\n    testing.expectEqual('function', typeof div.ondurationchange);\n    testing.expectEqual('function', typeof div.onemptied);\n    testing.expectEqual('function', typeof div.onloadeddata);\n    testing.expectEqual('function', typeof div.onloadedmetadata);\n    testing.expectEqual('function', typeof div.onratechange);\n}\n</script>\n\n<img src=\"https://cdn.lightpanda.io/website/assets/images/docs/hn.png\" />\n\n<script id=\"document-element-load\">\n{\n  let asyncBlockDispatched = false;\n  const docElement = document.documentElement;\n\n  testing.async(async () => {\n    const result = await new Promise(resolve => {\n    // We should get this fired at capturing phase when a resource loaded.\n      docElement.addEventListener(\"load\", e => {\n        testing.expectEqual(e.eventPhase, Event.CAPTURING_PHASE);\n        return resolve(true);\n      }, true);\n    });\n\n    asyncBlockDispatched = true;\n    testing.expectEqual(true, result);\n  });\n\n  testing.eventually(() => testing.expectEqual(true, asyncBlockDispatched));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/fieldset.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<fieldset id=\"fs1\" disabled name=\"group1\">\n  <input type=\"text\">\n</fieldset>\n<fieldset id=\"fs2\">\n  <input type=\"text\">\n</fieldset>\n\n<script id=\"disabled\">\n  {\n    const fs1 = document.getElementById('fs1');\n    testing.expectEqual(true, fs1.disabled);\n\n    fs1.disabled = false;\n    testing.expectEqual(false, fs1.disabled);\n\n    const fs2 = document.getElementById('fs2');\n    testing.expectEqual(false, fs2.disabled);\n  }\n</script>\n\n<script id=\"name\">\n  {\n    const fs1 = document.getElementById('fs1');\n    testing.expectEqual('group1', fs1.name);\n\n    fs1.name = 'updated';\n    testing.expectEqual('updated', fs1.name);\n\n    const fs2 = document.getElementById('fs2');\n    testing.expectEqual('', fs2.name);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/form.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Test fixtures for form.name -->\n<form id=\"form_with_name\" name=\"myForm\"></form>\n<form id=\"form_without_name\"></form>\n\n<script id=\"name_initial\">\n{\n  testing.expectEqual('myForm', $('#form_with_name').name)\n  testing.expectEqual('', $('#form_without_name').name)\n}\n</script>\n\n<script id=\"name_set\">\n  {\n    const form = document.createElement('form')\n    testing.expectEqual('', form.name)\n\n    form.name = 'testForm'\n    testing.expectEqual('testForm', form.name)\n    testing.expectEqual('testForm', form.getAttribute('name'))\n  }\n</script>\n\n<script id=\"action\">\n  {\n    const form = document.createElement('form')\n    testing.expectEqual(testing.BASE_URL + 'element/html/form.html', form.action)\n\n    form.action = 'hello';\n    testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action)\n\n    form.action = '/hello';\n    testing.expectEqual(testing.ORIGIN + '/hello', form.action)\n\n    form.action = 'https://lightpanda.io/hello';\n    testing.expectEqual('https://lightpanda.io/hello', form.action)\n  }\n</script>\n\n<!-- Test fixtures for form.method -->\n<form id=\"form_get\" method=\"get\"></form>\n<form id=\"form_post\" method=\"post\"></form>\n<form id=\"form_dialog\" method=\"dialog\"></form>\n<form id=\"form_default\"></form>\n\n<script id=\"method_initial\">\n{\n  testing.expectEqual('get', $('#form_get').method)\n  testing.expectEqual('post', $('#form_post').method)\n  testing.expectEqual('dialog', $('#form_dialog').method)\n  testing.expectEqual('get', $('#form_default').method)\n}\n</script>\n\n<script id=\"method_set\">\n  {\n    const form = document.createElement('form')\n    testing.expectEqual('get', form.method)\n\n    form.method = 'post'\n    testing.expectEqual('post', form.method)\n    testing.expectEqual('post', form.getAttribute('method'))\n  }\n</script>\n\n<script id=\"method_normalization\">\n  {\n    const form = document.createElement('form')\n\n    // Test uppercase normalization\n    form.setAttribute('method', 'POST')\n    testing.expectEqual('post', form.method)\n\n    form.setAttribute('method', 'GeT')\n    testing.expectEqual('get', form.method)\n\n    form.setAttribute('method', 'DIALOG')\n    testing.expectEqual('dialog', form.method)\n\n    // Test invalid value defaults to \"get\"\n    form.setAttribute('method', 'invalid')\n    testing.expectEqual('get', form.method)\n  }\n</script>\n\n<!-- Test fixtures for form.elements -->\n<form id=\"form1\">\n  <input name=\"field1\" value=\"value1\">\n  <button name=\"btn1\" type=\"submit\">Submit</button>\n  <select name=\"select1\">\n    <option value=\"opt1\">Option 1</option>\n  </select>\n  <textarea name=\"text1\">Text</textarea>\n</form>\n\n<!-- Control outside form with form=ID -->\n<input id=\"external1\" name=\"external\" form=\"form1\">\n\n<!-- Control outside form without form attribute -->\n<input id=\"orphan\" name=\"orphan\">\n\n<script id=\"elements_collection\">\n{\n  const form = $('#form1')\n  const elements = form.elements\n\n  testing.expectEqual('HTMLFormControlsCollection', elements.constructor.name)\n  testing.expectEqual(5, elements.length)\n}\n</script>\n\n<script id=\"length\">\n{\n  testing.expectEqual(5, $('#form1').length)\n}\n</script>\n\n<script id=\"elements_indexed_access\">\n{\n  const form = $('#form1')\n  const elements = form.elements\n\n  testing.expectEqual('field1', elements[0].name)\n  testing.expectEqual('INPUT', elements[0].tagName)\n\n  testing.expectEqual('btn1', elements[1].name)\n  testing.expectEqual('BUTTON', elements[1].tagName)\n\n  testing.expectEqual('select1', elements[2].name)\n  testing.expectEqual('SELECT', elements[2].tagName)\n\n  testing.expectEqual('text1', elements[3].name)\n  testing.expectEqual('TEXTAREA', elements[3].tagName)\n\n  testing.expectEqual('external', elements[4].name)\n  testing.expectEqual('INPUT', elements[4].tagName)\n}\n</script>\n\n<script id=\"elements_named_access\">\n{\n  const form = $('#form1')\n  const elements = form.elements\n\n  testing.expectEqual('field1', elements.field1.name)\n  testing.expectEqual('btn1', elements.btn1.name)\n  testing.expectEqual('select1', elements.select1.name)\n  testing.expectEqual('text1', elements.text1.name)\n  testing.expectEqual('external', elements.external.name)\n\n  testing.expectEqual('field1', elements.namedItem('field1').name)\n}\n</script>\n\n<script id=\"elements_excludes_orphans\">\n{\n  const form = $('#form1')\n  const elements = form.elements\n\n  let foundOrphan = false\n  for (let i = 0; i < elements.length; i++) {\n    if (elements[i].id === 'orphan') {\n      foundOrphan = true\n    }\n  }\n  testing.expectEqual(false, foundOrphan)\n}\n</script>\n\n<form id=\"form2\"></form>\n\n<script id=\"elements_live_collection\">\n{\n  const form = $('#form2')\n  testing.expectEqual(0, form.elements.length)\n\n  const input = document.createElement('input')\n  input.name = 'dynamic'\n  form.appendChild(input)\n\n  testing.expectEqual(1, form.elements.length)\n  testing.expectEqual('dynamic', form.elements[0].name)\n\n  input.remove()\n  testing.expectEqual(0, form.elements.length)\n}\n</script>\n\n<!-- Test with controls that have form attribute pointing to different form -->\n<form id=\"form3\"></form>\n<input id=\"belongs_to_3\" name=\"field3\" form=\"form3\">\n\n<script id=\"form_attribute_different_form\">\n{\n  const form1 = $('#form1')\n  const form3 = $('#form3')\n  const belongs3 = $('#belongs_to_3')\n\n  let inForm1 = false\n  for (let i = 0; i < form1.elements.length; i++) {\n    if (form1.elements[i].id === 'belongs_to_3') {\n      inForm1 = true\n    }\n  }\n  testing.expectEqual(false, inForm1)\n\n  let inForm3 = false\n  for (let i = 0; i < form3.elements.length; i++) {\n    if (form3.elements[i].id === 'belongs_to_3') {\n      inForm3 = true\n    }\n  }\n  testing.expectEqual(true, inForm3)\n}\n</script>\n\n<!-- CRITICAL TEST: Nested control with form attribute pointing elsewhere -->\n<form id=\"outer_form\">\n  <input id=\"nested_but_reassigned\" name=\"reassigned\" form=\"form3\">\n  <input id=\"nested_normal\" name=\"normal\">\n</form>\n\n<script id=\"nested_control_with_form_attribute\">\n{\n  // This test prevents a dangerous optimization bug:\n  // Even though nested_but_reassigned is physically nested in outer_form,\n  // it has form=\"form3\", so it belongs to form3, NOT outer_form.\n  // We MUST check the form attribute even for nested controls!\n\n  const outerForm = $('#outer_form')\n  const form3 = $('#form3')\n\n  // outer_form should have only 1 element (nested_normal)\n  testing.expectEqual(1, outerForm.elements.length)\n  testing.expectEqual('nested_normal', outerForm.elements[0].id)\n\n  // form3 should have 2 elements (belongs_to_3 and nested_but_reassigned)\n  testing.expectEqual(2, form3.elements.length)\n\n  let foundReassigned = false\n  for (let i = 0; i < form3.elements.length; i++) {\n    if (form3.elements[i].id === 'nested_but_reassigned') {\n      foundReassigned = true\n    }\n  }\n  testing.expectEqual(true, foundReassigned)\n}\n</script>\n\n<!-- Test radio buttons with same name -->\n<form id=\"radio_form\">\n  <input type=\"radio\" name=\"choice\" value=\"a\">\n  <input type=\"radio\" name=\"choice\" value=\"b\">\n  <input type=\"radio\" name=\"choice\" value=\"c\">\n</form>\n\n<script id=\"radio_buttons_in_elements\">\n{\n  const form = $('#radio_form')\n  testing.expectEqual(3, form.elements.length)\n\n  testing.expectEqual('a', form.elements[0].value)\n  testing.expectEqual('b', form.elements[1].value)\n  testing.expectEqual('c', form.elements[2].value)\n\n  // Ensure all radios are unchecked at start (cleanup from any previous tests)\n  form.elements[0].checked = false\n  form.elements[1].checked = false\n  form.elements[2].checked = false\n\n  // namedItem with duplicate names returns RadioNodeList\n  const result = form.elements.namedItem('choice')\n  testing.expectEqual('RadioNodeList', result.constructor.name)\n  testing.expectEqual(3, result.length)\n  testing.expectEqual('', result.value)\n\n  form.elements[1].checked = true\n  testing.expectEqual('b', result.value)\n\n  result.value = 'c'\n  testing.expectEqual(true, form.elements[2].checked)\n}\n</script>\n\n<!-- Test disabled controls -->\n<script id=\"disabled_controls_included\">\n{\n  const form = document.createElement('form')\n  const input1 = document.createElement('input')\n  input1.name = 'enabled'\n  const input2 = document.createElement('input')\n  input2.name = 'disabled'\n  input2.disabled = true\n\n  form.appendChild(input1)\n  form.appendChild(input2)\n\n  testing.expectEqual(2, form.elements.length)\n  testing.expectEqual('enabled', form.elements[0].name)\n  testing.expectEqual('disabled', form.elements[1].name)\n}\n</script>\n\n<!-- Test empty form -->\n<form id=\"empty_form\"></form>\n\n<script id=\"empty_form_elements\">\n{\n  const form = $('#empty_form')\n  testing.expectEqual(0, form.elements.length)\n  testing.expectEqual(0, form.length)\n}\n</script>\n\n<!-- Test form without id can't have external controls -->\n<form id=\"form_no_id_attr\"></form>\n<input id=\"orphan2\" name=\"orphan2\" form=\"nonexistent\">\n\n<script id=\"form_without_id_no_external\">\n{\n  const form = $('#form_no_id_attr')\n  testing.expectEqual(0, form.elements.length)\n}\n</script>\n\n<form id=\"duplicate_names\">\n  <input type=\"text\" name=\"choice\" value=\"a\">\n  <input type=\"text\" name=\"choice\" value=\"b\">\n  <input type=\"text\" name=\"choice\" value=\"c\">\n</form>\n\n<script id=\"duplicate_names_handling\">\n{\n  const form = $('#duplicate_names')\n  testing.expectEqual(3, form.elements.length)\n\n  testing.expectEqual('a', form.elements[0].value)\n  testing.expectEqual('b', form.elements[1].value)\n  testing.expectEqual('c', form.elements[2].value)\n\n  testing.expectEqual('', form.elements['choice'].value)\n}\n</script>\n\n<!-- Test: requestSubmit() fires the submit event (unlike submit()) -->\n<form id=\"test_form2\" action=\"/should-not-navigate2\" method=\"get\">\n  <input name=\"q\" value=\"test2\">\n</form>\n\n<script id=\"requestSubmit_fires_submit_event\">\n{\n  const form = $('#test_form2');\n  let submitFired = false;\n\n  form.addEventListener('submit', (e) => {\n    e.preventDefault();\n    submitFired = true;\n  });\n\n  form.requestSubmit();\n\n  testing.expectEqual(true, submitFired);\n}\n</script>\n\n<!-- Test: requestSubmit() with preventDefault stops navigation -->\n<form id=\"test_form3\" action=\"/should-not-navigate3\" method=\"get\">\n  <input name=\"q\" value=\"test3\">\n</form>\n\n<script id=\"requestSubmit_respects_preventDefault\">\n{\n  const form = $('#test_form3');\n\n  form.addEventListener('submit', (e) => {\n    e.preventDefault();\n  });\n\n  form.requestSubmit();\n\n  // Form submission was prevented, so no navigation should be scheduled\n  testing.expectEqual(true, true);\n}\n</script>\n\n<!-- Test: requestSubmit() with non-submit-button submitter throws TypeError -->\n<form id=\"test_form_rs1\" action=\"/should-not-navigate4\" method=\"get\">\n  <input id=\"rs1_text\" type=\"text\" name=\"q\" value=\"test\">\n  <input id=\"rs1_submit\" type=\"submit\" value=\"Go\">\n  <input id=\"rs1_image\" type=\"image\" src=\"x.png\">\n  <button id=\"rs1_btn_submit\" type=\"submit\">Submit</button>\n  <button id=\"rs1_btn_reset\" type=\"reset\">Reset</button>\n  <button id=\"rs1_btn_button\" type=\"button\">Button</button>\n</form>\n\n<script id=\"requestSubmit_rejects_non_submit_button\">\n{\n  const form = $('#test_form_rs1');\n  form.addEventListener('submit', (e) => e.preventDefault());\n\n  // A text input is not a submit button — should throw TypeError\n  testing.expectError('TypeError', () => {\n    form.requestSubmit($('#rs1_text'));\n  });\n\n  // A reset button is not a submit button — should throw TypeError\n  testing.expectError('TypeError', () => {\n    form.requestSubmit($('#rs1_btn_reset'));\n  });\n\n  // A <button type=\"button\"> is not a submit button — should throw TypeError\n  testing.expectError('TypeError', () => {\n    form.requestSubmit($('#rs1_btn_button'));\n  });\n\n  // A <div> is not a submit button — should throw TypeError\n  const div = document.createElement('div');\n  form.appendChild(div);\n  testing.expectError('TypeError', () => {\n    form.requestSubmit(div);\n  });\n}\n</script>\n\n<!-- Test: requestSubmit() accepts valid submit buttons -->\n<script id=\"requestSubmit_accepts_submit_buttons\">\n{\n  const form = $('#test_form_rs1');\n  let submitCount = 0;\n  form.addEventListener('submit', (e) => { e.preventDefault(); submitCount++; });\n\n  // <input type=\"submit\"> is a valid submitter\n  form.requestSubmit($('#rs1_submit'));\n  testing.expectEqual(1, submitCount);\n\n  // <input type=\"image\"> is a valid submitter\n  form.requestSubmit($('#rs1_image'));\n  testing.expectEqual(2, submitCount);\n\n  // <button type=\"submit\"> is a valid submitter\n  form.requestSubmit($('#rs1_btn_submit'));\n  testing.expectEqual(3, submitCount);\n}\n</script>\n\n<!-- Test: requestSubmit() with submitter not owned by form throws NotFoundError -->\n<form id=\"test_form_rs2\" action=\"/should-not-navigate5\" method=\"get\">\n  <input type=\"text\" name=\"q\" value=\"test\">\n</form>\n<form id=\"test_form_rs3\">\n  <input id=\"rs3_submit\" type=\"submit\" value=\"Other Submit\">\n</form>\n\n<script id=\"requestSubmit_rejects_wrong_form_submitter\">\n{\n  const form = $('#test_form_rs2');\n\n  // Submit button belongs to a different form — should throw NotFoundError\n  testing.expectError('NotFoundError', () => {\n    form.requestSubmit($('#rs3_submit'));\n  });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/htmlelement-props.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<div id=\"d1\" hidden>Hidden div</div>\n<div id=\"d2\">Visible div</div>\n<input id=\"i1\" tabindex=\"5\">\n<div id=\"d3\">No tabindex</div>\n\n<script id=\"hidden\">\n  {\n    const d1 = document.getElementById('d1');\n    testing.expectEqual(true, d1.hidden);\n\n    d1.hidden = false;\n    testing.expectEqual(false, d1.hidden);\n\n    const d2 = document.getElementById('d2');\n    testing.expectEqual(false, d2.hidden);\n\n    d2.hidden = true;\n    testing.expectEqual(true, d2.hidden);\n  }\n</script>\n\n<script id=\"tabIndex\">\n  {\n    const i1 = document.getElementById('i1');\n    testing.expectEqual(5, i1.tabIndex);\n\n    i1.tabIndex = 10;\n    testing.expectEqual(10, i1.tabIndex);\n\n    // Non-interactive elements default to -1\n    const d3 = document.getElementById('d3');\n    testing.expectEqual(-1, d3.tabIndex);\n\n    d3.tabIndex = 0;\n    testing.expectEqual(0, d3.tabIndex);\n\n    // Interactive elements default to 0 per spec\n    const input = document.createElement('input');\n    testing.expectEqual(0, input.tabIndex);\n\n    const button = document.createElement('button');\n    testing.expectEqual(0, button.tabIndex);\n\n    const a = document.createElement('a');\n    testing.expectEqual(0, a.tabIndex);\n\n    const select = document.createElement('select');\n    testing.expectEqual(0, select.tabIndex);\n\n    const textarea = document.createElement('textarea');\n    testing.expectEqual(0, textarea.tabIndex);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/image.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=\"Image\">\n{\n    let i1 = new Image()\n    testing.expectEqual(0, i1.width);\n    testing.expectEqual(null, i1.getAttribute('width'));\n    testing.expectEqual(0, i1.height);\n    testing.expectEqual(null, i1.getAttribute('height'));\n\n    let i2 = new Image(10)\n    testing.expectEqual(10, i2.width);\n    testing.expectEqual('10', i2.getAttribute('width'));\n    testing.expectEqual(0, i2.height);\n    testing.expectEqual(null, i2.getAttribute('height'));\n\n    let i3 = new Image(10, 20)\n    testing.expectEqual(10, i3.width);\n    testing.expectEqual('10', i3.getAttribute('width'));\n    testing.expectEqual(20, i3.height);\n    testing.expectEqual('20', i3.getAttribute('height'));\n}\n</script>\n\n<script id=\"src_alt\">\n{\n    const img = document.createElement('img');\n\n    testing.expectEqual('', img.src);\n    testing.expectEqual('', img.alt);\n\n    img.src = 'test.png';\n    // src property returns resolved absolute URL\n    testing.expectEqual(testing.BASE_URL + 'element/html/test.png', img.src);\n    // getAttribute returns the raw attribute value\n    testing.expectEqual('test.png', img.getAttribute('src'));\n\n    img.src = '/absolute/path.png';\n    testing.expectEqual(testing.ORIGIN + '/absolute/path.png', img.src);\n    testing.expectEqual('/absolute/path.png', img.getAttribute('src'));\n\n    img.src = 'https://example.com/image.png';\n    testing.expectEqual('https://example.com/image.png', img.src);\n    testing.expectEqual('https://example.com/image.png', img.getAttribute('src'));\n\n    img.alt = 'Test image';\n    testing.expectEqual('Test image', img.alt);\n    testing.expectEqual('Test image', img.getAttribute('alt'));\n}\n</script>\n\n<script id=\"width_height_setters\">\n{\n    const img = document.createElement('img');\n\n    img.width = 100;\n    testing.expectEqual(100, img.width);\n    testing.expectEqual('100', img.getAttribute('width'));\n\n    img.height = 200;\n    testing.expectEqual(200, img.height);\n    testing.expectEqual('200', img.getAttribute('height'));\n\n    img.setAttribute('width', '50');\n    testing.expectEqual(50, img.width);\n\n    img.setAttribute('height', 'invalid');\n    testing.expectEqual(0, img.height);\n}\n</script>\n\n<script id=\"crossOrigin\">\n{\n    const img = document.createElement('img');\n\n    testing.expectEqual(null, img.crossOrigin);\n\n    img.crossOrigin = 'anonymous';\n    testing.expectEqual('anonymous', img.crossOrigin);\n    testing.expectEqual('anonymous', img.getAttribute('crossorigin'));\n\n    img.crossOrigin = null;\n    testing.expectEqual(null, img.crossOrigin);\n    testing.expectEqual(null, img.getAttribute('crossorigin'));\n}\n</script>\n\n<script id=\"loading\">\n{\n    const img = document.createElement('img');\n\n    testing.expectEqual('eager', img.loading);\n\n    img.loading = 'lazy';\n    testing.expectEqual('lazy', img.loading);\n    testing.expectEqual('lazy', img.getAttribute('loading'));\n}\n</script>\n\n<script id=\"complete\">\n{\n    // Image with no src is complete per spec\n    const img = document.createElement('img');\n    testing.expectEqual(true, img.complete);\n\n    // Image with src is also complete (headless browser, no actual fetch)\n    img.src = 'test.png';\n    testing.expectEqual(true, img.complete);\n\n    // Image constructor also complete\n    const img2 = new Image();\n    testing.expectEqual(true, img2.complete);\n}\n</script>\n\n<body></body>\n\n<script id=\"img-load-event\">\n{\n  // An img fires a load event when src is set.\n  const img = document.createElement(\"img\");\n  let result = false;\n  testing.async(async () => {\n    await new Promise(resolve => {\n      img.addEventListener(\"load\", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {\n        testing.expectEqual(false, bubbles);\n        testing.expectEqual(false, cancelBubble);\n        testing.expectEqual(false, cancelable);\n        testing.expectEqual(false, composed);\n        testing.expectEqual(true, isTrusted);\n        testing.expectEqual(img, target);\n        result = true;\n        return resolve();\n      });\n      img.src = \"https://cdn.lightpanda.io/website/assets/images/docs/hn.png\";\n    });\n  });\n\n  testing.eventually(() => testing.expectEqual(true, result));\n}\n</script>\n\n<script id=\"img-no-load-without-src\">\n{\n  // An img without src should not fire a load event.\n  let fired = false;\n  const img = document.createElement(\"img\");\n  img.addEventListener(\"load\", () => { fired = true; });\n  document.body.appendChild(img);\n  testing.eventually(() => testing.expectEqual(false, fired));\n}\n</script>\n\n<script id=\"lazy-src-set\">\n{\n  // Append to DOM first, then set src — load should still fire.\n  const img = document.createElement(\"img\");\n  let result = false;\n  img.onload = () => result = true;\n  document.body.appendChild(img);\n  img.src = \"https://cdn.lightpanda.io/website/assets/images/docs/hn.png\";\n\n  testing.eventually(() => testing.expectEqual(true, result));\n}\n</script>\n\n<script id=url_encode>\n  {\n    let img = document.createElement('img');\n    img.src = 'over 9000!?hello=world !';\n    testing.expectEqual('over 9000!?hello=world !', img.getAttribute('src'));\n    testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!?hello=world%20!', img.src);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/input-attrs.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<input id=\"i1\" placeholder=\"Enter name\" min=\"0\" max=\"100\" step=\"5\" autocomplete=\"email\">\n<input id=\"i2\" type=\"file\" multiple>\n<input id=\"i3\">\n\n<script id=\"placeholder\">\n  {\n    const i1 = document.getElementById('i1');\n    testing.expectEqual('Enter name', i1.placeholder);\n\n    i1.placeholder = 'Updated';\n    testing.expectEqual('Updated', i1.placeholder);\n\n    const i3 = document.getElementById('i3');\n    testing.expectEqual('', i3.placeholder);\n  }\n</script>\n\n<script id=\"min\">\n  {\n    const i1 = document.getElementById('i1');\n    testing.expectEqual('0', i1.min);\n\n    i1.min = '10';\n    testing.expectEqual('10', i1.min);\n\n    const i3 = document.getElementById('i3');\n    testing.expectEqual('', i3.min);\n  }\n</script>\n\n<script id=\"max\">\n  {\n    const i1 = document.getElementById('i1');\n    testing.expectEqual('100', i1.max);\n\n    i1.max = '200';\n    testing.expectEqual('200', i1.max);\n\n    const i3 = document.getElementById('i3');\n    testing.expectEqual('', i3.max);\n  }\n</script>\n\n<script id=\"step\">\n  {\n    const i1 = document.getElementById('i1');\n    testing.expectEqual('5', i1.step);\n\n    i1.step = '0.5';\n    testing.expectEqual('0.5', i1.step);\n\n    const i3 = document.getElementById('i3');\n    testing.expectEqual('', i3.step);\n  }\n</script>\n\n<script id=\"multiple\">\n  {\n    const i2 = document.getElementById('i2');\n    testing.expectEqual(true, i2.multiple);\n\n    i2.multiple = false;\n    testing.expectEqual(false, i2.multiple);\n\n    const i3 = document.getElementById('i3');\n    testing.expectEqual(false, i3.multiple);\n\n    i3.multiple = true;\n    testing.expectEqual(true, i3.multiple);\n  }\n</script>\n\n<script id=\"autocomplete\">\n  {\n    const i1 = document.getElementById('i1');\n    testing.expectEqual('email', i1.autocomplete);\n\n    i1.autocomplete = 'off';\n    testing.expectEqual('off', i1.autocomplete);\n\n    const i3 = document.getElementById('i3');\n    testing.expectEqual('', i3.autocomplete);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/input.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<input id=\"text1\" type=\"text\" value=\"initial\">\n<input id=\"text2\" type=\"text\">\n\n<input id=\"check1\" type=\"checkbox\" checked>\n<input id=\"check2\" type=\"checkbox\">\n\n<input id=\"radio1\" type=\"radio\" name=\"group1\" checked>\n<input id=\"radio2\" type=\"radio\" name=\"group1\">\n<input id=\"radio3\" type=\"radio\" name=\"group1\">\n\n<input id=\"radio4\" type=\"radio\" name=\"group2\">\n\n<input id=\"disabled1\" type=\"text\" disabled>\n<input id=\"disabled2\" type=\"checkbox\">\n\n<script id=\"type\">\n  testing.expectEqual('text', $('#text1').type)\n  testing.expectEqual('text', $('#text2').type)\n  testing.expectEqual('checkbox', $('#check1').type)\n  testing.expectEqual('radio', $('#radio1').type)\n</script>\n\n<script id=attributes>\n  {\n    const input = $('#text1');\n\n    testing.expectEqual('', input.accept);\n    input.accept = 'anything';\n    testing.expectEqual('anything', input.accept);\n\n    testing.expectEqual('', input.alt);\n    input.alt = 'x1';\n    testing.expectEqual('x1', input.alt);\n\n    testing.expectEqual(false, input.readOnly);\n    input.readOnly = true;\n    testing.expectEqual(true, input.readOnly);\n    input.readOnly = false;\n    testing.expectEqual(false, input.readOnly);\n\n    testing.expectEqual(-1, input.maxLength);\n    input.maxLength = 5;\n    testing.expectEqual(5, input.maxLength);\n    input.maxLength = 'banana';\n    testing.expectEqual(0, input.maxLength);\n    testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => { input.maxLength = -45;});\n\n    testing.expectEqual(20, input.size);\n    input.size = 5;\n    testing.expectEqual(5, input.size);\n    input.size = -449;\n    testing.expectEqual(20, input.size);\n    testing.expectError('Error: ZeroNotAllowed', () => { input.size = 0; });\n\n    testing.expectEqual('', input.src);\n    input.src = 'foo'\n    testing.expectEqual(testing.BASE_URL + 'element/html/foo', input.src);\n    input.src = '-3'\n    testing.expectEqual(testing.BASE_URL + 'element/html/-3', input.src);\n    input.src = ''\n  }\n</script>\n\n<script id=\"type_case_insensitive\">\n  {\n    const input = document.createElement('input')\n    input.setAttribute('type', 'TEXT')\n    testing.expectEqual('text', input.type)\n\n    input.setAttribute('type', 'ChEcKbOx')\n    testing.expectEqual('checkbox', input.type)\n\n    input.setAttribute('type', 'EMAIL')\n    testing.expectEqual('email', input.type)\n  }\n</script>\n\n<script id=\"type_attribute_name_case_insensitive\">\n  {\n    const input = document.createElement('input')\n    input.setAttribute('TYPE', 'checkbox')\n    testing.expectEqual('checkbox', input.type)\n    testing.expectEqual('checkbox', input.getAttribute('type'))\n\n    input.setAttribute('TyPe', 'radio')\n    testing.expectEqual('radio', input.type)\n    testing.expectEqual('radio', input.getAttribute('type'))\n  }\n</script>\n\n<script id=\"type_invalid_defaults_to_text\">\n  {\n    const input = document.createElement('input')\n    input.setAttribute('type', 'invalid')\n    testing.expectEqual('text', input.type)\n\n    input.setAttribute('type', 'notavalidtype')\n    testing.expectEqual('text', input.type)\n\n    input.setAttribute('type', 'x'.repeat(50))\n    testing.expectEqual('text', input.type)\n  }\n</script>\n\n<script id=\"type_set\">\n  const input = document.createElement('input')\n  testing.expectEqual('text', input.type)\n\n  input.type = 'password'\n  testing.expectEqual('password', input.type)\n\n  input.type = 'EMAIL'\n  testing.expectEqual('email', input.type)\n\n  input.type = 'invalid'\n  testing.expectEqual('text', input.type)\n\n  input.type = 'ChEcKbOx'\n  testing.expectEqual('checkbox', input.type)\n</script>\n\n<script id=\"value_initial\">\n  testing.expectEqual('initial', $('#text1').value)\n  testing.expectEqual('', $('#text2').value)\n</script>\n\n<script id=\"value_set\">\n  $('#text1').value = 'changed'\n  testing.expectEqual('changed', $('#text1').value)\n\n  $('#text2').value = 'new value'\n  testing.expectEqual('new value', $('#text2').value)\n</script>\n\n<script id=\"checked_initial\">\n  testing.expectEqual(true, $('#check1').checked)\n  testing.expectEqual(false, $('#check2').checked)\n  testing.expectEqual(true, $('#radio1').checked)\n  testing.expectEqual(false, $('#radio2').checked)\n</script>\n\n<script id=\"checked_set\">\n  $('#check1').checked = false\n  testing.expectEqual(false, $('#check1').checked)\n\n  $('#check2').checked = true\n  testing.expectEqual(true, $('#check2').checked)\n\n  $('#radio2').checked = true\n  testing.expectEqual(true, $('#radio2').checked)\n</script>\n\n<script id=\"defaultValue\">\n  testing.expectEqual('initial', $('#text1').defaultValue)\n  testing.expectEqual('', $('#text2').defaultValue)\n\n  $('#text1').value = 'changed'\n  testing.expectEqual('initial', $('#text1').defaultValue)\n</script>\n\n<script id=\"defaultValue_set\">\n{\n  const input = document.createElement('input')\n  testing.expectEqual('', input.defaultValue)\n  testing.expectEqual(null, input.getAttribute('value'))\n\n  input.defaultValue = 'new default'\n  testing.expectEqual('new default', input.defaultValue)\n  testing.expectEqual('new default', input.getAttribute('value'))\n  testing.expectEqual('new default', input.value)\n\n  input.value = 'changed by user'\n  testing.expectEqual('changed by user', input.value)\n  testing.expectEqual('new default', input.defaultValue)\n\n  input.defaultValue = 'another default'\n  testing.expectEqual('another default', input.defaultValue)\n  testing.expectEqual('another default', input.getAttribute('value'))\n  testing.expectEqual('changed by user', input.value)\n}\n</script>\n\n<script id=\"selectionchange_event\">\n  {\n    const input = document.createElement('input');\n    input.value = 'Hello World';\n    document.body.appendChild(input);\n\n    let eventCount = 0;\n    let lastEvent = null;\n\n    input.addEventListener('selectionchange', (e) => {\n      eventCount++;\n      lastEvent = e;\n    });\n\n    testing.expectEqual(0, eventCount);\n\n    input.setSelectionRange(0, 5);\n    input.select();\n    input.selectionStart = 3;\n    input.selectionEnd = 8;\n\n    let bubbledToBody = false;\n    document.body.addEventListener('selectionchange', () => {\n      bubbledToBody = true;\n    });\n    input.setSelectionRange(1, 4);\n\n    testing.eventually(() => {\n      testing.expectEqual(5, eventCount);\n      testing.expectEqual('selectionchange', lastEvent.type);\n      testing.expectEqual(input, lastEvent.target);\n      testing.expectEqual(true, lastEvent.bubbles);\n      testing.expectEqual(false, lastEvent.cancelable);\n      testing.expectEqual(true, bubbledToBody);\n    });\n  }\n</script>\n\n<script id=\"select_event\">\n  {\n    const input = document.createElement('input');\n    input.value = 'Hello World';\n    document.body.appendChild(input);\n\n    let eventCount = 0;\n    let lastEvent = null;\n\n    input.addEventListener('select', (e) => {\n      eventCount++;\n      lastEvent = e;\n    });\n\n    let onselectFired = false;\n    input.onselect = () => { onselectFired = true; };\n\n    let bubbledToBody = false;\n    document.body.addEventListener('select', () => {\n      bubbledToBody = true;\n    });\n\n    testing.expectEqual(0, eventCount);\n\n    input.select();\n\n    testing.eventually(() => {\n      testing.expectEqual(1, eventCount);\n      testing.expectEqual('select', lastEvent.type);\n      testing.expectEqual(input, lastEvent.target);\n      testing.expectEqual(true, lastEvent.bubbles);\n      testing.expectEqual(false, lastEvent.cancelable);\n      testing.expectEqual(true, bubbledToBody);\n      testing.expectEqual(true, onselectFired);\n    });\n  }\n</script>\n\n<script id=\"defaultChecked\">\n  testing.expectEqual(true, $('#check1').defaultChecked)\n  testing.expectEqual(false, $('#check2').defaultChecked)\n  testing.expectEqual(true, $('#radio1').defaultChecked)\n  testing.expectEqual(false, $('#radio2').defaultChecked)\n\n  $('#check1').checked = false\n  testing.expectEqual(true, $('#check1').defaultChecked)\n</script>\n\n<script id=\"defaultChecked_set\">\n{\n  const input = document.createElement('input')\n  input.type = 'checkbox'\n  testing.expectEqual(false, input.defaultChecked)\n  testing.expectEqual(null, input.getAttribute('checked'))\n\n  input.defaultChecked = true\n  testing.expectEqual(true, input.defaultChecked)\n  testing.expectEqual('', input.getAttribute('checked'))\n  testing.expectEqual(true, input.checked)\n\n  input.checked = false\n  testing.expectEqual(false, input.checked)\n  testing.expectEqual(true, input.defaultChecked)\n\n  input.defaultChecked = false\n  testing.expectEqual(false, input.defaultChecked)\n  testing.expectEqual(null, input.getAttribute('checked'))\n  testing.expectEqual(false, input.checked)\n}\n</script>\n\n<script id=\"disabled_initial\">\n  testing.expectEqual(true, $('#disabled1').disabled)\n  testing.expectEqual(false, $('#disabled2').disabled)\n</script>\n\n<script id=\"disabled_set\">\n  $('#disabled1').disabled = false\n  testing.expectEqual(false, $('#disabled1').disabled)\n\n  $('#disabled2').disabled = true\n  testing.expectEqual(true, $('#disabled2').disabled)\n</script>\n\n<form id=\"form1\">\n  <input id=\"input_in_form\" type=\"text\">\n</form>\n\n<form id=\"form2\"></form>\n<input id=\"input_with_form_attr\" type=\"text\" form=\"form2\">\n\n<input id=\"input_no_form\" type=\"text\">\n\n<form id=\"form3\">\n  <input id=\"input_invalid_form_attr\" type=\"text\" form=\"nonexistent\">\n</form>\n\n<script id=\"form_ancestor\">\n  const inputInForm = $('#input_in_form')\n  testing.expectEqual('FORM', inputInForm.form.tagName)\n  testing.expectEqual('form1', inputInForm.form.id)\n</script>\n\n<script id=\"form_attribute\">\n  const inputWithFormAttr = $('#input_with_form_attr')\n  testing.expectEqual('FORM', inputWithFormAttr.form.tagName)\n  testing.expectEqual('form2', inputWithFormAttr.form.id)\n</script>\n\n<script id=\"form_null\">\n  const inputNoForm = $('#input_no_form')\n  testing.expectEqual(null, inputNoForm.form)\n</script>\n\n<script id=\"form_invalid_attribute\">\n  const inputInvalidFormAttr = $('#input_invalid_form_attr')\n  testing.expectEqual(null, inputInvalidFormAttr.form)\n</script>\n\n<script id=\"input_clone\">\n  {\n    const input = document.createElement('input');\n    input.type = 'image';\n    const clone = input.cloneNode();\n    testing.expectEqual('image', clone.type);\n  }\n</script>\n\n<script id=\"type_reflects_to_attribute\">\n  {\n    const input = document.createElement('input')\n    input.type = 'checkbox'\n    testing.expectEqual('checkbox', input.getAttribute('type'))\n    testing.expectTrue(input.outerHTML.includes('type=\"checkbox\"'))\n\n    input.type = 'radio'\n    testing.expectEqual('radio', input.getAttribute('type'))\n    testing.expectTrue(input.outerHTML.includes('type=\"radio\"'))\n  }\n</script>\n\n<script id=\"disabled_reflects_to_attribute\">\n  {\n    const input = document.createElement('input')\n    testing.expectEqual(null, input.getAttribute('disabled'))\n    testing.expectFalse(input.outerHTML.includes('disabled'))\n\n    input.disabled = true\n    testing.expectEqual('', input.getAttribute('disabled'))\n    testing.expectTrue(input.outerHTML.includes('disabled'))\n\n    input.disabled = false\n    testing.expectEqual(null, input.getAttribute('disabled'))\n    testing.expectFalse(input.outerHTML.includes('disabled'))\n  }\n</script>\n\n<script id=\"value_does_not_reflect_to_attribute\">\n  {\n    const input = document.createElement('input')\n    input.setAttribute('value', 'initial')\n    testing.expectEqual('initial', input.getAttribute('value'))\n\n    input.value = 'changed'\n    testing.expectEqual('changed', input.value)\n    testing.expectEqual('initial', input.getAttribute('value'))\n    testing.expectTrue(input.outerHTML.includes('value=\"initial\"'))\n    testing.expectFalse(input.outerHTML.includes('value=\"changed\"'))\n  }\n</script>\n\n<script id=\"checked_does_not_reflect_to_attribute\">\n  {\n    const input = document.createElement('input')\n    input.type = 'checkbox'\n    input.setAttribute('checked', '')\n    testing.expectEqual('', input.getAttribute('checked'))\n    testing.expectTrue(input.outerHTML.includes('checked'))\n\n    input.checked = false\n    testing.expectEqual(false, input.checked)\n    testing.expectEqual('', input.getAttribute('checked'))\n    testing.expectTrue(input.outerHTML.includes('checked'))\n  }\n</script>\n\n<input id=\"named1\" type=\"text\" name=\"username\">\n<input id=\"named2\" type=\"text\">\n\n<input id=\"required1\" type=\"text\" required>\n<input id=\"required2\" type=\"text\">\n\n<script id=\"name_initial\">\n  testing.expectEqual('username', $('#named1').name)\n  testing.expectEqual('', $('#named2').name)\n</script>\n\n<script id=\"name_set\">\n  {\n    const input = document.createElement('input')\n    testing.expectEqual('', input.name)\n\n    input.name = 'email'\n    testing.expectEqual('email', input.name)\n    testing.expectEqual('email', input.getAttribute('name'))\n\n    input.name = 'password'\n    testing.expectEqual('password', input.name)\n    testing.expectEqual('password', input.getAttribute('name'))\n  }\n</script>\n\n<script id=\"name_reflects_to_attribute\">\n  {\n    const input = document.createElement('input')\n    testing.expectEqual(null, input.getAttribute('name'))\n\n    input.name = 'fieldname'\n    testing.expectEqual('fieldname', input.getAttribute('name'))\n    testing.expectTrue(input.outerHTML.includes('name=\"fieldname\"'))\n  }\n</script>\n\n<script id=\"required_initial\">\n  testing.expectEqual(true, $('#required1').required)\n  testing.expectEqual(false, $('#required2').required)\n</script>\n\n<script id=\"required_set\">\n  {\n    const input = document.createElement('input')\n    testing.expectEqual(false, input.required)\n\n    input.required = true\n    testing.expectEqual(true, input.required)\n    testing.expectEqual('', input.getAttribute('required'))\n\n    input.required = false\n    testing.expectEqual(false, input.required)\n    testing.expectEqual(null, input.getAttribute('required'))\n  }\n</script>\n\n<script id=\"required_reflects_to_attribute\">\n  {\n    const input = document.createElement('input')\n    testing.expectEqual(null, input.getAttribute('required'))\n    testing.expectFalse(input.outerHTML.includes('required'))\n\n    input.required = true\n    testing.expectEqual('', input.getAttribute('required'))\n    testing.expectTrue(input.outerHTML.includes('required'))\n\n    input.required = false\n    testing.expectEqual(null, input.getAttribute('required'))\n    testing.expectFalse(input.outerHTML.includes('required'))\n  }\n</script>\n\n<script id=\"checked_pseudoclass\">\n  // At this point, check1 is unchecked, check2 is checked (from checked_set test)\n  // radio2 is checked (from checked_set test)\n\n  // querySelector should find the first checked input\n  testing.expectEqual($('#check2'), document.querySelector('input:checked'))\n  testing.expectEqual(null, document.querySelector('#check1:checked'))\n  testing.expectEqual($('#check2'), document.querySelector('#check2:checked'))\n\n  {\n    const checkedInputs = document.querySelectorAll('input:checked')\n    testing.expectEqual(2, checkedInputs.length)\n    testing.expectEqual($('#check2'), checkedInputs[0])\n    testing.expectEqual($('#radio2'), checkedInputs[1])\n  }\n\n  // Text inputs should never match :checked\n  testing.expectEqual(null, document.querySelector('#text1:checked'))\n  testing.expectEqual(null, document.querySelector('#text2:checked'))\n\n  // Compound selectors\n  testing.expectEqual($('#check2'), document.querySelector('input[type=\"checkbox\"]:checked'))\n  testing.expectEqual($('#radio2'), document.querySelector('input[type=\"radio\"]:checked'))\n  testing.expectEqual($('#radio2'), document.querySelector('input[type=\"radio\"][name=\"group1\"]:checked'))\n\n  // Change state and verify selector updates dynamically\n  $('#check2').checked = false\n  $('#radio3').checked = true\n\n  testing.expectEqual(null, document.querySelector('input[type=\"checkbox\"]:checked'))\n  testing.expectEqual($('#radio3'), document.querySelector('input[type=\"radio\"]:checked'))\n  testing.expectEqual($('#radio3'), document.querySelector('input[type=\"radio\"][name=\"group1\"]:checked'))\n</script>\n\n<script id=related>\n  {\n    let input_checked = document.createElement('input')\n    testing.expectEqual(false, input_checked.defaultChecked);\n    testing.expectEqual(false, input_checked.checked);\n\n    input_checked.defaultChecked = true;\n    testing.expectEqual(true, input_checked.defaultChecked);\n    testing.expectEqual(true, input_checked.checked);\n\n    input_checked.checked = false;\n    testing.expectEqual(true, input_checked.defaultChecked);\n    testing.expectEqual(false, input_checked.checked);\n\n    input_checked.defaultChecked = true;\n    testing.expectEqual(false, input_checked.checked);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/input_click.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- Checkbox click tests -->\n<input id=\"checkbox1\" type=\"checkbox\">\n<input id=\"checkbox2\" type=\"checkbox\" checked>\n<input id=\"checkbox_disabled\" type=\"checkbox\" disabled>\n\n<!-- Radio click tests -->\n<input id=\"radio1\" type=\"radio\" name=\"clickgroup\" checked>\n<input id=\"radio2\" type=\"radio\" name=\"clickgroup\">\n<input id=\"radio3\" type=\"radio\" name=\"clickgroup\">\n<input id=\"radio_disabled\" type=\"radio\" name=\"clickgroup\" disabled>\n\n<script id=\"checkbox_click_toggles\">\n  {\n    const cb = $('#checkbox1');\n    testing.expectEqual(false, cb.checked);\n\n    cb.click();\n    testing.expectEqual(true, cb.checked);\n\n    cb.click();\n    testing.expectEqual(false, cb.checked);\n  }\n</script>\n\n<script id=\"checkbox_click_preventDefault_reverts\">\n  {\n    const cb = document.createElement('input');\n    cb.type = 'checkbox';\n    testing.expectEqual(false, cb.checked);\n\n    cb.addEventListener('click', (e) => {\n      testing.expectEqual(true, cb.checked, 'checkbox should be checked during click handler');\n      e.preventDefault();\n    });\n\n    cb.click();\n    testing.expectEqual(false, cb.checked, 'checkbox should revert after preventDefault');\n  }\n</script>\n\n<script id=\"checkbox_click_events_order\">\n  {\n    const cb = document.createElement('input');\n    cb.type = 'checkbox';\n    document.body.appendChild(cb);\n\n    const events = [];\n\n    cb.addEventListener('click', () => events.push('click'));\n    cb.addEventListener('input', () => events.push('input'));\n    cb.addEventListener('change', () => events.push('change'));\n\n    cb.click();\n\n    testing.expectEqual(3, events.length);\n    testing.expectEqual('click', events[0]);\n    testing.expectEqual('input', events[1]);\n    testing.expectEqual('change', events[2]);\n\n    document.body.removeChild(cb);\n  }\n</script>\n\n<script id=\"checkbox_click_preventDefault_no_input_change\">\n  {\n    const cb = document.createElement('input');\n    cb.type = 'checkbox';\n    document.body.appendChild(cb);\n\n    const events = [];\n\n    cb.addEventListener('click', (e) => {\n      events.push('click');\n      e.preventDefault();\n    });\n    cb.addEventListener('input', () => events.push('input'));\n    cb.addEventListener('change', () => events.push('change'));\n\n    cb.click();\n\n    testing.expectEqual(1, events.length, 'only click event should fire');\n    testing.expectEqual('click', events[0]);\n\n    document.body.removeChild(cb);\n  }\n</script>\n\n<script id=\"checkbox_click_state_visible_in_handler\">\n  {\n    const cb = document.createElement('input');\n    cb.type = 'checkbox';\n    cb.checked = true;\n\n    cb.addEventListener('click', (e) => {\n      testing.expectEqual(false, cb.checked, 'should see toggled state in handler');\n      e.preventDefault();\n      testing.expectEqual(false, cb.checked, 'should still be toggled after preventDefault in handler');\n    });\n\n    cb.click();\n    testing.expectEqual(true, cb.checked, 'should revert to original state after handler completes');\n  }\n</script>\n\n<script id=\"radio_click_checks_clicked\">\n  {\n    const r1 = $('#radio1');\n    const r2 = $('#radio2');\n\n    testing.expectEqual(true, r1.checked);\n    testing.expectEqual(false, r2.checked);\n\n    r2.click();\n    testing.expectEqual(false, r1.checked);\n    testing.expectEqual(true, r2.checked);\n  }\n</script>\n\n<script id=\"radio_click_preventDefault_reverts\">\n  {\n    const r1 = document.createElement('input');\n    r1.type = 'radio';\n    r1.name = 'testgroup';\n    r1.checked = true;\n\n    const r2 = document.createElement('input');\n    r2.type = 'radio';\n    r2.name = 'testgroup';\n\n    document.body.appendChild(r1);\n    document.body.appendChild(r2);\n\n    r2.addEventListener('click', (e) => {\n      testing.expectEqual(false, r1.checked, 'r1 should be unchecked during click handler');\n      testing.expectEqual(true, r2.checked, 'r2 should be checked during click handler');\n      e.preventDefault();\n    });\n\n    r2.click();\n\n    testing.expectEqual(true, r1.checked, 'r1 should be restored after preventDefault');\n    testing.expectEqual(false, r2.checked, 'r2 should revert after preventDefault');\n\n    document.body.removeChild(r1);\n    document.body.removeChild(r2);\n  }\n</script>\n\n<script id=\"radio_click_events_order\">\n  {\n    const r = document.createElement('input');\n    r.type = 'radio';\n    r.name = 'eventtest';\n    document.body.appendChild(r);\n\n    const events = [];\n\n    r.addEventListener('click', () => events.push('click'));\n    r.addEventListener('input', () => events.push('input'));\n    r.addEventListener('change', () => events.push('change'));\n\n    r.click();\n\n    testing.expectEqual(3, events.length);\n    testing.expectEqual('click', events[0]);\n    testing.expectEqual('input', events[1]);\n    testing.expectEqual('change', events[2]);\n\n    document.body.removeChild(r);\n  }\n</script>\n\n<script id=\"radio_click_already_checked_no_events\">\n  {\n    const r = document.createElement('input');\n    r.type = 'radio';\n    r.name = 'alreadytest';\n    r.checked = true;\n    document.body.appendChild(r);\n\n    const events = [];\n\n    r.addEventListener('click', () => events.push('click'));\n    r.addEventListener('input', () => events.push('input'));\n    r.addEventListener('change', () => events.push('change'));\n\n    r.click();\n\n    testing.expectEqual(1, events.length, 'only click event should fire for already-checked radio');\n    testing.expectEqual('click', events[0]);\n\n    document.body.removeChild(r);\n  }\n</script>\n\n<script id=\"disabled_checkbox_no_click\">\n  {\n    const cb = $('#checkbox_disabled');\n    const events = [];\n\n    cb.addEventListener('click', () => events.push('click'));\n    cb.addEventListener('input', () => events.push('input'));\n    cb.addEventListener('change', () => events.push('change'));\n\n    cb.click();\n\n    testing.expectEqual(0, events.length, 'disabled checkbox should not fire any events');\n  }\n</script>\n\n<script id=\"disabled_radio_no_click\">\n  {\n    const r = $('#radio_disabled');\n    const events = [];\n\n    r.addEventListener('click', () => events.push('click'));\n    r.addEventListener('input', () => events.push('input'));\n    r.addEventListener('change', () => events.push('change'));\n\n    r.click();\n\n    testing.expectEqual(0, events.length, 'disabled radio should not fire any events');\n  }\n</script>\n\n<script id=\"input_and_change_are_trusted\">\n  {\n    const cb = document.createElement('input');\n    cb.type = 'checkbox';\n    document.body.appendChild(cb);\n\n    let inputEvent = null;\n    let changeEvent = null;\n\n    cb.addEventListener('input', (e) => inputEvent = e);\n    cb.addEventListener('change', (e) => changeEvent = e);\n\n    cb.click();\n\n    testing.expectEqual(true, inputEvent.isTrusted, 'input event should be trusted');\n    testing.expectEqual(true, inputEvent.bubbles, 'input event should bubble');\n    testing.expectEqual(false, inputEvent.cancelable, 'input event should not be cancelable');\n\n    testing.expectEqual(true, changeEvent.isTrusted, 'change event should be trusted');\n    testing.expectEqual(true, changeEvent.bubbles, 'change event should bubble');\n    testing.expectEqual(false, changeEvent.cancelable, 'change event should not be cancelable');\n\n    document.body.removeChild(cb);\n  }\n</script>\n\n<script id=\"multiple_radios_click_sequence\">\n  {\n    const r1 = $('#radio1');\n    const r2 = $('#radio2');\n    const r3 = $('#radio3');\n\n    // Reset to known state\n    r1.checked = true;\n\n    testing.expectEqual(true, r1.checked);\n    testing.expectEqual(false, r2.checked);\n    testing.expectEqual(false, r3.checked);\n\n    r2.click();\n    testing.expectEqual(false, r1.checked);\n    testing.expectEqual(true, r2.checked);\n    testing.expectEqual(false, r3.checked);\n\n    r3.click();\n    testing.expectEqual(false, r1.checked);\n    testing.expectEqual(false, r2.checked);\n    testing.expectEqual(true, r3.checked);\n\n    r1.click();\n    testing.expectEqual(true, r1.checked);\n    testing.expectEqual(false, r2.checked);\n    testing.expectEqual(false, r3.checked);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/input_radio.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<input id=\"radio1\" type=\"radio\" name=\"group1\" checked>\n<input id=\"radio2\" type=\"radio\" name=\"group1\">\n<input id=\"radio3\" type=\"radio\" name=\"group1\">\n\n<input id=\"radio4\" type=\"radio\" name=\"group2\">\n\n<script id=\"radio_grouping_basic\">\n  testing.expectEqual(true, $('#radio1').checked)\n  testing.expectEqual(false, $('#radio2').checked)\n  testing.expectEqual(false, $('#radio3').checked)\n\n  $('#radio2').checked = true\n  testing.expectEqual(false, $('#radio1').checked)\n  testing.expectEqual(true, $('#radio2').checked)\n  testing.expectEqual(false, $('#radio3').checked)\n\n  $('#radio3').checked = true\n  testing.expectEqual(false, $('#radio1').checked)\n  testing.expectEqual(false, $('#radio2').checked)\n  testing.expectEqual(true, $('#radio3').checked)\n</script>\n\n<script id=\"radio_grouping_different_groups\">\n  $('#radio1').checked = true\n  $('#radio4').checked = true\n  testing.expectEqual(true, $('#radio1').checked)\n  testing.expectEqual(true, $('#radio4').checked)\n\n  $('#radio2').checked = true\n  testing.expectEqual(false, $('#radio1').checked)\n  testing.expectEqual(true, $('#radio2').checked)\n  testing.expectEqual(true, $('#radio4').checked)\n</script>\n\n<script id=\"radio_grouping_unchecking\">\n  $('#radio1').checked = true\n  testing.expectEqual(true, $('#radio1').checked)\n\n  $('#radio1').checked = false\n  testing.expectEqual(false, $('#radio1').checked)\n  testing.expectEqual(false, $('#radio2').checked)\n  testing.expectEqual(false, $('#radio3').checked)\n</script>\n\n<form id=\"radio_form1\">\n  <input id=\"radio_form1_a\" type=\"radio\" name=\"formgroup\">\n  <input id=\"radio_form1_b\" type=\"radio\" name=\"formgroup\">\n</form>\n\n<form id=\"radio_form2\">\n  <input id=\"radio_form2_a\" type=\"radio\" name=\"formgroup\">\n  <input id=\"radio_form2_b\" type=\"radio\" name=\"formgroup\">\n</form>\n\n<input id=\"radio_no_form_a\" type=\"radio\" name=\"formgroup\">\n<input id=\"radio_no_form_b\" type=\"radio\" name=\"formgroup\">\n\n<script id=\"radio_grouping_same_name_different_forms\">\n  $('#radio_form1_a').checked = true\n  $('#radio_form2_a').checked = true\n  $('#radio_no_form_a').checked = true\n\n  testing.expectEqual(true, $('#radio_form1_a').checked)\n  testing.expectEqual(true, $('#radio_form2_a').checked)\n  testing.expectEqual(true, $('#radio_no_form_a').checked)\n\n  $('#radio_form1_b').checked = true\n  testing.expectEqual(false, $('#radio_form1_a').checked)\n  testing.expectEqual(true, $('#radio_form1_b').checked)\n  testing.expectEqual(true, $('#radio_form2_a').checked)\n  testing.expectEqual(true, $('#radio_no_form_a').checked)\n\n  $('#radio_no_form_b').checked = true\n  testing.expectEqual(false, $('#radio_form1_a').checked)\n  testing.expectEqual(true, $('#radio_form1_b').checked)\n  testing.expectEqual(true, $('#radio_form2_a').checked)\n  testing.expectEqual(false, $('#radio_no_form_a').checked)\n  testing.expectEqual(true, $('#radio_no_form_b').checked)\n</script>\n\n<form id=\"radio_form3\"></form>\n<input id=\"radio_form_attr_a\" type=\"radio\" name=\"attrgroup\" form=\"radio_form3\">\n<input id=\"radio_form_attr_b\" type=\"radio\" name=\"attrgroup\" form=\"radio_form3\">\n<input id=\"radio_form_attr_c\" type=\"radio\" name=\"attrgroup\">\n\n<script id=\"radio_grouping_form_attribute\">\n  $('#radio_form_attr_a').checked = true\n  $('#radio_form_attr_c').checked = true\n\n  testing.expectEqual(true, $('#radio_form_attr_a').checked)\n  testing.expectEqual(false, $('#radio_form_attr_b').checked)\n  testing.expectEqual(true, $('#radio_form_attr_c').checked)\n\n  $('#radio_form_attr_b').checked = true\n  testing.expectEqual(false, $('#radio_form_attr_a').checked)\n  testing.expectEqual(true, $('#radio_form_attr_b').checked)\n  testing.expectEqual(true, $('#radio_form_attr_c').checked)\n</script>\n\n<input id=\"radio_no_name_a\" type=\"radio\">\n<input id=\"radio_no_name_b\" type=\"radio\">\n\n<script id=\"radio_no_name_no_grouping\">\n  $('#radio_no_name_a').checked = true\n  $('#radio_no_name_b').checked = true\n\n  testing.expectEqual(true, $('#radio_no_name_a').checked)\n  testing.expectEqual(true, $('#radio_no_name_b').checked)\n</script>\n\n<input id=\"radio_empty_name_a\" type=\"radio\" name=\"\">\n<input id=\"radio_empty_name_b\" type=\"radio\" name=\"\">\n\n<script id=\"radio_empty_name_no_grouping\">\n  $('#radio_empty_name_a').checked = true\n  $('#radio_empty_name_b').checked = true\n\n  testing.expectEqual(true, $('#radio_empty_name_a').checked)\n  testing.expectEqual(true, $('#radio_empty_name_b').checked)\n</script>\n\n<input id=\"radio_case_a\" type=\"radio\" name=\"CaseSensitive\">\n<input id=\"radio_case_b\" type=\"radio\" name=\"casesensitive\">\n<input id=\"radio_case_c\" type=\"radio\" name=\"CaseSensitive\">\n\n<script id=\"radio_name_case_sensitive\">\n  $('#radio_case_a').checked = true\n  $('#radio_case_b').checked = true\n\n  testing.expectEqual(true, $('#radio_case_a').checked)\n  testing.expectEqual(true, $('#radio_case_b').checked)\n\n  $('#radio_case_c').checked = true\n  testing.expectEqual(false, $('#radio_case_a').checked)\n  testing.expectEqual(true, $('#radio_case_b').checked)\n  testing.expectEqual(true, $('#radio_case_c').checked)\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/label.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<label id=\"l1\" for=\"input1\">Name</label>\n<input id=\"input1\">\n\n<script id=\"htmlFor\">\n  {\n    const l1 = document.getElementById('l1');\n    testing.expectEqual('input1', l1.htmlFor);\n\n    l1.htmlFor = 'input2';\n    testing.expectEqual('input2', l1.htmlFor);\n\n    const l2 = document.createElement('label');\n    testing.expectEqual('', l2.htmlFor);\n  }\n</script>\n\n<label id=\"l2\" for=\"input1\"><span>Name</span></label>\n<input id=\"input2\" type=\"text\">\n<input id=\"input-hidden\" type=\"hidden\">\n<select id=\"sel1\"><option>a</option></select>\n<button id=\"btn1\">Click</button>\n<label id=\"l3\"><input id=\"input3\"><span>desc</span></label>\n<label id=\"l4\"><span>no control here</span></label>\n<label id=\"l5\"><label id=\"l5-inner\"><input id=\"input5\"></label></label>\n\n<script id=\"control\">\n  {\n    // for attribute pointing to a text input\n    const l2 = document.getElementById('l2');\n    testing.expectEqual('input1', l2.control.id);\n\n    // for attribute pointing to a non-existent id\n    const lMissing = document.createElement('label');\n    lMissing.htmlFor = 'does-not-exist';\n    testing.expectEqual(null, lMissing.control);\n\n    // for attribute pointing to a hidden input -> not labelable, returns null\n    const lHidden = document.createElement('label');\n    lHidden.htmlFor = 'input-hidden';\n    document.body.appendChild(lHidden);\n    testing.expectEqual(null, lHidden.control);\n\n    // for attribute pointing to a select\n    const lSel = document.createElement('label');\n    lSel.htmlFor = 'sel1';\n    document.body.appendChild(lSel);\n    testing.expectEqual('sel1', lSel.control.id);\n\n    // for attribute pointing to a button\n    const lBtn = document.createElement('label');\n    lBtn.htmlFor = 'btn1';\n    document.body.appendChild(lBtn);\n    testing.expectEqual('btn1', lBtn.control.id);\n\n    // no for attribute: first labelable descendant\n    const l3 = document.getElementById('l3');\n    testing.expectEqual('input3', l3.control.id);\n\n    // no for attribute: no labelable descendant -> null\n    const l4 = document.getElementById('l4');\n    testing.expectEqual(null, l4.control);\n\n    // no for attribute: nested labels, first labelable in tree order\n    const l5 = document.getElementById('l5');\n    testing.expectEqual('input5', l5.control.id);\n\n    // label with no for and not in document -> null\n    const lDetached = document.createElement('label');\n    testing.expectEqual(null, lDetached.control);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/li.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<ol>\n  <li id=\"li1\" value=\"5\">Item</li>\n  <li id=\"li2\">Item</li>\n</ol>\n\n<script id=\"value\">\n  {\n    const li1 = document.getElementById('li1');\n    testing.expectEqual(5, li1.value);\n\n    li1.value = 10;\n    testing.expectEqual(10, li1.value);\n\n    const li2 = document.getElementById('li2');\n    testing.expectEqual(0, li2.value);\n\n    li2.value = -3;\n    testing.expectEqual(-3, li2.value);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/link.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=link>\n  let l2 = document.createElement('link');\n  testing.expectEqual('', l2.href);\n  l2.href = 'https://lightpanda.io/opensource-browser/15';\n  testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);\n\n  l2.href = '/over/9000';\n  testing.expectEqual(testing.ORIGIN + '/over/9000', l2.href);\n\n  l2.crossOrigin = 'nope';\n  testing.expectEqual('anonymous', l2.crossOrigin);\n\n  l2.crossOrigin = 'use-Credentials';\n  testing.expectEqual('use-credentials', l2.crossOrigin);\n\n  l2.crossOrigin = '';\n  testing.expectEqual('anonymous', l2.crossOrigin);\n</script>\n\n<script id=\"link-load-event\">\n{\n  // A link with rel=stylesheet and a non-empty href fires a load event when appended to the DOM\n  const link = document.createElement('link');\n  link.rel = 'stylesheet';\n  link.href = 'https://lightpanda.io/opensource-browser/15';\n\n  testing.async(async () => {\n    const result = await new Promise(resolve => {\n      link.addEventListener('load', ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {\n        testing.expectEqual(false, bubbles);\n        testing.expectEqual(false, cancelBubble);\n        testing.expectEqual(false, cancelable);\n        testing.expectEqual(false, composed);\n        testing.expectEqual(true, isTrusted);\n        testing.expectEqual(link, target);\n        resolve(true);\n      });\n      document.head.appendChild(link);\n    });\n    testing.expectEqual(true, result);\n  });\n  testing.expectEqual(true, true);\n}\n</script>\n\n<script id=\"link-no-load-without-href\">\n{\n  // A link with rel=stylesheet but no href should not fire a load event\n  let fired = false;\n  const link = document.createElement('link');\n  link.rel = 'stylesheet';\n  link.addEventListener('load', () => { fired = true; });\n  document.head.appendChild(link);\n  testing.eventually(() => testing.expectEqual(false, fired));\n}\n</script>\n\n<script id=\"link-no-load-wrong-rel\">\n{\n  // A link without rel=stylesheet should not fire a load event\n  let fired = false;\n  const link = document.createElement('link');\n  link.href = 'https://lightpanda.io/opensource-browser/15';\n  link.addEventListener('load', () => { fired = true; });\n  document.head.appendChild(link);\n  testing.eventually(() => testing.expectEqual(false, fired));\n}\n</script>\n\n<script id=\"lazy-href-set\">\n{\n  let result = false;\n  const link = document.createElement(\"link\");\n  link.rel = \"stylesheet\";\n  link.onload = () => result = true;\n  // Append to DOM,\n  document.head.appendChild(link);\n  // then set href.\n  link.href = 'https://lightpanda.io/opensource-browser/15';\n\n  testing.eventually(() => testing.expectEqual(true, result));\n}\n</script>\n\n<script id=\"refs\">\n{\n  const rels = ['stylesheet', 'preload', 'modulepreload'];\n  const results = rels.map(() => false);\n  rels.forEach((rel, i) => {\n    let link = document.createElement('link')\n    link.rel = rel;\n    link.href = '/nope';\n    link.onload = () => results[i] = true;\n    document.documentElement.appendChild(link);\n  });\n\n\n  testing.eventually(() => {\n    results.forEach((r) => {\n      testing.expectEqual(true, r);\n    });\n  });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/media.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<audio id=\"audio1\" src=\"test.mp3\"></audio>\n<video id=\"video1\" src=\"test.mp4\"></video>\n\n<script id=\"canPlayType_audio\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual('probably', audio.canPlayType('audio/mp3'));\n    testing.expectEqual('probably', audio.canPlayType('audio/mpeg'));\n    testing.expectEqual('probably', audio.canPlayType('audio/webm'));\n    testing.expectEqual('probably', audio.canPlayType('audio/ogg'));\n    testing.expectEqual('probably', audio.canPlayType('audio/wav'));\n    testing.expectEqual('maybe', audio.canPlayType('audio/aac'));\n    testing.expectEqual('maybe', audio.canPlayType('audio/flac'));\n    testing.expectEqual('', audio.canPlayType('audio/invalid'));\n    testing.expectEqual('', audio.canPlayType('video/invalid'));\n  }\n</script>\n\n<script id=\"canPlayType_video\">\n  {\n    const video = document.getElementById('video1');\n    testing.expectEqual('probably', video.canPlayType('video/mp4'));\n    testing.expectEqual('probably', video.canPlayType('video/webm'));\n    testing.expectEqual('probably', video.canPlayType('video/ogg'));\n    testing.expectEqual('', video.canPlayType('video/invalid'));\n  }\n</script>\n\n<script id=\"canPlayType_with_codecs\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual('probably', audio.canPlayType('audio/mp3; codecs=\"mp3\"'));\n    testing.expectEqual('probably', audio.canPlayType('video/mp4; codecs=\"avc1.42E01E\"'));\n  }\n</script>\n\n<script id=\"play_pause\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(true, audio.paused);\n\n    audio.play();\n    testing.expectEqual(false, audio.paused);\n\n    audio.pause();\n    testing.expectEqual(true, audio.paused);\n  }\n</script>\n\n<script id=\"play_pause_events\">\n  {\n    const audio = document.createElement('audio');\n    const events = [];\n\n    audio.addEventListener('play', () => events.push('play'));\n    audio.addEventListener('playing', () => events.push('playing'));\n    audio.addEventListener('pause', () => events.push('pause'));\n    audio.addEventListener('emptied', () => events.push('emptied'));\n\n    // First play: paused -> playing, fires play + playing\n    audio.play();\n    testing.expectEqual('play,playing', events.join(','));\n\n    // Second play: already playing, no events\n    audio.play();\n    testing.expectEqual('play,playing', events.join(','));\n\n    // Pause: playing -> paused, fires pause\n    audio.pause();\n    testing.expectEqual('play,playing,pause', events.join(','));\n\n    // Second pause: already paused, no event\n    audio.pause();\n    testing.expectEqual('play,playing,pause', events.join(','));\n\n    // Third play: resume from pause, fires play + playing (verified in Chrome)\n    audio.play();\n    testing.expectEqual('play,playing,pause,play,playing', events.join(','));\n\n    // Pause again\n    audio.pause();\n    testing.expectEqual('play,playing,pause,play,playing,pause', events.join(','));\n\n    // Load: resets state, fires emptied\n    audio.load();\n    testing.expectEqual('play,playing,pause,play,playing,pause,emptied', events.join(','));\n\n    // Play after load: fires play + playing\n    audio.play();\n    testing.expectEqual('play,playing,pause,play,playing,pause,emptied,play,playing', events.join(','));\n  }\n</script>\n\n<script id=\"volume_muted\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(1.0, audio.volume);\n    testing.expectEqual(false, audio.muted);\n\n    audio.volume = 0.5;\n    testing.expectEqual(0.5, audio.volume);\n\n    audio.volume = 1.5; // Should clamp to 1.0\n    testing.expectEqual(1.0, audio.volume);\n\n    audio.volume = -0.5; // Should clamp to 0.0\n    testing.expectEqual(0.0, audio.volume);\n\n    audio.muted = true;\n    testing.expectEqual(true, audio.muted);\n  }\n</script>\n\n<script id=\"playback_rate\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(1.0, audio.playbackRate);\n\n    audio.playbackRate = 2.0;\n    testing.expectEqual(2.0, audio.playbackRate);\n\n    audio.playbackRate = 0.5;\n    testing.expectEqual(0.5, audio.playbackRate);\n  }\n</script>\n\n<script id=\"currentTime\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(0, audio.currentTime);\n\n    audio.currentTime = 10.5;\n    testing.expectEqual(10.5, audio.currentTime);\n  }\n</script>\n\n<script id=\"duration_nan\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(true, isNaN(audio.duration));\n  }\n</script>\n\n<script id=\"ended_seeking\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(false, audio.ended);\n    testing.expectEqual(false, audio.seeking);\n  }\n</script>\n\n<script id=\"ready_state_network_state\">\n  {\n    // Create a fresh element to test initial state\n    const audio = document.createElement('audio');\n    testing.expectEqual(0, audio.readyState); // HAVE_NOTHING initially\n    testing.expectEqual(0, audio.networkState); // NETWORK_EMPTY initially\n\n    audio.play();\n    // In headless mode, play() immediately succeeds without actual media loading\n    testing.expectEqual(4, audio.readyState); // HAVE_ENOUGH_DATA in headless\n    testing.expectEqual(1, audio.networkState); // NETWORK_IDLE in headless\n  }\n</script>\n\n<script id=\"load_reset\">\n  {\n    // Create a fresh element to test load() behavior\n    const audio = document.createElement('audio');\n    audio.currentTime = 50;\n    audio.play();\n\n    audio.load();\n    testing.expectEqual(true, audio.paused);\n    testing.expectEqual(0, audio.currentTime);\n    testing.expectEqual(0, audio.readyState);\n  }\n</script>\n\n<script id=\"attributes_autoplay\">\n  {\n    const video = document.createElement('video');\n    testing.expectEqual(false, video.autoplay);\n\n    video.setAttribute('autoplay', '');\n    testing.expectEqual(true, video.autoplay);\n\n    video.autoplay = false;\n    testing.expectEqual(false, video.autoplay);\n\n    video.autoplay = true;\n    testing.expectEqual(true, video.autoplay);\n  }\n</script>\n\n<script id=\"attributes_controls\">\n  {\n    const video = document.createElement('video');\n    testing.expectEqual(false, video.controls);\n\n    video.controls = true;\n    testing.expectEqual(true, video.controls);\n\n    video.controls = false;\n    testing.expectEqual(false, video.controls);\n  }\n</script>\n\n<script id=\"attributes_loop\">\n  {\n    const audio = document.createElement('audio');\n    testing.expectEqual(false, audio.loop);\n\n    audio.loop = true;\n    testing.expectEqual(true, audio.loop);\n  }\n</script>\n\n<script id=\"attributes_preload\">\n  {\n    const audio = document.createElement('audio');\n    testing.expectEqual('auto', audio.preload);\n\n    audio.preload = 'none';\n    testing.expectEqual('none', audio.preload);\n\n    audio.preload = 'metadata';\n    testing.expectEqual('metadata', audio.preload);\n  }\n</script>\n\n<script id=\"attributes_src\">\n  {\n    const audio = document.createElement('audio');\n    testing.expectEqual('', audio.src);\n\n    audio.src = 'test.mp3';\n    testing.expectEqual(testing.BASE_URL + 'element/html/test.mp3', audio.src);\n  }\n</script>\n\n<script id=\"video_poster\">\n  {\n    const video = document.createElement('video');\n    testing.expectEqual('', video.poster);\n\n    video.poster = 'poster.jpg';\n    testing.expectEqual(testing.BASE_URL + 'element/html/poster.jpg', video.poster);\n  }\n</script>\n\n<script id=\"video_dimensions\">\n  {\n    const video = document.getElementById('video1');\n    testing.expectEqual(0, video.videoWidth);\n    testing.expectEqual(0, video.videoHeight);\n  }\n</script>\n\n<script id=\"constants\">\n  {\n    const audio = document.getElementById('audio1');\n    testing.expectEqual(0, audio.NETWORK_EMPTY);\n    testing.expectEqual(1, audio.NETWORK_IDLE);\n    testing.expectEqual(2, audio.NETWORK_LOADING);\n    testing.expectEqual(3, audio.NETWORK_NO_SOURCE);\n\n    testing.expectEqual(0, audio.HAVE_NOTHING);\n    testing.expectEqual(1, audio.HAVE_METADATA);\n    testing.expectEqual(2, audio.HAVE_CURRENT_DATA);\n    testing.expectEqual(3, audio.HAVE_FUTURE_DATA);\n    testing.expectEqual(4, audio.HAVE_ENOUGH_DATA);\n  }\n</script>\n\n<script id=\"create_audio_element\">\n  {\n    const audio = document.createElement('audio');\n    testing.expectEqual('[object HTMLAudioElement]', audio.toString());\n    testing.expectEqual(true, audio.paused);\n  }\n\n  // Create with `Audio` constructor.\n  {\n    const audio = new Audio();\n    testing.expectEqual(true, audio instanceof HTMLAudioElement);\n    testing.expectEqual(\"[object HTMLAudioElement]\", audio.toString());\n    testing.expectEqual(true, audio.paused);\n    testing.expectEqual(\"auto\", audio.getAttribute(\"preload\"));\n  }\n</script>\n\n<script id=\"create_video_element\">\n  {\n    const video = document.createElement('video');\n    testing.expectEqual('[object HTMLVideoElement]', video.toString());\n    testing.expectEqual(true, video.paused);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/ol.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<ol id=\"ol1\" start=\"5\" reversed type=\"a\">\n  <li>Item</li>\n</ol>\n<ol id=\"ol2\">\n  <li>Item</li>\n</ol>\n\n<script id=\"start\">\n  {\n    const ol1 = document.getElementById('ol1');\n    testing.expectEqual(5, ol1.start);\n\n    ol1.start = 10;\n    testing.expectEqual(10, ol1.start);\n\n    const ol2 = document.getElementById('ol2');\n    testing.expectEqual(1, ol2.start);\n  }\n</script>\n\n<script id=\"reversed\">\n  {\n    const ol1 = document.getElementById('ol1');\n    testing.expectEqual(true, ol1.reversed);\n\n    ol1.reversed = false;\n    testing.expectEqual(false, ol1.reversed);\n\n    const ol2 = document.getElementById('ol2');\n    testing.expectEqual(false, ol2.reversed);\n\n    ol2.reversed = true;\n    testing.expectEqual(true, ol2.reversed);\n  }\n</script>\n\n<script id=\"type\">\n  {\n    const ol1 = document.getElementById('ol1');\n    testing.expectEqual('a', ol1.type);\n\n    ol1.type = '1';\n    testing.expectEqual('1', ol1.type);\n\n    const ol2 = document.getElementById('ol2');\n    testing.expectEqual('1', ol2.type);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/optgroup.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<select>\n  <optgroup id=\"og1\" label=\"Group 1\" disabled>\n    <option>A</option>\n  </optgroup>\n  <optgroup id=\"og2\" label=\"Group 2\">\n    <option>B</option>\n  </optgroup>\n</select>\n\n<script id=\"disabled\">\n  {\n    const og1 = document.getElementById('og1');\n    testing.expectEqual(true, og1.disabled);\n\n    og1.disabled = false;\n    testing.expectEqual(false, og1.disabled);\n\n    const og2 = document.getElementById('og2');\n    testing.expectEqual(false, og2.disabled);\n\n    og2.disabled = true;\n    testing.expectEqual(true, og2.disabled);\n  }\n</script>\n\n<script id=\"label\">\n  {\n    const og1 = document.getElementById('og1');\n    testing.expectEqual('Group 1', og1.label);\n\n    og1.label = 'Updated';\n    testing.expectEqual('Updated', og1.label);\n\n    const og = document.createElement('optgroup');\n    testing.expectEqual('', og.label);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/option.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<select id=\"select1\">\n  <option id=\"opt1\" value=\"val1\">Text 1</option>\n  <option id=\"opt2\" value=\"val2\" selected>Text 2</option>\n  <option id=\"opt3\">Text 3</option>\n  <option id=\"opt4\" disabled>Text 4</option>\n</select>\n\n<script id=\"value_with_attribute\">\n  testing.expectEqual('val1', $('#opt1').value)\n  testing.expectEqual('val2', $('#opt2').value)\n</script>\n\n<script id=\"value_without_attribute\">\n  // When value attribute is not present, value should be text content\n  testing.expectEqual('Text 3', $('#opt3').value)\n</script>\n\n<script id=\"value_set\">\n  $('#opt1').value = 'changed'\n  testing.expectEqual('changed', $('#opt1').value)\n</script>\n\n<script id=\"text\">\n  testing.expectEqual('Text 1', $('#opt1').text)\n  testing.expectEqual('Text 2', $('#opt2').text)\n  testing.expectEqual('Text 3', $('#opt3').text)\n</script>\n\n<script id=\"text_set\">\n  $('#opt1').text = 'New Text 1'\n  testing.expectEqual('New Text 1', $('#opt1').text)\n  testing.expectEqual('New Text 1', $('#opt1').textContent)\n</script>\n\n<script id=\"selected\">\n  testing.expectEqual(false, $('#opt1').selected)\n  testing.expectEqual(true, $('#opt2').selected)\n  testing.expectEqual(false, $('#opt3').selected)\n</script>\n\n<script id=\"selected_set\">\n  $('#opt1').selected = true\n  testing.expectEqual(true, $('#opt1').selected)\n\n  $('#opt2').selected = false\n  testing.expectEqual(false, $('#opt2').selected)\n</script>\n\n<script id=\"defaultSelected\">\n  testing.expectEqual(false, $('#opt1').defaultSelected)\n  testing.expectEqual(true, $('#opt2').defaultSelected)\n  testing.expectEqual(false, $('#opt3').defaultSelected)\n\n  // Setting selected shouldn't change defaultSelected\n  $('#opt1').selected = true\n  testing.expectEqual(false, $('#opt1').defaultSelected)\n</script>\n\n<script id=\"disabled\">\n  testing.expectEqual(false, $('#opt1').disabled)\n  testing.expectEqual(true, $('#opt4').disabled)\n</script>\n\n<script id=\"disabled_set\">\n  $('#opt1').disabled = true\n  testing.expectEqual(true, $('#opt1').disabled)\n\n  $('#opt4').disabled = false\n  testing.expectEqual(false, $('#opt4').disabled)\n</script>\n\n<option id=\"named1\" name=\"choice1\">Named option</option>\n<option id=\"named2\">Unnamed option</option>\n\n<script id=\"name_initial\">\n  testing.expectEqual('choice1', $('#named1').name)\n  testing.expectEqual('', $('#named2').name)\n</script>\n\n<script id=\"name_set\">\n  {\n    const option = document.createElement('option')\n    testing.expectEqual('', option.name)\n\n    option.name = 'opt-name'\n    testing.expectEqual('opt-name', option.name)\n    testing.expectEqual('opt-name', option.getAttribute('name'))\n\n    option.name = 'another-name'\n    testing.expectEqual('another-name', option.name)\n    testing.expectEqual('another-name', option.getAttribute('name'))\n  }\n</script>\n\n<script id=\"name_reflects_to_attribute\">\n  {\n    const option = document.createElement('option')\n    testing.expectEqual(null, option.getAttribute('name'))\n\n    option.name = 'fieldname'\n    testing.expectEqual('fieldname', option.getAttribute('name'))\n    testing.expectTrue(option.outerHTML.includes('name=\"fieldname\"'))\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/picture.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- <script id=\"createElement\">\n{\n    const picture = document.createElement('picture');\n    testing.expectEqual('PICTURE', picture.tagName);\n    testing.expectEqual('[object HTMLPictureElement]', Object.prototype.toString.call(picture));\n}\n</script>\n\n<script id=\"constructor_type\">\n{\n    const picture = document.createElement('picture');\n    testing.expectEqual(true, picture instanceof HTMLElement);\n    testing.expectEqual(true, picture instanceof Element);\n    testing.expectEqual(true, picture instanceof Node);\n}\n</script> -->\n\n<picture id=\"inline-picture\">\n  <source media=\"(min-width: 800px)\" srcset=\"large.jpg\">\n  <source media=\"(min-width: 400px)\" srcset=\"medium.jpg\">\n  <img src=\"small.jpg\" alt=\"Test image\">\n</picture>\n\n<script id=\"inline_picture\">\n{\n    const picture = document.getElementById('inline-picture');\n    testing.expectEqual('PICTURE', picture.tagName);\n    testing.expectEqual(3, picture.children.length);\n\n    const sources = picture.querySelectorAll('source');\n    testing.expectEqual(2, sources.length);\n\n    // const img = picture.querySelector('img');\n    // testing.expectEqual('IMG', img.tagName);\n}\n</script>\n\n<!-- <script id=\"appendChild\">\n{\n    const picture = document.createElement('picture');\n    const source = document.createElement('source');\n    const img = document.createElement('img');\n\n    picture.appendChild(source);\n    picture.appendChild(img);\n\n    testing.expectEqual(2, picture.children.length);\n    testing.expectEqual('SOURCE', picture.children[0].tagName);\n    testing.expectEqual('IMG', picture.children[1].tagName);\n}\n</script>\n -->\n"
  },
  {
    "path": "src/browser/tests/element/html/quote.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<blockquote id=\"q1\" cite=\"https://example.com/source\">Quote</blockquote>\n\n<script id=\"cite\">\n  {\n    const q1 = document.getElementById('q1');\n    testing.expectEqual('https://example.com/source', q1.cite);\n\n    q1.cite = 'https://example.com/other';\n    testing.expectEqual('https://example.com/other', q1.cite);\n\n    const q2 = document.createElement('blockquote');\n    testing.expectEqual('', q2.cite);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/script/async_text.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../../testing.js\"></script>\n\n<script id=force_async>\n{\n   // Dynamically created scripts have async=true by default\n   let s = document.createElement('script');\n   testing.expectEqual(true, s.async);\n\n   // Setting async=false clears the force async flag and removes attribute\n   s.async = false;\n   testing.expectEqual(false, s.async);\n   testing.expectEqual(false, s.hasAttribute('async'));\n\n   // Setting async=true adds the attribute\n   s.async = true;\n   testing.expectEqual(true, s.async);\n   testing.expectEqual(true, s.hasAttribute('async'));\n}\n</script>\n\n<script></script>\n<script id=empty>\n{\n   // Empty parser-inserted script should have async=true (force async retained)\n   let scripts = document.getElementsByTagName('script');\n   let emptyScript = scripts[scripts.length - 2];\n   testing.expectEqual(true, emptyScript.async);\n}\n</script>\n\n<script id=text_content>\n{\n   let s = document.createElement('script');\n   s.appendChild(document.createComment('COMMENT'));\n   s.appendChild(document.createTextNode('  TEXT  '));\n   s.appendChild(document.createProcessingInstruction('P', 'I'));\n   let a = s.appendChild(document.createElement('a'));\n   a.appendChild(document.createTextNode('ELEMENT'));\n\n   // script.text should return only direct Text node children\n   testing.expectEqual('  TEXT  ', s.text);\n   // script.textContent should return all descendant text\n   testing.expectEqual('  TEXT  ELEMENT', s.textContent);\n}\n</script>\n\n<script id=lazy_inline>\n{\n   // Empty script in DOM, then append text - should execute\n   window.lazyScriptRan = false;\n   let s = document.createElement('script');\n   document.head.appendChild(s);\n   // Script is in DOM but empty, so not yet executed\n   testing.expectEqual(false, window.lazyScriptRan);\n   // Append text node with code\n   s.appendChild(document.createTextNode('window.lazyScriptRan = true;'));\n   // Now it should have executed\n   testing.expectEqual(true, window.lazyScriptRan);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/script/dynamic.html",
    "content": "<!DOCTYPE html>\n<head></head>\n<script src=\"../../../testing.js\"></script>\n\n<script id=append_before_src1>\n  loaded1 = 0;\n  const script1 = document.createElement('script');\n  script1.async = false;\n  script1.src = \"dynamic1.js\";\n  document.getElementsByTagName('head')[0].appendChild(script1);\n  testing.eventually(() => {\n    testing.expectEqual(1, loaded1);\n  });\n</script>\n\n<script id=no_double_execute>\n  document.getElementsByTagName('head')[0].appendChild(script1);\n  testing.eventually(() => {\n    testing.expectEqual(1, loaded1);\n  });\n</script>\n\n<script id=append_before_src2>\n  loaded2 = 0;\n  const script2a = document.createElement('script');\n  script2a.src = \"dynamic2.js\";\n  document.getElementsByTagName('head')[0].appendChild(script2a);\n  testing.eventually(() => {\n    testing.expectEqual(2, loaded2);\n  });\n</script>\n\n<script>\n  const script2b = document.createElement('script');\n  document.getElementsByTagName('head')[0].appendChild(script2b);\n  script2b.src = \"dynamic2.js\";\n  script2b.src = \"dynamic2.js\";\n</script>\n\n<script id=src_after_append>\n  testing.eventually(() => {\n    testing.expectEqual(2, loaded2);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/script/dynamic1.js",
    "content": "loaded1 += 1;\n"
  },
  {
    "path": "src/browser/tests/element/html/script/dynamic2.js",
    "content": "loaded2 += 1;\n"
  },
  {
    "path": "src/browser/tests/element/html/script/dynamic_inline.html",
    "content": "<!DOCTYPE html>\n<head></head>\n<script src=\"../../../testing.js\"></script>\n\n<script id=textContent_inline>\n  window.inline_executed = false;\n  const s1 = document.createElement('script');\n  s1.textContent = 'window.inline_executed = true;';\n  document.head.appendChild(s1);\n  testing.expectTrue(window.inline_executed);\n</script>\n\n<script id=text_property_inline>\n  window.text_executed = false;\n  const s2 = document.createElement('script');\n  s2.text = 'window.text_executed = true;';\n  document.head.appendChild(s2);\n  testing.expectTrue(window.text_executed);\n</script>\n\n<script id=innerHTML_inline>\n  window.innerHTML_executed = false;\n  const s3 = document.createElement('script');\n  s3.innerHTML = 'window.innerHTML_executed = true;';\n  document.head.appendChild(s3);\n  testing.expectTrue(window.innerHTML_executed);\n</script>\n\n<script id=no_double_execute_inline>\n  window.inline_counter = 0;\n  const s4 = document.createElement('script');\n  s4.textContent = 'window.inline_counter++;';\n  document.head.appendChild(s4);\n  document.head.appendChild(s4);\n  testing.expectEqual(1, window.inline_counter);\n</script>\n\n<script id=empty_script_no_execute>\n  window.empty_ran = false;\n  const s5 = document.createElement('script');\n  document.head.appendChild(s5);\n  testing.expectFalse(window.empty_ran);\n</script>\n\n<script id=module_inline>\n  window.module_executed = false;\n  const s6 = document.createElement('script');\n  s6.type = 'module';\n  s6.textContent = 'window.module_executed = true;';\n  document.head.appendChild(s6);\n  testing.eventually(() => {\n    testing.expectTrue(window.module_executed);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/script/empty.js",
    "content": ""
  },
  {
    "path": "src/browser/tests/element/html/script/order.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../../testing.js\"></script>\n\n<script defer id=\"remote_defer\" src=\"order_defer.js\"></script>\n<script defer id=\"remote_async\" src=\"order_async.js\"></script>\n\n<script type=module id=\"inline_module\">\n    // inline module is always deferred.\n    list += 'g';\n    testing.expectEqual('abcdefg', list);\n</script>\n\n<script>\n    var list = '';\n</script>\n\n<script id=\"remote\" src=\"order.js\"></script>\n\n<script async id=\"inline_async\">\n    // inline script ignore async\n    list += 'b';\n    testing.expectEqual('ab', list);\n</script>\n\n<script defer id=\"inline_defer\">\n    // inline script ignore defer\n    list += 'c';\n    testing.expectEqual('abc', list);\n</script>\n\n<script id=\"default\">\n    // simple inline script\n    list += 'd';\n    testing.expectEqual('abcd', list);\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/script/order.js",
    "content": "list += 'a';\ntesting.expectEqual('a', list);\n"
  },
  {
    "path": "src/browser/tests/element/html/script/order_async.js",
    "content": "list += 'f';\ntesting.expectEqual('abcdef', list);\n"
  },
  {
    "path": "src/browser/tests/element/html/script/order_defer.js",
    "content": "list += 'e';\ntesting.expectEqual('abcde', list);\n"
  },
  {
    "path": "src/browser/tests/element/html/script/script.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../../testing.js\"></script>\n\n<script id=\"script\">\n{\n   let dom_load = false;\n   let attribute_load = false;\n\n   let s = document.createElement('script');\n   document.documentElement.addEventListener('load', (e) => {\n      testing.expectEqual(s, e.target);\n      dom_load = true;\n   }, true);\n\n   testing.expectEqual('', s.src);\n   s.onload = function(e) {\n      testing.expectEqual(s, e.target);\n      attribute_load = true;\n   }\n   s.src = 'empty.js';\n   testing.expectEqual(testing.BASE_URL + 'element/html/script/empty.js', s.src);\n   document.head.appendChild(s);\n\n   testing.eventually(() => {\n      testing.expectEqual(true, dom_load);\n      testing.expectEqual(true, attribute_load);\n   });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/select.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<select id=\"select1\">\n  <option value=\"val1\">Option 1</option>\n  <option value=\"val2\" selected>Option 2</option>\n  <option value=\"val3\">Option 3</option>\n</select>\n\n<select id=\"select2\">\n  <option value=\"a\">A</option>\n  <option value=\"b\">B</option>\n</select>\n\n<select id=\"select3\" disabled>\n  <option value=\"x\">X</option>\n</select>\n\n<form id=\"form1\">\n  <select id=\"select_in_form\">\n    <option value=\"f1\">Form option</option>\n  </select>\n</form>\n\n<form id=\"form2\"></form>\n<select id=\"select_with_form_attr\" form=\"form2\">\n  <option value=\"f2\">Form attr option</option>\n</select>\n\n<select id=\"select_no_form\">\n  <option value=\"nf\">No form option</option>\n</select>\n\n<script id=\"value_initial\">\n  // Should return value of selected option\n  testing.expectEqual('val2', $('#select1').value)\n\n  // If no option is explicitly selected, first option is implicitly selected\n  testing.expectEqual('a', $('#select2').value)\n</script>\n\n<script id=\"value_set\">\n  $('#select1').value = 'val3'\n  testing.expectEqual('val3', $('#select1').value)\n\n  // The option should now be selected\n  const opt3 = $('#select1').querySelector('option[value=\"val3\"]')\n  testing.expectEqual(true, opt3.selected)\n\n  // Other options should be unselected\n  const opt2 = $('#select1').querySelector('option[value=\"val2\"]')\n  testing.expectEqual(false, opt2.selected)\n</script>\n\n<script id=\"disabled_initial\">\n  testing.expectEqual(false, $('#select1').disabled)\n  testing.expectEqual(true, $('#select3').disabled)\n</script>\n\n<script id=\"disabled_set\">\n  $('#select1').disabled = true\n  testing.expectEqual(true, $('#select1').disabled)\n\n  $('#select3').disabled = false\n  testing.expectEqual(false, $('#select3').disabled)\n</script>\n\n<script id=\"form_ancestor\">\n  const selectInForm = $('#select_in_form')\n  testing.expectEqual('FORM', selectInForm.form.tagName)\n  testing.expectEqual('form1', selectInForm.form.id)\n</script>\n\n<script id=\"form_attribute\">\n  const selectWithFormAttr = $('#select_with_form_attr')\n  testing.expectEqual('FORM', selectWithFormAttr.form.tagName)\n  testing.expectEqual('form2', selectWithFormAttr.form.id)\n</script>\n\n<script id=\"form_null\">\n  const selectNoForm = $('#select_no_form')\n  testing.expectEqual(null, selectNoForm.form)\n</script>\n\n<script id=\"selectedIndex_initial\">\n  {\n    testing.expectEqual(2, $('#select1').selectedIndex)\n    testing.expectEqual(0, $('#select2').selectedIndex)\n  }\n</script>\n\n<script id=\"selectedIndex_set\">\n  {\n    $('#select1').selectedIndex = 2\n    testing.expectEqual(2, $('#select1').selectedIndex)\n    testing.expectEqual('val3', $('#select1').value)\n\n    const opt3 = $('#select1').querySelector('option[value=\"val3\"]')\n    testing.expectEqual(true, opt3.selected)\n\n    const opt2 = $('#select1').querySelector('option[value=\"val2\"]')\n    testing.expectEqual(false, opt2.selected)\n  }\n</script>\n\n<script id=\"selectedIndex_set_negative\">\n  {\n    $('#select1').selectedIndex = -1\n    testing.expectEqual(-1, $('#select1').selectedIndex)\n\n    const opt1 = $('#select1').querySelector('option[value=\"val1\"]')\n    const opt2 = $('#select1').querySelector('option[value=\"val2\"]')\n    const opt3 = $('#select1').querySelector('option[value=\"val3\"]')\n    testing.expectEqual(false, opt1.selected)\n    testing.expectEqual(false, opt2.selected)\n    testing.expectEqual(false, opt3.selected)\n  }\n</script>\n\n<script id=\"selectedIndex_set_out_of_bounds\">\n  {\n    $('#select2').selectedIndex = 999\n    const optA = $('#select2').querySelector('option[value=\"a\"]')\n    const optB = $('#select2').querySelector('option[value=\"b\"]')\n    testing.expectEqual(false, optA.selected)\n    testing.expectEqual(false, optB.selected)\n  }\n</script>\n\n<select id=\"select_multi\" multiple>\n  <option value=\"opt1\">Option 1</option>\n  <option value=\"opt2\" selected>Option 2</option>\n  <option value=\"opt3\">Option 3</option>\n</select>\n\n<script id=\"multiple_attribute\">\n  {\n    const sel = $('#select_multi')\n    testing.expectEqual(true, sel.multiple)\n    testing.expectEqual('opt2', sel.value)\n    testing.expectEqual(1, sel.selectedIndex)\n  }\n</script>\n\n<script id=\"multiple_setValue\">\n  {\n    const sel = $('#select_multi')\n    sel.value = 'opt1'\n\n    const opt1 = sel.querySelector('option[value=\"opt1\"]')\n    const opt2 = sel.querySelector('option[value=\"opt2\"]')\n    testing.expectEqual(true, opt1.selected)\n    testing.expectEqual(false, opt2.selected)\n  }\n</script>\n\n<script id=\"options_collection\">\n  {\n    const sel = $('#select1')\n    const opts = sel.options\n    testing.expectEqual(3, sel.length)\n    testing.expectEqual(3, opts.length)\n    testing.expectEqual('HTMLOptionsCollection', opts.constructor.name)\n\n    // Test indexed access\n    testing.expectEqual('val1', opts[0].value)\n    testing.expectEqual('val2', opts[1].value)\n    testing.expectEqual('val3', opts[2].value)\n    testing.expectEqual('val1', opts.item(0).value);\n    testing.expectEqual('val2', opts.item(1).value);\n    testing.expectEqual('val3', opts.item(2).value);\n  }\n</script>\n\n<script id=\"options_selectedIndex\">\n  {\n    // Create a fresh select to avoid state from previous tests\n    const sel = document.createElement('select')\n    sel.innerHTML = '<option value=\"a\">A</option><option value=\"b\" selected>B</option><option value=\"c\">C</option>'\n    const opts = sel.options\n\n    // selectedIndex should forward to the select element\n    testing.expectEqual(1, sel.selectedIndex)\n    testing.expectEqual(1, opts.selectedIndex)\n\n    // Setting via options collection should update the select\n    opts.selectedIndex = 2\n    testing.expectEqual(2, sel.selectedIndex)\n    testing.expectEqual(2, opts.selectedIndex)\n  }\n</script>\n\n<script id=\"options_add_remove\">\n  {\n    const sel = document.createElement('select')\n    const opts = sel.options\n\n    testing.expectEqual(0, opts.length)\n\n    // Add an option\n    const opt1 = document.createElement('option')\n    opt1.value = 'a'\n    opt1.textContent = 'Option A'\n    opts.add(opt1, null)\n    testing.expectEqual(1, opts.length)\n    testing.expectEqual('a', opts[0].value)\n\n    // Add another option\n    const opt2 = document.createElement('option')\n    opt2.value = 'b'\n    opt2.textContent = 'Option B'\n    opts.add(opt2, null)\n    testing.expectEqual(2, opts.length)\n    testing.expectEqual('b', opts[1].value)\n\n    // Add an option before the first one\n    const opt0 = document.createElement('option')\n    opt0.value = 'zero'\n    opt0.textContent = 'Option Zero'\n    opts.add(opt0, opt1)\n    testing.expectEqual(3, opts.length)\n    testing.expectEqual('zero', opts[0].value)\n    testing.expectEqual('a', opts[1].value)\n    testing.expectEqual('b', opts[2].value)\n\n    // Remove the middle option (index 1, which is 'a')\n    opts.remove(1)\n    testing.expectEqual(2, opts.length)\n    testing.expectEqual('zero', opts[0].value)\n    testing.expectEqual('b', opts[1].value)\n\n    opts.add(opt1, 0)\n    testing.expectEqual(3, opts.length)\n    testing.expectEqual('a', opts[0].value)\n    testing.expectEqual('zero', opts[1].value)\n    testing.expectEqual('b', opts[2].value)\n  }\n</script>\n\n<script id=\"selectedOptions\">\n  {\n    const sel = document.createElement('select')\n    sel.multiple = true\n    sel.innerHTML = '<option value=\"a\">A</option><option value=\"b\" selected>B</option><option value=\"c\" selected>C</option><option value=\"d\">D</option>'\n\n    const selectedOpts = sel.selectedOptions\n    testing.expectEqual('HTMLCollection', selectedOpts.constructor.name)\n    testing.expectEqual(2, selectedOpts.length)\n    testing.expectEqual('b', selectedOpts[0].value)\n    testing.expectEqual('c', selectedOpts[1].value)\n\n    // Deselect one\n    sel.options[1].selected = false\n    testing.expectEqual(1, selectedOpts.length)\n    testing.expectEqual('c', selectedOpts[0].value)\n\n    // Select another\n    sel.options[3].selected = true\n    testing.expectEqual(2, selectedOpts.length)\n    testing.expectEqual('c', selectedOpts[0].value)\n    testing.expectEqual('d', selectedOpts[1].value)\n  }\n</script>\n\n<select id=\"named1\" name=\"country\"></select>\n<select id=\"named2\"></select>\n\n<select id=\"required1\" required></select>\n<select id=\"required2\"></select>\n\n<script id=\"name_initial\">\n  testing.expectEqual('country', $('#named1').name)\n  testing.expectEqual('', $('#named2').name)\n</script>\n\n<script id=\"name_set\">\n  {\n    const select = document.createElement('select')\n    testing.expectEqual('', select.name)\n\n    select.name = 'choices'\n    testing.expectEqual('choices', select.name)\n    testing.expectEqual('choices', select.getAttribute('name'))\n\n    select.name = 'options'\n    testing.expectEqual('options', select.name)\n    testing.expectEqual('options', select.getAttribute('name'))\n  }\n</script>\n\n<script id=\"name_reflects_to_attribute\">\n  {\n    const select = document.createElement('select')\n    testing.expectEqual(null, select.getAttribute('name'))\n\n    select.name = 'fieldname'\n    testing.expectEqual('fieldname', select.getAttribute('name'))\n    testing.expectTrue(select.outerHTML.includes('name=\"fieldname\"'))\n  }\n</script>\n\n<script id=\"required_initial\">\n  testing.expectEqual(true, $('#required1').required)\n  testing.expectEqual(false, $('#required2').required)\n</script>\n\n<script id=\"required_set\">\n  {\n    const select = document.createElement('select')\n    testing.expectEqual(false, select.required)\n\n    select.required = true\n    testing.expectEqual(true, select.required)\n    testing.expectEqual('', select.getAttribute('required'))\n\n    select.required = false\n    testing.expectEqual(false, select.required)\n    testing.expectEqual(null, select.getAttribute('required'))\n  }\n</script>\n\n<script id=\"required_reflects_to_attribute\">\n  {\n    const select = document.createElement('select')\n    testing.expectEqual(null, select.getAttribute('required'))\n    testing.expectFalse(select.outerHTML.includes('required'))\n\n    select.required = true\n    testing.expectEqual('', select.getAttribute('required'))\n    testing.expectTrue(select.outerHTML.includes('required'))\n\n    select.required = false\n    testing.expectEqual(null, select.getAttribute('required'))\n    testing.expectFalse(select.outerHTML.includes('required'))\n  }\n</script>\n\n<select id=\"sized1\" size=\"5\"></select>\n<select id=\"sized2\" size=\"   932 asd\"></select>\n<select id=\"sized3\" size=\"93abc\"></select>\n<select id=\"sized4\" size=\"nope\"></select>\n<select id=\"sized5\"></select>\n\n<script id=\"size_initial\">\n  testing.expectEqual(5, $('#sized1').size)\n  testing.expectEqual(932, $('#sized2').size)\n  testing.expectEqual(93, $('#sized3').size)\n  testing.expectEqual(0, $('#sized4').size)\n  testing.expectEqual(0, $('#sized5').size)\n</script>\n\n<script id=\"size_set\">\n  {\n    const select = document.createElement('select')\n    testing.expectEqual(0, select.size)\n\n    select.size = 10\n    testing.expectEqual(10, select.size)\n    testing.expectEqual('10', select.getAttribute('size'))\n\n    select.size = 42\n    testing.expectEqual(42, select.size)\n    testing.expectEqual('42', select.getAttribute('size'))\n  }\n</script>\n\n<script id=\"size_reflects_to_attribute\">\n  {\n    const select = document.createElement('select')\n    testing.expectEqual(null, select.getAttribute('size'))\n\n    select.size = 7\n    testing.expectEqual('7', select.getAttribute('size'))\n    testing.expectTrue(select.outerHTML.includes('size=\"7\"'))\n  }\n</script>\n\n<select id=\"no_value\">\n  <option>d1\n  <option>d2\n</select>\n<script id=\"no_value_attribute\">\n  {\n    const select = $('#no_value');\n    testing.expectEqual(2, select.length)\n    testing.expectEqual('d1', select.options[0].value)\n    testing.expectEqual('d2', select.options[1].value)\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/slot.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=\"Slot#basic_creation\">\n{\n    const slot = document.createElement('slot');\n    testing.expectEqual('SLOT', slot.tagName);\n}\n</script>\n\n<script id=\"Slot#name_attribute\">\n{\n    const slot = document.createElement('slot');\n\n    // Set name via setAttribute\n    slot.setAttribute('name', 'header');\n    testing.expectEqual('header', slot.getAttribute('name'));\n\n    // Change name\n    slot.setAttribute('name', 'footer');\n    testing.expectEqual('footer', slot.getAttribute('name'));\n}\n</script>\n\n<script id=\"Slot#assignedNodes_empty\">\n{\n    // Slot not in shadow tree\n    const slot = document.createElement('slot');\n    const nodes = slot.assignedNodes();\n    testing.expectEqual(0, nodes.length);\n}\n</script>\n\n<script id=\"Slot#assignedElements_empty\">\n{\n    // Slot not in shadow tree\n    const slot = document.createElement('slot');\n    const elements = slot.assignedElements();\n    testing.expectEqual(0, elements.length);\n}\n</script>\n\n<script id=\"Slot#default_slot_basic\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    // Create default slot (no name)\n    const slot = document.createElement('slot');\n    shadow.appendChild(slot);\n\n    // Add content to host\n    const span1 = document.createElement('span');\n    span1.textContent = 'Content 1';\n    host.appendChild(span1);\n\n    const span2 = document.createElement('span');\n    span2.textContent = 'Content 2';\n    host.appendChild(span2);\n\n    // Both spans should be assigned to default slot\n    const nodes = slot.assignedNodes();\n    testing.expectEqual(2, nodes.length);\n    testing.expectTrue(nodes[0] === span1);\n    testing.expectTrue(nodes[1] === span2);\n}\n</script>\n\n<script id=\"Slot#named_slot\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    // Create named slot\n    const headerSlot = document.createElement('slot');\n    headerSlot.name = 'header';\n    shadow.appendChild(headerSlot);\n\n    // Add content with slot attribute\n    const h1 = document.createElement('h1');\n    h1.textContent = 'Title';\n    h1.setAttribute('slot', 'header');\n    host.appendChild(h1);\n\n    const p = document.createElement('p');\n    p.textContent = 'Body';\n    host.appendChild(p);\n\n    // Only h1 should be assigned to header slot\n    const nodes = headerSlot.assignedNodes();\n    testing.expectEqual(1, nodes.length);\n    testing.expectTrue(nodes[0] === h1);\n}\n</script>\n\n<script id=\"Slot#multiple_slots\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    // Create multiple named slots\n    const headerSlot = document.createElement('slot');\n    headerSlot.name = 'header';\n    shadow.appendChild(headerSlot);\n\n    const footerSlot = document.createElement('slot');\n    footerSlot.name = 'footer';\n    shadow.appendChild(footerSlot);\n\n    const defaultSlot = document.createElement('slot');\n    shadow.appendChild(defaultSlot);\n\n    // Add content\n    const h1 = document.createElement('h1');\n    h1.setAttribute('slot', 'header');\n    host.appendChild(h1);\n\n    const p = document.createElement('p');\n    host.appendChild(p);\n\n    const footer = document.createElement('footer');\n    footer.setAttribute('slot', 'footer');\n    host.appendChild(footer);\n\n    // Check each slot\n    const headerNodes = headerSlot.assignedNodes();\n    testing.expectEqual(1, headerNodes.length);\n    testing.expectTrue(headerNodes[0] === h1);\n\n    const footerNodes = footerSlot.assignedNodes();\n    testing.expectEqual(1, footerNodes.length);\n    testing.expectTrue(footerNodes[0] === footer);\n\n    const defaultNodes = defaultSlot.assignedNodes();\n    testing.expectEqual(1, defaultNodes.length);\n    testing.expectTrue(defaultNodes[0] === p);\n}\n</script>\n\n<script id=\"Slot#assignedElements_filters_text_nodes\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    shadow.appendChild(slot);\n\n    // Add mixed content\n    const span = document.createElement('span');\n    host.appendChild(span);\n\n    const text = document.createTextNode('Some text');\n    host.appendChild(text);\n\n    const div = document.createElement('div');\n    host.appendChild(div);\n\n    // assignedNodes should include all\n    const nodes = slot.assignedNodes();\n    testing.expectEqual(3, nodes.length);\n\n    // assignedElements should only include elements\n    const elements = slot.assignedElements();\n    testing.expectEqual(2, elements.length);\n    testing.expectTrue(elements[0] === span);\n    testing.expectTrue(elements[1] === div);\n}\n</script>\n\n<script id=\"Slot#text_nodes_default_slot_only\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const namedSlot = document.createElement('slot');\n    namedSlot.name = 'named';\n    shadow.appendChild(namedSlot);\n\n    const defaultSlot = document.createElement('slot');\n    shadow.appendChild(defaultSlot);\n\n    // Add text node\n    const text = document.createTextNode('Text content');\n    host.appendChild(text);\n\n    // Text should go to default slot only\n    const namedNodes = namedSlot.assignedNodes();\n    testing.expectEqual(0, namedNodes.length);\n\n    const defaultNodes = defaultSlot.assignedNodes();\n    testing.expectEqual(1, defaultNodes.length);\n    testing.expectTrue(defaultNodes[0] === text);\n}\n</script>\n\n<script id=\"Slot#flatten_false\">\n{\n    const outerHost = document.createElement('div');\n    const outerShadow = outerHost.attachShadow({ mode: 'open' });\n\n    const innerHost = document.createElement('div');\n    const innerShadow = innerHost.attachShadow({ mode: 'open' });\n\n    // Inner slot\n    const innerSlot = document.createElement('slot');\n    innerShadow.appendChild(innerSlot);\n\n    // Outer slot contains inner host\n    const outerSlot = document.createElement('slot');\n    outerShadow.appendChild(outerSlot);\n    outerHost.appendChild(innerHost);\n\n    // Add content to inner host\n    const span = document.createElement('span');\n    innerHost.appendChild(span);\n\n    // Without flatten, outer slot should see inner host (not span)\n    const nodes = outerSlot.assignedNodes();\n    testing.expectEqual(1, nodes.length);\n    testing.expectTrue(nodes[0] === innerHost);\n}\n</script>\n\n<script id=\"Slot#flatten_true\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    shadow.appendChild(slot);\n\n    // Add an element with a nested slot in it\n    const container = document.createElement('div');\n    container.innerHTML = '<slot name=\"nested\"></slot>';\n    host.appendChild(container);\n\n    // Add a regular span\n    const span = document.createElement('span');\n    host.appendChild(span);\n\n    // Without flatten: should see container and span (2 elements)\n    const nodesNoFlat = slot.assignedNodes({ flatten: false });\n    testing.expectEqual(2, nodesNoFlat.length);\n\n    // With flatten: should still see container and span\n    // (flatten only matters if the assigned node is itself a slot in a shadow tree)\n    const nodesFlat = slot.assignedNodes({ flatten: true });\n    testing.expectEqual(2, nodesFlat.length);\n}\n</script>\n\n<script id=\"Slot#assignedElements_with_flatten\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    shadow.appendChild(slot);\n\n    // Add mixed content\n    const div = document.createElement('div');\n    host.appendChild(div);\n\n    const text = document.createTextNode('text');\n    host.appendChild(text);\n\n    const span = document.createElement('span');\n    host.appendChild(span);\n\n    // assignedElements with flatten should only return elements\n    const elements = slot.assignedElements({ flatten: true });\n    testing.expectEqual(2, elements.length);\n    testing.expectTrue(elements[0] === div);\n    testing.expectTrue(elements[1] === span);\n}\n</script>\n\n<script id=\"Slot#empty_slot_name_matches_no_slot_attribute\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const defaultSlot = document.createElement('slot');\n    shadow.appendChild(defaultSlot);\n\n    // Element without slot attribute\n    const div = document.createElement('div');\n    host.appendChild(div);\n\n    // Element with empty slot attribute\n    const span = document.createElement('span');\n    span.setAttribute('slot', '');\n    host.appendChild(span);\n\n    // Both should go to default slot\n    const nodes = defaultSlot.assignedNodes();\n    testing.expectEqual(2, nodes.length);\n    testing.expectTrue(nodes[0] === div);\n    testing.expectTrue(nodes[1] === span);\n}\n</script>\n\n<script id=\"Slot#slot_attribute_case_sensitive\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    slot.name = 'MySlot';\n    shadow.appendChild(slot);\n\n    // Matching case\n    const div1 = document.createElement('div');\n    div1.setAttribute('slot', 'MySlot');\n    host.appendChild(div1);\n\n    // Different case\n    const div2 = document.createElement('div');\n    div2.setAttribute('slot', 'myslot');\n    host.appendChild(div2);\n\n    // Only exact match should be assigned\n    const nodes = slot.assignedNodes();\n    testing.expectEqual(1, nodes.length);\n    testing.expectTrue(nodes[0] === div1);\n}\n</script>\n\n<script id=\"Slot#slot_in_slot_shadow_tree\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    // Create a container in shadow tree\n    const container = document.createElement('div');\n    shadow.appendChild(container);\n\n    // Slot inside container\n    const slot = document.createElement('slot');\n    container.appendChild(slot);\n\n    // Add content to host\n    const span = document.createElement('span');\n    host.appendChild(span);\n\n    // Slot should still work even when not direct child of shadow root\n    const nodes = slot.assignedNodes();\n    testing.expectEqual(1, nodes.length);\n    testing.expectTrue(nodes[0] === span);\n}\n</script>\n\n<script id=\"Slot#flatten_with_slot_and_siblings\">\n{\n    // Test that flatten continues processing siblings after a nested slot\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const outerSlot = document.createElement('slot');\n    shadow.appendChild(outerSlot);\n\n    // Add a nested slot as first child\n    const innerSlot = document.createElement('slot');\n    innerSlot.setAttribute('name', 'inner');\n    host.appendChild(innerSlot);\n\n    // Add a regular element after the slot\n    const div = document.createElement('div');\n    div.textContent = 'After slot';\n    host.appendChild(div);\n\n    // Add another element\n    const span = document.createElement('span');\n    span.textContent = 'Another element';\n    host.appendChild(span);\n\n    // With flatten=true, should get all elements including those after the nested slot\n    const flattened = outerSlot.assignedElements({ flatten: true });\n    testing.expectEqual(3, flattened.length);\n    testing.expectTrue(flattened[0] === innerSlot);\n    testing.expectTrue(flattened[1] === div);\n    testing.expectTrue(flattened[2] === span);\n}\n</script>\n\n<script id=\"Slot#assignedSlot_returns_null_when_not_assigned\">\n{\n    const div = document.createElement('div');\n    testing.expectEqual(null, div.assignedSlot);\n}\n</script>\n\n<script id=\"Slot#assignedSlot_property\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    slot.name = 'header';\n    shadow.appendChild(slot);\n\n    const h1 = document.createElement('h1');\n    h1.setAttribute('slot', 'header');\n    host.appendChild(h1);\n\n    testing.expectEqual(slot, h1.assignedSlot);\n}\n</script>\n\n<script id=\"Slot#slotchange_on_insertion\">\n{\n    let calls = 0;\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    slot.name = 'content';\n    shadow.appendChild(slot);\n\n    slot.addEventListener('slotchange', (e) => {\n        const nodes = slot.assignedNodes();\n        testing.expectEqual(1, nodes.length);\n        calls += 1;\n    });\n\n    const div = document.createElement('div');\n    div.setAttribute('slot', 'content');\n    host.appendChild(div);\n\n    testing.eventually(() => {\n        testing.expectEqual(1, calls);\n    });\n}\n</script>\n\n<script id=\"Slot#slotchange_on_attribute_change\">\n{\n    let calls = 0;\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    slot.name = 'content';\n    shadow.appendChild(slot);\n\n    const div = document.createElement('div');\n    div.setAttribute('slot', 'content');\n    host.appendChild(div);\n\n    slot.addEventListener('slotchange', (e) => {\n        const nodes = slot.assignedNodes();\n        testing.expectEqual(0, nodes.length);\n        calls += 1;\n    });\n\n    div.setAttribute('slot', 'other');\n\n    testing.eventually(() => {\n        testing.expectEqual(1, calls);\n    });\n}\n</script>\n\n<script id=\"Slot#slotchange_on_removal\">\n{\n    let calls = 0;\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    slot.name = 'content';\n    shadow.appendChild(slot);\n\n    const div = document.createElement('div');\n    div.setAttribute('slot', 'content');\n    host.appendChild(div);\n\n    slot.addEventListener('slotchange', (e) => {\n        const nodes = slot.assignedNodes();\n        testing.expectEqual(0, nodes.length);\n        calls += 1;\n    });\n\n    div.remove();\n\n    testing.eventually(() => {\n        testing.expectEqual(1, calls);\n    });\n}\n</script>\n\n<script id=\"Slot#slotchange_with_slot_property\">\n{\n    let calls = 0;\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const slot = document.createElement('slot');\n    slot.name = 'content';\n    shadow.appendChild(slot);\n\n    const div = document.createElement('div');\n    div.slot = 'content';\n    host.appendChild(div);\n\n    slot.addEventListener('slotchange', (e) => {\n        const nodes = slot.assignedNodes();\n        testing.expectEqual(0, nodes.length);\n        calls += 1;\n    });\n\n    div.slot = 'other';\n\n    testing.eventually(() => {\n        testing.expectEqual(1, calls);\n    });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/style.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=\"sheet\">\n  {\n    // Disconnected style element should have no sheet\n    testing.expectEqual(null, document.createElement('style').sheet);\n\n    // Connected style element should have a CSSStyleSheet\n    const style = document.createElement('style');\n    document.head.appendChild(style);\n    testing.expectEqual(true, style.sheet instanceof CSSStyleSheet);\n\n    // Same sheet instance on repeated access\n    testing.expectEqual(true, style.sheet === style.sheet);\n\n    // Non-CSS type should have no sheet\n    const lessStyle = document.createElement('style');\n    lessStyle.type = 'text/less';\n    document.head.appendChild(lessStyle);\n    testing.expectEqual(null, lessStyle.sheet);\n\n    // Empty type attribute is valid (defaults to text/css per spec)\n    const emptyType = document.createElement('style');\n    emptyType.setAttribute('type', '');\n    document.head.appendChild(emptyType);\n    testing.expectEqual(true, emptyType.sheet instanceof CSSStyleSheet);\n\n    // Case-insensitive type check\n    const upperType = document.createElement('style');\n    upperType.type = 'TEXT/CSS';\n    document.head.appendChild(upperType);\n    testing.expectEqual(true, upperType.sheet instanceof CSSStyleSheet);\n\n    // Disconnection clears sheet\n    const tempStyle = document.createElement('style');\n    document.head.appendChild(tempStyle);\n    testing.expectEqual(true, tempStyle.sheet instanceof CSSStyleSheet);\n    document.head.removeChild(tempStyle);\n    testing.expectEqual(null, tempStyle.sheet);\n\n    // ownerNode points back to the style element\n    const ownStyle = document.createElement('style');\n    document.head.appendChild(ownStyle);\n    testing.expectEqual(true, ownStyle.sheet.ownerNode === ownStyle);\n  }\n</script>\n\n<script id=\"type\">\n  {\n    const style = document.createElement('style');\n    testing.expectEqual('text/css', style.type);\n\n    style.type = 'text/plain';\n    testing.expectEqual('text/plain', style.type);\n  }\n</script>\n\n<script id=\"media\">\n  {\n    const style = document.createElement('style');\n    testing.expectEqual('', style.media);\n\n    style.media = 'screen';\n    testing.expectEqual('screen', style.media);\n\n    style.media = 'print and (max-width: 600px)';\n    testing.expectEqual('print and (max-width: 600px)', style.media);\n  }\n</script>\n\n<script id=\"blocking\">\n  {\n    const style = document.createElement('style');\n    testing.expectEqual('', style.blocking);\n\n    style.blocking = 'render';\n    testing.expectEqual('render', style.blocking);\n  }\n</script>\n\n<script id=\"disabled\">\n  {\n    const style = document.createElement('style');\n    testing.expectEqual(false, style.disabled);\n\n    style.disabled = true;\n    testing.expectEqual(true, style.disabled);\n\n    style.disabled = false;\n    testing.expectEqual(false, style.disabled);\n  }\n</script>\n\n<script id=\"attributes\">\n  {\n    const style = document.createElement('style');\n\n    style.setAttribute('media', 'screen');\n    testing.expectEqual('screen', style.media);\n\n    style.setAttribute('type', 'text/less');\n    testing.expectEqual('text/less', style.type);\n\n    style.setAttribute('disabled', '');\n    testing.expectEqual(true, style.disabled);\n  }\n</script>\n\n<script id=\"style-load-event\">\n{\n  // A style element fires a load event when appended to the DOM.\n  const style = document.createElement(\"style\");\n  let result = false;\n  testing.async(async () => {\n    await new Promise(resolve => {\n      style.addEventListener(\"load\", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {\n        testing.expectEqual(false, bubbles);\n        testing.expectEqual(false, cancelBubble);\n        testing.expectEqual(false, cancelable);\n        testing.expectEqual(false, composed);\n        testing.expectEqual(true, isTrusted);\n        testing.expectEqual(style, target);\n        result = true;\n        return resolve();\n      });\n      document.head.appendChild(style);\n    });\n  });\n\n  testing.eventually(() => testing.expectEqual(true, result));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/tablecell.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<table>\n  <tr>\n    <td id=\"td1\" colspan=\"3\" rowspan=\"2\">Cell</td>\n    <td id=\"td2\">Cell</td>\n  </tr>\n</table>\n\n<script id=\"colSpan\">\n  {\n    const td1 = document.getElementById('td1');\n    testing.expectEqual(3, td1.colSpan);\n\n    td1.colSpan = 5;\n    testing.expectEqual(5, td1.colSpan);\n\n    const td2 = document.getElementById('td2');\n    testing.expectEqual(1, td2.colSpan);\n\n    // colSpan 0 clamps to 1\n    td2.colSpan = 0;\n    testing.expectEqual(1, td2.colSpan);\n\n    // colSpan > 1000 clamps to 1000\n    td2.colSpan = 9999;\n    testing.expectEqual(1000, td2.colSpan);\n  }\n</script>\n\n<script id=\"rowSpan\">\n  {\n    const td1 = document.getElementById('td1');\n    testing.expectEqual(2, td1.rowSpan);\n\n    td1.rowSpan = 4;\n    testing.expectEqual(4, td1.rowSpan);\n\n    const td2 = document.getElementById('td2');\n    testing.expectEqual(1, td2.rowSpan);\n\n    // rowSpan 0 is valid per spec (span remaining rows)\n    td2.rowSpan = 0;\n    testing.expectEqual(0, td2.rowSpan);\n\n    // rowSpan > 65534 clamps to 65534\n    td2.rowSpan = 99999;\n    testing.expectEqual(65534, td2.rowSpan);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/template.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n<body>\n<template id=\"basic\">\n  <div class=\"container\">\n    <h1>Hello Template</h1>\n    <p>This is template content</p>\n  </div>\n</template>\n\n<template id=\"nested\">\n  <div class=\"outer\">\n    <span id=\"inner1\">First</span>\n    <span id=\"inner2\">Second</span>\n  </div>\n</template>\n\n<template id=\"empty\"></template>\n\n<script id=ids>\n  testing.expectEqual(null, document.querySelector('#inner1'));\n  testing.expectEqual(null, document.getElementById('inner1'));\n  testing.expectEqual('First', document.getElementById('nested').content.getElementById('inner1').textContent);\n  testing.expectEqual('First', document.getElementById('nested').content.querySelector('#inner1').textContent);\n</script>\n\n<script id=content_property>\n{\n  const template = $('#basic');\n  const content = template.content;\n\n  testing.expectEqual('[object DocumentFragment]', content.toString());\n  testing.expectEqual(true, content instanceof DocumentFragment);\n}\n</script>\n\n<script id=content_has_children>\n{\n  const template = $('#basic');\n  const content = template.content;\n\n  const div = content.firstElementChild;\n  testing.expectTrue(div !== null);\n  testing.expectEqual('DIV', div.tagName);\n  testing.expectEqual('container', div.className);\n\n  const h1 = div.querySelector('h1');\n  testing.expectTrue(h1 !== null);\n  testing.expectEqual('Hello Template', h1.textContent);\n\n  const p = div.querySelector('p');\n  testing.expectTrue(p !== null);\n  testing.expectEqual('This is template content', p.textContent);\n}\n</script>\n\n<script id=template_children_not_in_dom>\n{\n  const template = $('#basic');\n\n  testing.expectEqual(0, template.children.length);\n  testing.expectEqual(null, template.firstElementChild);\n\n  const h1InDoc = document.querySelector('#basic h1');\n  testing.expectEqual(null, h1InDoc);\n}\n</script>\n\n<script id=nested_template_content>\n{\n  const template = $('#nested');\n  const content = template.content;\n\n  const outer = content.firstElementChild;\n  testing.expectEqual('DIV', outer.tagName);\n  testing.expectEqual('outer', outer.className);\n\n  const inner1 = content.querySelector('#inner1');\n  testing.expectTrue(inner1 !== null);\n  testing.expectEqual('First', inner1.textContent);\n\n  const inner2 = content.querySelector('#inner2');\n  testing.expectTrue(inner2 !== null);\n  testing.expectEqual('Second', inner2.textContent);\n}\n</script>\n\n<script id=empty_template>\n{\n  const template = $('#empty');\n  const content = template.content;\n\n  testing.expectEqual('[object DocumentFragment]', content.toString());\n  testing.expectEqual(null, content.firstElementChild);\n  testing.expectEqual(0, content.childElementCount);\n}\n</script>\n\n<script id=query_selector_on_content>\n{\n  const template = $('#nested');\n  const content = template.content;\n\n  const spans = content.querySelectorAll('span');\n  testing.expectEqual(2, spans.length);\n  testing.expectEqual('inner1', spans[0].id);\n  testing.expectEqual('inner2', spans[1].id);\n}\n</script>\n\n<script id=modify_content>\n{\n  const template = $('#basic');\n  const content = template.content;\n\n  testing.expectEqual(1, content.childElementCount);\n\n  const newDiv = document.createElement('div');\n  newDiv.id = 'added';\n  newDiv.textContent = 'Added element';\n  content.append(newDiv);\n\n  testing.expectEqual(2, content.childElementCount);\n}\n</script>\n\n<script id=clone_template_content>\n{\n  const template = $('#basic');\n  const content = template.content;\n\n  const clone = content.cloneNode(true);\n  testing.expectEqual('[object DocumentFragment]', clone.toString());\n\n  const div = clone.firstElementChild;\n  testing.expectTrue(div !== null);\n  testing.expectEqual('container', div.className);\n\n  const h1 = clone.querySelector('h1');\n  testing.expectEqual('Hello Template', h1.textContent);\n}\n</script>\n\n<script id=instantiate_template>\n{\n  const template = $('#nested');\n  const content = template.content;\n\n  const originalOuter = content.firstElementChild;\n  testing.expectEqual(2, originalOuter.childElementCount);\n\n  const clone = content.cloneNode(true);\n\n  testing.expectEqual(1, clone.childElementCount);\n  const clonedDiv = clone.firstElementChild;\n  testing.expectEqual('DIV', clonedDiv.tagName);\n  testing.expectEqual('outer', clonedDiv.className);\n  testing.expectEqual(2, clonedDiv.childElementCount);\n\n  document.body.appendChild(clone);\n\n  testing.expectEqual(0, clone.childElementCount);\n\n  const outerInDoc = document.body.querySelector('.outer');\n  testing.expectTrue(outerInDoc !== null);\n  testing.expectEqual(clonedDiv, outerInDoc);\n\n  testing.expectEqual(2, outerInDoc.childElementCount);\n\n  const inner1 = outerInDoc.firstElementChild;\n  testing.expectEqual('SPAN', inner1.tagName);\n  testing.expectEqual('inner1', inner1.id);\n  testing.expectEqual('First', inner1.textContent);\n}\n</script>\n\n<script id=dynamic_content>\n{\n  let template = document.createElement('template');\n  template.innerHTML = '<p>1</p><span>2</span>';\n  testing.expectEqual(2, template.content.children.length);\n}\n</script>\n\n<script id=outerHTML_includes_content>\n{\n  const template = $('#basic');\n  const outer = template.outerHTML;\n\n  testing.expectTrue(outer.includes('<template'));\n  testing.expectTrue(outer.includes('</template>'));\n  testing.expectTrue(outer.includes('<div class=\"container\">'));\n  testing.expectTrue(outer.includes('Hello Template'));\n}\n</script>\n\n<script id=outerHTML_with_attributes>\n{\n  let template = document.createElement('template');\n  template.id = 'test-template';\n  template.innerHTML = '<p>Content</p>';\n\n  const outer = template.outerHTML;\n  testing.expectEqual('<template id=\"test-template\"><p>Content</p></template>', outer);\n}\n</script>\n\n<script id=textContent_empty>\n{\n  const template = $('#basic');\n  // textContent on template operates on the template element itself,\n  // NOT the DocumentFragment, so it should be empty\n  testing.expectEqual('', template.textContent);\n}\n</script>\n\n<template id=\"hello\"><p>hello, world</p></template>\n<script id=template_parsing>\n  const tt = document.getElementById('hello');\n  testing.expectEqual('<p>hello, world</p>', tt.innerHTML);\n\n  // > The Node.childNodes property of the <template> element is always empty\n  // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes\n  testing.expectEqual(0, tt.childNodes.length);\n\n  let out = document.createElement('div');\n  out.appendChild(tt.content.cloneNode(true));\n\n  testing.expectEqual('<p>hello, world</p>', out.innerHTML);\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/textarea.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<!-- TextArea elements -->\n<textarea id=\"textarea1\">initial text</textarea>\n<textarea id=\"textarea2\"></textarea>\n<textarea id=\"textarea3\" disabled>disabled text</textarea>\n\n<!-- Form association tests -->\n<form id=\"form1\">\n  <textarea id=\"textarea_in_form\">in form</textarea>\n</form>\n\n<form id=\"form2\"></form>\n<textarea id=\"textarea_with_form_attr\" form=\"form2\">with form attr</textarea>\n\n<textarea id=\"textarea_no_form\">no form</textarea>\n\n<form id=\"form3\">\n  <textarea id=\"textarea_invalid_form_attr\" form=\"nonexistent\">invalid form</textarea>\n</form>\n\n<script id=\"value_initial\">\n  testing.expectEqual('initial text', $('#textarea1').value)\n  testing.expectEqual('', $('#textarea2').value)\n</script>\n\n<script id=\"value_set\">\n  $('#textarea1').value = 'changed'\n  testing.expectEqual('changed', $('#textarea1').value)\n\n  $('#textarea2').value = 'new value'\n  testing.expectEqual('new value', $('#textarea2').value)\n</script>\n\n<script id=\"defaultValue\">\n  testing.expectEqual('initial text', $('#textarea1').defaultValue)\n  testing.expectEqual('', $('#textarea2').defaultValue)\n\n  // Setting value shouldn't change defaultValue\n  $('#textarea1').value = 'changed'\n  testing.expectEqual('initial text', $('#textarea1').defaultValue)\n</script>\n\n<script id=\"defaultValue_set\">\n  {\n    const textarea = document.createElement('textarea')\n    testing.expectEqual('', textarea.defaultValue)\n    testing.expectEqual('', textarea.value)\n\n    // Setting defaultValue should update the text content\n    textarea.defaultValue = 'new default'\n    testing.expectEqual('new default', textarea.defaultValue)\n    testing.expectEqual('new default', textarea.value)\n    testing.expectEqual('new default', textarea.textContent)\n\n    // Setting value should not affect defaultValue\n    textarea.value = 'user input'\n    testing.expectEqual('new default', textarea.defaultValue)\n    testing.expectEqual('user input', textarea.value)\n\n    // Test setting defaultValue on element that already has content\n    const textarea2 = document.createElement('textarea')\n    textarea2.textContent = 'initial content'\n    testing.expectEqual('initial content', textarea2.defaultValue)\n\n    textarea2.defaultValue = 'modified default'\n    testing.expectEqual('modified default', textarea2.defaultValue)\n    testing.expectEqual('modified default', textarea2.textContent)\n  }\n</script>\n\n<script id=\"disabled_initial\">\n  testing.expectEqual(false, $('#textarea1').disabled)\n  testing.expectEqual(true, $('#textarea3').disabled)\n</script>\n\n<script id=\"disabled_set\">\n  $('#textarea1').disabled = true\n  testing.expectEqual(true, $('#textarea1').disabled)\n\n  $('#textarea3').disabled = false\n  testing.expectEqual(false, $('#textarea3').disabled)\n</script>\n\n<script id=\"form_ancestor\">\n  const textareaInForm = $('#textarea_in_form')\n  testing.expectEqual('FORM', textareaInForm.form.tagName)\n  testing.expectEqual('form1', textareaInForm.form.id)\n</script>\n\n<script id=\"form_attribute\">\n  const textareaWithFormAttr = $('#textarea_with_form_attr')\n  testing.expectEqual('FORM', textareaWithFormAttr.form.tagName)\n  testing.expectEqual('form2', textareaWithFormAttr.form.id)\n</script>\n\n<script id=\"form_null\">\n  const textareaNoForm = $('#textarea_no_form')\n  testing.expectEqual(null, textareaNoForm.form)\n</script>\n\n<script id=\"form_invalid_attribute\">\n  const textareaInvalidFormAttr = $('#textarea_invalid_form_attr')\n  testing.expectEqual(null, textareaInvalidFormAttr.form)\n</script>\n\n<textarea id=\"named1\" name=\"comments\"></textarea>\n<textarea id=\"named2\"></textarea>\n\n<textarea id=\"required1\" required></textarea>\n<textarea id=\"required2\"></textarea>\n\n<script id=\"name_initial\">\n  testing.expectEqual('comments', $('#named1').name)\n  testing.expectEqual('', $('#named2').name)\n</script>\n\n<script id=\"name_set\">\n  {\n    const textarea = document.createElement('textarea')\n    testing.expectEqual('', textarea.name)\n\n    textarea.name = 'message'\n    testing.expectEqual('message', textarea.name)\n    testing.expectEqual('message', textarea.getAttribute('name'))\n\n    textarea.name = 'feedback'\n    testing.expectEqual('feedback', textarea.name)\n    testing.expectEqual('feedback', textarea.getAttribute('name'))\n  }\n</script>\n\n<script id=\"name_reflects_to_attribute\">\n  {\n    const textarea = document.createElement('textarea')\n    testing.expectEqual(null, textarea.getAttribute('name'))\n\n    textarea.name = 'fieldname'\n    testing.expectEqual('fieldname', textarea.getAttribute('name'))\n    testing.expectTrue(textarea.outerHTML.includes('name=\"fieldname\"'))\n  }\n</script>\n\n<script id=\"required_initial\">\n  testing.expectEqual(true, $('#required1').required)\n  testing.expectEqual(false, $('#required2').required)\n</script>\n\n<script id=\"required_set\">\n  {\n    const textarea = document.createElement('textarea')\n    testing.expectEqual(false, textarea.required)\n\n    textarea.required = true\n    testing.expectEqual(true, textarea.required)\n    testing.expectEqual('', textarea.getAttribute('required'))\n\n    textarea.required = false\n    testing.expectEqual(false, textarea.required)\n    testing.expectEqual(null, textarea.getAttribute('required'))\n  }\n</script>\n\n<script id=\"required_reflects_to_attribute\">\n  {\n    const textarea = document.createElement('textarea')\n    testing.expectEqual(null, textarea.getAttribute('required'))\n    testing.expectFalse(textarea.outerHTML.includes('required'))\n\n    textarea.required = true\n    testing.expectEqual('', textarea.getAttribute('required'))\n    testing.expectTrue(textarea.outerHTML.includes('required'))\n\n    textarea.required = false\n    testing.expectEqual(null, textarea.getAttribute('required'))\n    testing.expectFalse(textarea.outerHTML.includes('required'))\n  }\n</script>\n\n<script id=\"clone_basic\">\n  {\n    const original = document.createElement('textarea')\n    original.defaultValue = 'default text'\n    testing.expectEqual('default text', original.value)\n\n    // Change the value\n    original.value = 'user modified'\n    testing.expectEqual('user modified', original.value)\n    testing.expectEqual('default text', original.defaultValue)\n\n    // Clone the textarea\n    const clone = original.cloneNode(true)\n\n    // Clone should have the runtime value copied\n    testing.expectEqual('user modified', clone.value)\n    testing.expectEqual('default text', clone.defaultValue)\n  }\n</script>\n\n<script id=\"clone_preserves_user_changes\">\n  {\n    // Create a fresh element to avoid interfering with other tests\n    const original = document.createElement('textarea')\n    original.textContent = 'initial text'\n    testing.expectEqual('initial text', original.defaultValue)\n    testing.expectEqual('initial text', original.value)\n\n    // User modifies the value\n    original.value = 'user typed this'\n    testing.expectEqual('user typed this', original.value)\n    testing.expectEqual('initial text', original.defaultValue)\n\n    // Clone should preserve the user's changes\n    const clone = original.cloneNode(true)\n    testing.expectEqual('user typed this', clone.value)\n    testing.expectEqual('initial text', clone.defaultValue)\n  }\n</script>\n\n<script id=\"clone_empty_textarea\">\n  {\n    const original = document.createElement('textarea')\n    testing.expectEqual('', original.value)\n\n    original.value = 'some content'\n    const clone = original.cloneNode(true)\n\n    testing.expectEqual('some content', clone.value)\n  }\n</script>\n\n<script id=\"select_event\">\n  {\n    const textarea = document.createElement('textarea');\n    textarea.value = 'Hello World';\n    document.body.appendChild(textarea);\n\n    let eventCount = 0;\n    let lastEvent = null;\n\n    textarea.addEventListener('select', (e) => {\n      eventCount++;\n      lastEvent = e;\n    });\n\n    let onselectFired = false;\n    textarea.onselect = () => { onselectFired = true; };\n\n    let bubbledToBody = false;\n    document.body.addEventListener('select', () => {\n      bubbledToBody = true;\n    });\n\n    testing.expectEqual(0, eventCount);\n\n    textarea.select();\n\n    testing.eventually(() => {\n      testing.expectEqual(1, eventCount);\n      testing.expectEqual('select', lastEvent.type);\n      testing.expectEqual(textarea, lastEvent.target);\n      testing.expectEqual(true, lastEvent.bubbles);\n      testing.expectEqual(false, lastEvent.cancelable);\n      testing.expectEqual(true, bubbledToBody);\n      testing.expectEqual(true, onselectFired);\n    });\n  }\n</script>\n\n<script id=\"selectionchange_event\">\n  {\n    const textarea = document.createElement('textarea');\n    textarea.value = 'Hello World';\n    document.body.appendChild(textarea);\n    \n    let eventCount = 0;\n    let lastEvent = null;\n    \n    textarea.addEventListener('selectionchange', (e) => {\n      eventCount++;\n      lastEvent = e;\n    });\n    \n    testing.expectEqual(0, eventCount);\n    \n    textarea.setSelectionRange(0, 5);\n    textarea.select();\n    textarea.selectionStart = 3;\n    textarea.selectionEnd = 8;\n    \n    let bubbledToBody = false;\n    document.body.addEventListener('selectionchange', () => {\n      bubbledToBody = true;\n    });\n    textarea.setSelectionRange(1, 4);\n    \n    testing.eventually(() => {\n      testing.expectEqual(5, eventCount);\n      testing.expectEqual('selectionchange', lastEvent.type);\n      testing.expectEqual(textarea, lastEvent.target);\n      testing.expectEqual(true, lastEvent.bubbles);\n      testing.expectEqual(false, lastEvent.cancelable);\n      testing.expectEqual(true, bubbledToBody);\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/time.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<time id=\"t1\" datetime=\"2024-01-15\">January 15</time>\n\n<script id=\"dateTime\">\n  {\n    const t = document.getElementById('t1');\n    testing.expectEqual('2024-01-15', t.dateTime);\n\n    t.dateTime = '2024-12-25T10:00';\n    testing.expectEqual('2024-12-25T10:00', t.dateTime);\n\n    const t2 = document.createElement('time');\n    testing.expectEqual('', t2.dateTime);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/html/track.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<video id=\"video1\">\n  <track id=\"track1\" kind=\"subtitles\">\n  <track id=\"track2\" kind=\"captions\">\n  <track id=\"track3\" kind=\"invalid-kind\">\n</video>\n\n<script id=\"instanceof\">\n{\n  const track = document.createElement(\"track\");\n  testing.expectEqual(true, track instanceof HTMLTrackElement);\n  testing.expectEqual(\"[object HTMLTrackElement]\", track.toString());\n}\n</script>\n\n<script id=\"kind_default\">\n{\n  const track = document.createElement(\"track\");\n  testing.expectEqual(\"subtitles\", track.kind);\n}\n</script>\n\n<script id=\"kind_valid_values\">\n{\n  const track = document.createElement(\"track\");\n\n  track.kind = \"captions\";\n  testing.expectEqual(\"captions\", track.kind);\n\n  track.kind = \"descriptions\";\n  testing.expectEqual(\"descriptions\", track.kind);\n\n  track.kind = \"chapters\";\n  testing.expectEqual(\"chapters\", track.kind);\n\n  track.kind = \"metadata\";\n  testing.expectEqual(\"metadata\", track.kind);\n}\n</script>\n\n<script id=\"kind_invalid\">\n{\n  const track = document.createElement(\"track\");\n\n  track.kind = null;\n  testing.expectEqual(\"metadata\", track.kind);\n\n  track.kind = \"Subtitles\";\n  testing.expectEqual(\"subtitles\", track.kind);\n\n  track.kind = \"\";\n  testing.expectEqual(\"metadata\", track.kind);\n}\n</script>\n\n<script id=\"constants\">\n{\n  const track = document.createElement(\"track\");\n  testing.expectEqual(0, track.NONE);\n  testing.expectEqual(1, track.LOADING);\n  testing.expectEqual(2, track.LOADED);\n  testing.expectEqual(3, track.ERROR);\n}\n</script>\n\n<script id=\"constants_static\">\n{\n  testing.expectEqual(0, HTMLTrackElement.NONE);\n  testing.expectEqual(1, HTMLTrackElement.LOADING);\n  testing.expectEqual(2, HTMLTrackElement.LOADED);\n  testing.expectEqual(3, HTMLTrackElement.ERROR);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/inner.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1>hello <em>world</em></div>\n<div id=d2>\n  <style>h1 { font-size: 1em; }</style>\n  <!-- this is a comment -->\n  This is a <br>\n  text\n</div>\n<p id=d3><span>Hello</span> <span>World</span></p>\n<p id=d4><span>Hello</span>\n  <span>World</span></p>\n\n<script id=innerHTML>\n  const d1 = $('#d1');\n  testing.expectEqual('hello <em>world</em>', d1.innerHTML);\n\n  d1.innerHTML = 'only text';\n  testing.expectEqual('only text', d1.innerHTML);\n\n  d1.innerHTML = 'hello <div>world</div><b>!!</b>';\n  testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerHTML);\n  for (let child of d1.childNodes) {\n    testing.expectEqual(d1, child.parentNode);\n  }\n\n  // doesn't run JavaScript, important!\n  let inner_loaded = false;\n  d1.innerHTML = '<script src=inner.js>';\n  testing.expectEqual('<script src=\"inner.js\"><\\/script>', d1.innerHTML);\n  testing.expectEqual(false, inner_loaded);\n\n  const d2 = $('#d2');\n  testing.expectEqual(\"\\n  <style>h1 { font-size: 1em; }</style>\\n  <!-- this is a comment -->\\n  This is a <br>\\n  text\\n\", d2.innerHTML);\n</script>\n\n<script id=ids>\n  d1.innerHTML = '<a id=link>home</a>';\n  testing.expectEqual('home', $('#link').innerText);\n\n  d1.innerHTML = '';\n  testing.expectEqual(null, $('#link'));\n\n  d1.innerHTML = '<div><p><a id=link>hi</a></p></div>';\n  testing.expectEqual('hi', $('#link').innerText);\n\n  d1.innerHTML = '';\n</script>\n\n<script id=attributeSerialization>\n  d1.innerHTML = '<div class=\"test\">text</div>';\n  testing.expectEqual('<div class=\"test\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<div id=simple>text</div>';\n  testing.expectEqual('<div id=\"simple\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<a href=\"/?foo=1&bar=2\">link</a>';\n  testing.expectEqual('<a href=\"/?foo=1&amp;bar=2\">link</a>', d1.innerHTML);\n\n  d1.innerHTML = '<div title=\"She said &quot;hello&quot;\">text</div>';\n  testing.expectEqual('<div title=\"She said &quot;hello&quot;\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<div data-value=\"<tag>\">text</div>';\n  testing.expectEqual('<div data-value=\"&lt;tag&gt;\">text</div>', d1.innerHTML);\n\n  const div1 = document.createElement('div');\n  div1.setAttribute('title', 'line1\\nline2');\n  d1.innerHTML = '';\n  d1.appendChild(div1);\n  testing.expectEqual('<div title=\"line1\\nline2\"></div>', d1.innerHTML);\n\n  const div2 = document.createElement('div');\n  div2.setAttribute('title', 'line1\\rline2');\n  d1.innerHTML = '';\n  d1.appendChild(div2);\n  testing.expectEqual('<div title=\"line1\\rline2\"></div>', d1.innerHTML);\n\n  const div3 = document.createElement('div');\n  div3.setAttribute('title', 'col1\\tcol2');\n  d1.innerHTML = '';\n  d1.appendChild(div3);\n  testing.expectEqual('<div title=\"col1\\tcol2\"></div>', d1.innerHTML);\n\n  d1.innerHTML = '<div data-test=\"&quot;&amp;&lt;&gt;\">text</div>';\n  testing.expectEqual('<div data-test=\"&quot;&amp;&lt;&gt;\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<input type=\"text\" value=\"\">';\n  testing.expectEqual('<input type=\"text\" value=\"\">', d1.innerHTML);\n\n  d1.innerHTML = '<div class=\"foo bar\">text</div>';\n  testing.expectEqual('<div class=\"foo bar\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<div data-expr=\"x=5\">text</div>';\n  testing.expectEqual('<div data-expr=\"x=5\">text</div>', d1.innerHTML);\n\n  const div4 = document.createElement('div');\n  div4.setAttribute('title', \"it's working\");\n  d1.innerHTML = '';\n  d1.appendChild(div4);\n  testing.expectEqual('<div title=\"it\\'s working\"></div>', d1.innerHTML);\n\n  const div5 = document.createElement('div');\n  div5.setAttribute('data-code', '`template`');\n  d1.innerHTML = '';\n  d1.appendChild(div5);\n  testing.expectEqual('<div data-code=\"`template`\"></div>', d1.innerHTML);\n\n  d1.innerHTML = '<a href=\"/search?q=test&lang=en\" title=\"Search: &quot;test&quot;\">Search</a>';\n  testing.expectEqual('<a href=\"/search?q=test&amp;lang=en\" title=\"Search: &quot;test&quot;\">Search</a>', d1.innerHTML);\n\n  d1.innerHTML = '<div data-start=\"&start\">text</div>';\n  testing.expectEqual('<div data-start=\"&amp;start\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<div data-end=\"end&\">text</div>';\n  testing.expectEqual('<div data-end=\"end&amp;\">text</div>', d1.innerHTML);\n\n  d1.innerHTML = '<div data-middle=\"mid&dle\">text</div>';\n  testing.expectEqual('<div data-middle=\"mid&amp;dle\">text</div>', d1.innerHTML);\n</script>\n\n<script id=voidElements>\n  d1.innerHTML = '<br>';\n  testing.expectEqual('<br>', d1.innerHTML);\n\n  d1.innerHTML = '<hr>';\n  testing.expectEqual('<hr>', d1.innerHTML);\n\n  d1.innerHTML = '<img src=\"test.png\" alt=\"test\">';\n  testing.expectEqual('<img src=\"test.png\" alt=\"test\">', d1.innerHTML);\n\n  d1.innerHTML = '<input type=\"text\" name=\"field\">';\n  testing.expectEqual('<input type=\"text\" name=\"field\">', d1.innerHTML);\n\n  d1.innerHTML = '<link rel=\"stylesheet\" href=\"style.css\">';\n  testing.expectEqual('<link rel=\"stylesheet\" href=\"style.css\">', d1.innerHTML);\n\n  d1.innerHTML = '<meta charset=\"utf-8\">';\n  testing.expectEqual('<meta charset=\"utf-8\">', d1.innerHTML);\n\n  d1.innerHTML = '<div><br><hr><img src=\"x.png\"></div>';\n  testing.expectEqual('<div><br><hr><img src=\"x.png\"></div>', d1.innerHTML);\n</script>\n\n<script id=innerText>\n  // Get innerText from element\n  d1.innerHTML = 'hello <em>world</em>';\n  testing.expectEqual('hello world', d1.innerText);\n\n  // Set innerText - should replace children with text node\n  d1.innerText = 'only text';\n  testing.expectEqual('only text', d1.innerText);\n  testing.expectEqual('only text', d1.innerHTML);\n\n  // innerText does NOT parse HTML (unlike innerHTML)\n  d1.innerText = 'hello <div>world</div><b>!!</b>';\n  testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerText);\n  testing.expectEqual('hello &lt;div&gt;world&lt;/div&gt;&lt;b&gt;!!&lt;/b&gt;', d1.innerHTML);\n\n  // Setting empty string clears children\n  d1.innerText = '';\n  testing.expectEqual('', d1.innerText);\n  testing.expectEqual('', d1.innerHTML);\n\n  // innerText with nested elements\n  d1.innerHTML = '<div>hello <span>beautiful</span> world</div>';\n  testing.expectEqual('hello beautiful world', d1.innerText);\n\n  // Setting innerText removes all previous children including elements\n  d1.innerHTML = '<div><p><a id=link2>hi</a></p></div>';\n  testing.expectEqual('hi', d1.innerText);\n  d1.innerText = 'new content';\n  testing.expectEqual('new content', d1.innerText);\n  testing.expectEqual(null, $('#link2'));\n\n  testing.expectEqual(\"This is a\\ntext\", d2.innerText);\n  testing.expectEqual(\"Hello World\", $('#d3').innerText);\n  testing.expectEqual(\"Hello World\", $('#d4').innerText);\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/inner.js",
    "content": "inner_loaded = true;\n"
  },
  {
    "path": "src/browser/tests/element/matches.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"test-container\" class=\"container main\">\n  <p class=\"text highlight\">Paragraph 1</p>\n  <div class=\"nested\">\n    <p class=\"text\">Paragraph 2</p>\n    <span id=\"special\" class=\"wrapper\">\n      <p class=\"deep\">Paragraph 3</p>\n    </span>\n  </div>\n</div>\n\n<script id=basicMatches>\n{\n  const container = $('#test-container');\n\n  testing.expectEqual(true, container.matches('#test-container'));\n  testing.expectEqual(false, container.matches('#other'));\n\n  testing.expectEqual(true, container.matches('.container'));\n  testing.expectEqual(true, container.matches('.main'));\n  testing.expectEqual(false, container.matches('.nested'));\n\n  testing.expectEqual(true, container.matches('div'));\n  testing.expectEqual(false, container.matches('p'));\n\n  testing.expectEqual(true, container.matches('div.container'));\n  testing.expectEqual(true, container.matches('div#test-container'));\n  testing.expectEqual(true, container.matches('.container.main'));\n  testing.expectEqual(false, container.matches('div.nested'));\n\n  testing.expectEqual(true, container.matches('*'));\n}\n</script>\n\n<script id=childMatches>\n{\n  const paragraph = $('#test-container > p');\n\n  testing.expectEqual(true, paragraph.matches('p'));\n  testing.expectEqual(true, paragraph.matches('.text'));\n  testing.expectEqual(true, paragraph.matches('.highlight'));\n  testing.expectEqual(true, paragraph.matches('p.text.highlight'));\n\n  testing.expectEqual(false, paragraph.matches('#test-container'));\n  testing.expectEqual(false, paragraph.matches('div'));\n}\n</script>\n\n<script id=specialSpan>\n{\n  const span = $('#special');\n\n  testing.expectEqual(true, span.matches('#special'));\n  testing.expectEqual(true, span.matches('span'));\n  testing.expectEqual(true, span.matches('.wrapper'));\n  testing.expectEqual(true, span.matches('span.wrapper'));\n  testing.expectEqual(true, span.matches('span#special.wrapper'));\n  testing.expectEqual(false, span.matches('#other'));\n  testing.expectEqual(false, span.matches('div'));\n}\n</script>\n\n<script id=errorHandling>\n{\n  const container = $('#test-container');\n\n  testing.expectError(\"SyntaxError\", () => container.matches(''));\n  testing.withError((err) => {\n    testing.expectEqual(12, err.code);\n    testing.expectEqual(\"SyntaxError\", err.name);\n  }, () => container.matches(''));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/outer.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1>hello <em>world</em></div>\n\n<script id=outerHTML>\n  const d1 = $('#d1');\n  testing.expectEqual('<div id=\\\"d1\\\">hello <em>world</em></div>', d1.outerHTML);\n  d1.outerHTML = '<p id=p1>spice</p>';\n  // setting outerHTML doesn't update what d1 points to\n  testing.expectEqual('<div id=\"d1\">hello <em>world</em></div>', d1.outerHTML);\n\n  // but it does update the document\n  testing.expectEqual(null, document.getElementById('d1'));\n  testing.expectEqual(true, document.getElementById('p1') != null);\n  testing.expectEqual('<p id=\"p1\">spice</p>', document.getElementById('p1').outerHTML);\n  // testing.expectEqual(true, document.body.outerHTML.replaceAll(/\\n/g, '').startsWith('<body><p id=\"p1\">spice</p><script id=\"outerHTML\">'));\n\n  // document.getElementById('p1').outerHTML = '';\n  // testing.expectEqual(null, document.getElementById('p1'));\n  // testing.expectEqual(true, document.body.outerHTML.replaceAll(/\\n/g, '').startsWith('<body><script id=\"outerHTML\">'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/position.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"test1\">Test Element</div>\n<div id=\"test2\">Another Element</div>\n\n<script id=\"clientDimensions\">\n{\n  const test1 = $('#test1');\n\n  // clientWidth/Height - default is 5px in dummy layout\n  testing.expectEqual('number', typeof test1.clientWidth);\n  testing.expectEqual('number', typeof test1.clientHeight);\n  testing.expectTrue(test1.clientWidth >= 0);\n  testing.expectTrue(test1.clientHeight >= 0);\n\n  // clientTop/Left should be 0 (no borders in dummy layout)\n  testing.expectEqual(0, test1.clientTop);\n  testing.expectEqual(0, test1.clientLeft);\n}\n</script>\n\n<script id=\"scrollDimensions\">\n{\n  const test1 = $('#test1');\n\n  // In dummy layout, scroll dimensions equal client dimensions (no overflow)\n  testing.expectEqual(test1.clientWidth, test1.scrollWidth);\n  testing.expectEqual(test1.clientHeight, test1.scrollHeight);\n}\n</script>\n\n<script id=\"scrollPosition\">\n{\n  const test1 = $('#test1');\n\n  // Initial scroll position should be 0\n  testing.expectEqual(0, test1.scrollTop);\n  testing.expectEqual(0, test1.scrollLeft);\n\n  // Setting scroll position\n  test1.scrollTop = 50;\n  testing.expectEqual(50, test1.scrollTop);\n\n  test1.scrollLeft = 25;\n  testing.expectEqual(25, test1.scrollLeft);\n\n  // Negative values should be clamped to 0\n  test1.scrollTop = -10;\n  testing.expectEqual(0, test1.scrollTop);\n\n  test1.scrollLeft = -5;\n  testing.expectEqual(0, test1.scrollLeft);\n\n  // Each element has independent scroll position\n  const test2 = $('#test2');\n  testing.expectEqual(0, test2.scrollTop);\n  testing.expectEqual(0, test2.scrollLeft);\n\n  test2.scrollTop = 100;\n  testing.expectEqual(100, test2.scrollTop);\n  testing.expectEqual(0, test1.scrollTop); // test1 should still be 0\n}\n</script>\n\n<script id=\"offsetDimensions\">\n{\n  const test1 = $('#test1');\n\n  // offsetWidth/Height should be numbers\n  testing.expectEqual('number', typeof test1.offsetWidth);\n  testing.expectEqual('number', typeof test1.offsetHeight);\n  testing.expectTrue(test1.offsetWidth >= 0);\n  testing.expectTrue(test1.offsetHeight >= 0);\n\n  // Should equal client dimensions\n  testing.expectEqual(test1.clientWidth, test1.offsetWidth);\n  testing.expectEqual(test1.clientHeight, test1.offsetHeight);\n}\n</script>\n\n<script id=\"offsetPosition\">\n{\n  const test1 = $('#test1');\n  const test2 = $('#test2');\n\n  // offsetTop/Left should be calculated from tree position\n  // These values are based on the heuristic layout engine\n  const top1 = test1.offsetTop;\n  const left1 = test1.offsetLeft;\n  const top2 = test2.offsetTop;\n  const left2 = test2.offsetLeft;\n\n  // Position values should be numbers\n  testing.expectEqual('number', typeof top1);\n  testing.expectEqual('number', typeof left1);\n  testing.expectEqual('number', typeof top2);\n  testing.expectEqual('number', typeof left2);\n\n  // Siblings should have different positions (either different x or y)\n  testing.expectTrue(top1 !== top2 || left1 !== left2);\n}\n</script>\n\n<script id=\"offsetVsBounding\">\n{\n  const test1 = $('#test1');\n\n  // offsetTop/Left should match getBoundingClientRect\n  const rect = test1.getBoundingClientRect();\n  testing.expectEqual(rect.y, test1.offsetTop);\n  testing.expectEqual(rect.x, test1.offsetLeft);\n  testing.expectEqual(rect.width, test1.offsetWidth);\n  testing.expectEqual(rect.height, test1.offsetHeight);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/pseudo_classes.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\">\n    <p id=\"first\">First</p>\n    <input type=\"checkbox\" id=\"checkbox\">\n    <span id=\"empty\"></span>\n    <span id=\"nonempty\">Content</span>\n</div>\n\n<script id=\"parsing\">\n{\n    const container = $('#container');\n\n    // Test that all pseudo-classes parse without throwing errors\n    const pseudoClasses = [\n        ':modal', ':checked', ':disabled', ':enabled', ':indeterminate',\n        ':valid', ':invalid', ':required', ':optional', ':in-range', ':out-of-range',\n        ':placeholder-shown', ':read-only', ':read-write', ':default',\n        ':hover', ':active', ':focus', ':focus-within', ':focus-visible',\n        ':link', ':visited', ':any-link', ':target',\n        ':root', ':empty', ':first-child', ':last-child', ':only-child',\n        ':first-of-type', ':last-of-type', ':only-of-type',\n        ':nth-child(2)', ':nth-last-child(2)', ':nth-of-type(2)', ':nth-last-of-type(2)',\n        ':defined', ':not(div)', ':is(div, span)', ':where(p, a)', ':has(span)', ':lang(en)'\n    ];\n\n    // Each querySelector should not throw an error\n    let allPassed = true;\n    for (const pseudo of pseudoClasses) {\n        try {\n            container.querySelector(pseudo);\n        } catch (e) {\n            allPassed = false;\n            break;\n        }\n    }\n    testing.expectTrue(allPassed);\n}\n</script>\n\n<script id=\"empty\">\n{\n    const empty = document.querySelector(':empty');\n    testing.expectTrue(empty !== null);\n}\n</script>\n\n<script id=\"root\">\n{\n    const html = document.querySelector(':root');\n    testing.expectTrue(html !== null);\n}\n</script>\n\n<script id=\"has\">\n{\n    const result = document.querySelector(':has(span)');\n    testing.expectTrue(result !== null);\n}\n</script>\n\n<script id=\"not\">\n{\n    const noDiv = document.querySelectorAll(':not(div)');\n    testing.expectTrue(noDiv.length > 0);\n}\n</script>\n\n<script id=\"is\">\n{\n    const isResult = document.querySelectorAll(':is(p, span, input)');\n    testing.expectTrue(isResult.length >= 4);\n}\n</script>\n\n<script id=\"where\">\n{\n    const whereResult = document.querySelectorAll(':where(p, span)');\n    testing.expectTrue(whereResult.length >= 3);\n}\n</script>\n\n<script id=\"is_empty\">\n{\n    // Empty :is() and :where() are valid per spec and match nothing\n    const isEmptyResult = document.querySelectorAll(':is()');\n    testing.expectEqual(0, isEmptyResult.length);\n\n    const whereEmptyResult = document.querySelectorAll(':where()');\n    testing.expectEqual(0, whereEmptyResult.length);\n}\n</script>\n\n<div id=escaped class=\":popover-open\"></div>\n<script id=\"escaped\">\n{\n    const escaped = document.querySelector('.\\\\:popover-open');\n    testing.expectEqual('escaped', escaped.id);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/query_selector.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=p1>\n  <div id=c2>\n    <div id=c3></div>\n  </div>\n</div>\n<div id=other></div>\n\n<script id=querySelector\">\n  const p1 = $('#p1');\n  testing.expectEqual(null, p1.querySelector('#p1'));\n\n  testing.expectError(\"SyntaxError\", () => p1.querySelector(''));\n  testing.withError((err) => {\n    testing.expectEqual(12, err.code);\n    testing.expectEqual(\"SyntaxError\", err.name);\n  }, () => p1.querySelector(''));\n\n  testing.expectEqual($('#c2'), p1.querySelector('#c2'));\n  testing.expectEqual($('#c3'), p1.querySelector('#c3'));\n  testing.expectEqual(null, p1.querySelector('#nope'));\n  testing.expectEqual(null, p1.querySelector('#other'));\n\n  testing.expectEqual($('#c2'), p1.querySelector('*'));\n  testing.expectEqual($('#c3'), p1.querySelector('*#c3'));\n</script>\n\n<div id=\"desc-container\">\n  <p class=\"text\">Direct child paragraph</p>\n  <div class=\"nested\">\n    <p class=\"text\">Nested paragraph</p>\n    <span class=\"wrapper\">\n      <p class=\"deep\">Deeply nested paragraph</p>\n    </span>\n  </div>\n  <article>\n    <p class=\"text\">Article paragraph</p>\n  </article>\n</div>\n\n<div id=\"outer-div\">\n  <div class=\"level1\">\n    <div class=\"level2\">\n      <span id=\"deep-span\">Deep span</span>\n    </div>\n  </div>\n</div>\n\n<script id=descendantSelectors>\n{\n  const container = $('#desc-container');\n\n  testing.expectEqual('Direct child paragraph', container.querySelector('div p').textContent);\n  testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent);\n  testing.expectEqual('Deeply nested paragraph', container.querySelector('div span p').textContent);\n  testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent);\n  testing.expectEqual(null, container.querySelector('article div p'));\n\n  const outerDiv = $('#outer-div');\n  testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id);\n  testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id);\n  testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/query_selector_all.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=root>\n  <div class=\"item\">Item 1</div>\n  <span class=\"item\">Item 2</span>\n  <div class=\"item special\">Item 3</div>\n  <div id=nested>\n    <div class=\"item\">Item 4</div>\n    <span class=\"special\">Item 5</span>\n  </div>\n</div>\n<div id=outside class=\"item\">Outside</div>\n\n<script>\n  function assertList(expected, result) {\n    testing.expectEqual(expected.length, result.length);\n    testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));\n    testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));\n    testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));\n  }\n</script>\n\n<script id=errors>\n{\n  const root = $('#root');\n  testing.expectError(\"SyntaxError\", () => root.querySelectorAll(''));\n  testing.withError((err) => {\n    testing.expectEqual(12, err.code);\n    testing.expectEqual(\"SyntaxError\", err.name);\n  }, () => root.querySelectorAll(''));\n}\n</script>\n\n<script id=byId>\n{\n  const root = $('#root');\n  const nested = root.querySelectorAll('#nested');\n  testing.expectEqual(true, nested instanceof NodeList);\n  testing.expectEqual(1, nested.length);\n  testing.expectEqual('nested', nested[0].getAttribute('id'));\n\n  assertList([], root.querySelectorAll('#outside'));\n  assertList([], root.querySelectorAll('#nope'));\n}\n</script>\n\n<script id=byClass>\n{\n  const root = $('#root');\n  assertList(['Item 1', 'Item 2', 'Item 3', 'Item 4'], root.querySelectorAll('.item'));\n  assertList(['Item 3', 'Item 5'], root.querySelectorAll('.special'));\n  assertList([], root.querySelectorAll('.nope'));\n}\n</script>\n\n<script id=byTag>\n{\n  const root = $('#root');\n  const divs = root.querySelectorAll('div');\n  testing.expectEqual(4, divs.length);\n\n  assertList(['Item 2', 'Item 5'], root.querySelectorAll('span'));\n  assertList([], root.querySelectorAll('article'));\n}\n</script>\n\n<script id=compound>\n{\n  const root = $('#root');\n  const divItems = root.querySelectorAll('div.item');\n  testing.expectEqual(3, divItems.length);\n\n  assertList(['Item 2'], root.querySelectorAll('span.item'));\n  assertList(['Item 3'], root.querySelectorAll('div.item.special'));\n  assertList(['Item 3'], root.querySelectorAll('.item.special'));\n  assertList(['Item 3'], root.querySelectorAll('.special.item'));\n}\n</script>\n\n<script id=universal>\n{\n  const root = $('#root');\n  const all = root.querySelectorAll('*');\n  testing.expectEqual(true, all.length >= 6);\n  testing.expectEqual('Item 1', all[0].textContent);\n  testing.expectEqual('Item 1', all.item(0).textContent);\n  testing.expectEqual(null, all.item(99));\n  testing.expectEqual(undefined, all[99]);\n\n  const items = root.querySelectorAll('*.item');\n  testing.expectEqual(4, items.length);\n\n  assertList(['Item 3', 'Item 5'], root.querySelectorAll('*.special'));\n}\n</script>\n\n<script id=nested>\n{\n  const nested = $('#nested');\n  assertList(['Item 4'], nested.querySelectorAll('.item'));\n  assertList(['Item 5'], nested.querySelectorAll('.special'));\n  assertList(['Item 4'], nested.querySelectorAll('div'));\n  assertList(['Item 5'], nested.querySelectorAll('span'));\n}\n</script>\n\n<script id=iterators>\n{\n  const root = $('#root');\n  const list = root.querySelectorAll('.special');\n\n  const entries = Array.from(list.entries());\n  testing.expectEqual(2, entries.length);\n  testing.expectEqual(0, entries[0][0]);\n  testing.expectEqual('Item 3', entries[0][1].textContent);\n  testing.expectEqual(1, entries[1][0]);\n  testing.expectEqual('Item 5', entries[1][1].textContent);\n\n  const keys = Array.from(list.keys());\n  testing.expectEqual(2, keys.length);\n  testing.expectEqual(0, keys[0]);\n  testing.expectEqual(1, keys[1]);\n\n  const values = Array.from(list.values());\n  testing.expectEqual(['Item 3', 'Item 5'], values.map((e) => e.textContent));\n\n  const defaultIter = Array.from(list);\n  testing.expectEqual(['Item 3', 'Item 5'], defaultIter.map((e) => e.textContent));\n}\n</script>\n\n<div id=\"desc-root\">\n  <p class=\"text\">Direct child paragraph</p>\n  <div class=\"nested\">\n    <p class=\"text\">Nested paragraph</p>\n    <span class=\"wrapper\">\n      <p class=\"deep\">Deeply nested paragraph</p>\n    </span>\n  </div>\n  <article>\n    <p class=\"text\">Article paragraph</p>\n    <div class=\"inner\">\n      <span>Article span</span>\n    </div>\n  </article>\n</div>\n\n<div id=\"multi-level\">\n  <div class=\"level1\">\n    <div class=\"level2\">\n      <span class=\"target\">Target 1</span>\n    </div>\n  </div>\n  <div class=\"level1\">\n    <span class=\"target\">Target 2</span>\n  </div>\n</div>\n\n<script id=descendantSelectors>\n{\n  const root = $('#desc-root');\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], root.querySelectorAll('div p'));\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Article paragraph'], root.querySelectorAll('div .text'));\n  assertList(['Nested paragraph', 'Deeply nested paragraph'], root.querySelectorAll('.nested p'));\n  assertList(['Deeply nested paragraph'], root.querySelectorAll('div span p'));\n  assertList(['Nested paragraph'], root.querySelectorAll('div.nested p.text'));\n  assertList([], root.querySelectorAll('article div p'));\n  assertList(['Article span'], root.querySelectorAll('article span'));\n  assertList(['Article span'], root.querySelectorAll('article div span'));\n}\n</script>\n\n<script id=multiLevelDescendant>\n{\n  const root = $('#multi-level');\n  assertList(['Target 1', 'Target 2'], root.querySelectorAll('div span'));\n  assertList(['Target 1', 'Target 2'], root.querySelectorAll('div div span'));\n  assertList(['Target 1'], root.querySelectorAll('.level1 .level2 .target'));\n  assertList(['Target 1', 'Target 2'], root.querySelectorAll('.level1 .target'));\n}\n</script>\n\n<script id=descendantWithWhitespace>\n{\n  const root = $('#desc-root');\n  assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], root.querySelectorAll('  div   p  '));\n  assertList(['Nested paragraph'], root.querySelectorAll('  div.nested    p.text  '));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/query_selector_scope.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\">\n  <div id=\"child1\" class=\"item\">\n    <span id=\"grandchild1\">Grandchild 1</span>\n    <span id=\"grandchild2\">Grandchild 2</span>\n  </div>\n  <div id=\"child2\" class=\"item\">\n    <span id=\"grandchild3\">Grandchild 3</span>\n  </div>\n</div>\n\n<script id=\"scopeBasic\">\n{\n  // :scope refers to the reference element but querySelector only returns descendants\n  const container = $('#container');\n\n  // :scope alone doesn't match anything because querySelector only returns descendants\n  const scopeMatch = container.querySelector(':scope');\n  testing.expectEqual(null, scopeMatch);\n\n  // :scope in querySelectorAll should also return empty\n  const scopeMatches = container.querySelectorAll(':scope');\n  testing.expectEqual(0, scopeMatches.length);\n}\n</script>\n\n<script id=\"scopeWithCombinators\">\n{\n  const container = $('#container');\n\n  // :scope > child - direct children of scope\n  const directChildren = container.querySelectorAll(':scope > div');\n  testing.expectEqual(2, directChildren.length);\n  testing.expectEqual($('#child1'), directChildren[0]);\n  testing.expectEqual($('#child2'), directChildren[1]);\n\n  // :scope > .item - direct children with class\n  const itemChildren = container.querySelectorAll(':scope > .item');\n  testing.expectEqual(2, itemChildren.length);\n\n  // :scope span - descendant spans of scope\n  const spans = container.querySelectorAll(':scope span');\n  testing.expectEqual(3, spans.length);\n\n  // :scope > div > span - grandchildren via specific path\n  const grandchildren = container.querySelectorAll(':scope > div > span');\n  testing.expectEqual(3, grandchildren.length);\n}\n</script>\n\n<div id=\"nested-container\">\n  <div class=\"outer\">\n    <div class=\"inner\" id=\"target\">\n      <span class=\"text\">Inner text</span>\n    </div>\n    <div class=\"inner\">\n      <span class=\"text\">Other text</span>\n    </div>\n  </div>\n</div>\n\n<script id=\"scopeNested\">\n{\n  const target = $('#target');\n\n  // :scope refers to target but querySelector only returns descendants\n  const scopeMatch = target.querySelector(':scope');\n  testing.expectEqual(null, scopeMatch);\n\n  // :scope > span should find direct children of target\n  const directSpan = target.querySelector(':scope > span');\n  testing.expectEqual('Inner text', directSpan.textContent);\n\n  // When querySelector is called on target, :scope refers to target\n  const scopeChildren = target.querySelectorAll(':scope > .text');\n  testing.expectEqual(1, scopeChildren.length);\n}\n</script>\n\n<div id=\"compound-test\">\n  <div class=\"box\" id=\"box1\">Box 1</div>\n  <div class=\"box\" id=\"box2\">Box 2</div>\n  <span class=\"box\" id=\"box3\">Box 3</span>\n</div>\n\n<script id=\"scopeCompound\">\n{\n  const compound = $('#compound-test');\n\n  // Compound selector with :scope\n  const divBoxes = compound.querySelectorAll(':scope > div.box');\n  testing.expectEqual(2, divBoxes.length);\n  testing.expectEqual($('#box1'), divBoxes[0]);\n  testing.expectEqual($('#box2'), divBoxes[1]);\n\n  // :scope with multiple parts\n  const spanBox = compound.querySelector(':scope > span.box');\n  testing.expectEqual($('#box3'), spanBox);\n}\n</script>\n\n<div id=\"pseudo-container\">\n  <div class=\"parent\" id=\"p1\">\n    <div class=\"child\">Child 1</div>\n    <div class=\"child\">Child 2</div>\n  </div>\n  <div class=\"parent\" id=\"p2\">\n    <div class=\"child\">Child 3</div>\n  </div>\n</div>\n\n<script id=\"scopeWithOtherPseudos\">\n{\n  const container = $('#pseudo-container');\n\n  // :scope with :not()\n  const notP1Children = container.querySelectorAll(':scope > .parent:not(#p1) > .child');\n  testing.expectEqual(1, notP1Children.length);\n  testing.expectEqual('Child 3', notP1Children[0].textContent);\n\n  // :scope with :first-child\n  const firstParent = container.querySelector(':scope > .parent:first-child');\n  testing.expectEqual($('#p1'), firstParent);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/remove.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1>\n  <div id=d2>\n    <div id=d3>\n      <div id=d4>\n        <div id=d5>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script id=remove>\n  testing.expectEqual(1, $('#d4').childElementCount);\n  $('#d5').remove();\n  testing.expectEqual(null, document.getElementById('#d5'));\n  testing.expectEqual(0, $('#d4').childElementCount);\n\n  testing.expectEqual(1, $('#d1').childElementCount);\n  $('#d2').remove();\n  testing.expectEqual(null, document.getElementById('#d2'));\n  testing.expectEqual(null, document.getElementById('#d3'));\n  testing.expectEqual(null, document.getElementById('#d4'));\n  testing.expectEqual(0, $('#d1').childElementCount);\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/replace_with.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<!-- Test 1: Basic single element replacement -->\n<div id=\"test1\">\n  <div id=\"parent1\">\n    <div id=\"old1\">Old Content</div>\n  </div>\n</div>\n\n<script id=\"test1-basic-replacement\">\n  const old1 = $('#old1');\n  const parent1 = $('#parent1');\n\n  testing.expectEqual(1, parent1.childElementCount);\n  testing.expectEqual(old1, document.getElementById('old1'));\n\n  const new1 = document.createElement('div');\n  new1.id = 'new1';\n  new1.textContent = 'New Content';\n\n  old1.replaceWith(new1);\n\n  testing.expectEqual(1, parent1.childElementCount);\n  testing.expectEqual(null, document.getElementById('old1'));\n  testing.expectEqual(new1, document.getElementById('new1'));\n  testing.expectEqual(parent1, new1.parentElement);\n</script>\n\n<!-- Test 2: Replace with multiple elements -->\n<div id=\"test2\">\n  <div id=\"parent2\">\n    <div id=\"old2\">Old</div>\n  </div>\n</div>\n\n<script id=\"test2-multiple-elements\">\n  const old2 = $('#old2');\n  const parent2 = $('#parent2');\n\n  testing.expectEqual(1, parent2.childElementCount);\n\n  const new2a = document.createElement('div');\n  new2a.id = 'new2a';\n  const new2b = document.createElement('div');\n  new2b.id = 'new2b';\n  const new2c = document.createElement('div');\n  new2c.id = 'new2c';\n\n  old2.replaceWith(new2a, new2b, new2c);\n\n  testing.expectEqual(3, parent2.childElementCount);\n  testing.expectEqual(null, document.getElementById('old2'));\n  testing.expectEqual(new2a, document.getElementById('new2a'));\n  testing.expectEqual(new2b, document.getElementById('new2b'));\n  testing.expectEqual(new2c, document.getElementById('new2c'));\n\n  // Check order\n  testing.expectEqual(new2a, parent2.children[0]);\n  testing.expectEqual(new2b, parent2.children[1]);\n  testing.expectEqual(new2c, parent2.children[2]);\n</script>\n\n<!-- Test 3: Replace with text nodes -->\n<div id=\"test3\">\n  <div id=\"parent3\"><div id=\"old3\">Old</div></div>\n</div>\n\n<script id=\"test3-text-nodes\">\n  const old3 = $('#old3');\n  const parent3 = $('#parent3');\n\n  old3.replaceWith('Text1', ' ', 'Text2');\n\n  testing.expectEqual(null, document.getElementById('old3'));\n  testing.expectEqual('Text1 Text2', parent3.textContent);\n</script>\n\n<!-- Test 4: Replace with mix of elements and text -->\n<div id=\"test4\">\n  <div id=\"parent4\"><div id=\"old4\">Old</div></div>\n</div>\n\n<script id=\"test4-mixed\">\n  const old4 = $('#old4');\n  const parent4 = $('#parent4');\n\n  const new4 = document.createElement('span');\n  new4.id = 'new4';\n  new4.textContent = 'Element';\n\n  old4.replaceWith('Before ', new4, ' After');\n\n  testing.expectEqual(null, document.getElementById('old4'));\n  testing.expectEqual(new4, document.getElementById('new4'));\n  testing.expectEqual('Before Element After', parent4.textContent);\n</script>\n\n<!-- Test 5: Replace element not connected to document -->\n<script id=\"test5-not-connected\">\n  const disconnected = document.createElement('div');\n  disconnected.id = 'disconnected5';\n\n  const replacement = document.createElement('div');\n  replacement.id = 'replacement5';\n\n  // Should do nothing since element has no parent\n  disconnected.replaceWith(replacement);\n\n  // Neither should be in the document\n  testing.expectEqual(null, document.getElementById('disconnected5'));\n  testing.expectEqual(null, document.getElementById('replacement5'));\n</script>\n\n<!-- Test 6: Replace with nodes that already have a parent -->\n<div id=\"test6\">\n  <div id=\"parent6a\">\n    <div id=\"old6\">Old</div>\n  </div>\n  <div id=\"parent6b\">\n    <div id=\"moving6a\">Moving A</div>\n    <div id=\"moving6b\">Moving B</div>\n  </div>\n</div>\n\n<script id=\"test6-moving-nodes\">\n  const old6 = $('#old6');\n  const parent6a = $('#parent6a');\n  const parent6b = $('#parent6b');\n  const moving6a = $('#moving6a');\n  const moving6b = $('#moving6b');\n\n  testing.expectEqual(1, parent6a.childElementCount);\n  testing.expectEqual(2, parent6b.childElementCount);\n\n  // Replace old6 with nodes that already have parent6b as parent\n  old6.replaceWith(moving6a, moving6b);\n\n  // old6 should be gone\n  testing.expectEqual(null, document.getElementById('old6'));\n\n  // parent6a should now have the moved elements\n  testing.expectEqual(2, parent6a.childElementCount);\n  testing.expectEqual(moving6a, parent6a.children[0]);\n  testing.expectEqual(moving6b, parent6a.children[1]);\n\n  // parent6b should now be empty\n  testing.expectEqual(0, parent6b.childElementCount);\n\n  // getElementById should still work\n  testing.expectEqual(moving6a, document.getElementById('moving6a'));\n  testing.expectEqual(moving6b, document.getElementById('moving6b'));\n  testing.expectEqual(parent6a, moving6a.parentElement);\n  testing.expectEqual(parent6a, moving6b.parentElement);\n</script>\n\n<!-- Test 7: Replace with nested elements -->\n<div id=\"test7\">\n  <div id=\"parent7\">\n    <div id=\"old7\">Old</div>\n  </div>\n</div>\n\n<script id=\"test7-nested\">\n  const old7 = $('#old7');\n  const parent7 = $('#parent7');\n\n  const new7 = document.createElement('div');\n  new7.id = 'new7';\n\n  const child7a = document.createElement('div');\n  child7a.id = 'child7a';\n  const child7b = document.createElement('div');\n  child7b.id = 'child7b';\n\n  new7.appendChild(child7a);\n  new7.appendChild(child7b);\n\n  old7.replaceWith(new7);\n\n  testing.expectEqual(null, document.getElementById('old7'));\n  testing.expectEqual(new7, document.getElementById('new7'));\n  testing.expectEqual(child7a, document.getElementById('child7a'));\n  testing.expectEqual(child7b, document.getElementById('child7b'));\n  testing.expectEqual(2, new7.childElementCount);\n</script>\n\n<!-- Test 8: Replace maintains sibling order -->\n<div id=\"test8\">\n  <div id=\"parent8\">\n    <div id=\"before8\">Before</div>\n    <div id=\"old8\">Old</div>\n    <div id=\"after8\">After</div>\n  </div>\n</div>\n\n<script id=\"test8-sibling-order\">\n  const old8 = $('#old8');\n  const parent8 = $('#parent8');\n  const before8 = $('#before8');\n  const after8 = $('#after8');\n\n  testing.expectEqual(3, parent8.childElementCount);\n\n  const new8 = document.createElement('div');\n  new8.id = 'new8';\n\n  old8.replaceWith(new8);\n\n  testing.expectEqual(3, parent8.childElementCount);\n  testing.expectEqual(before8, parent8.children[0]);\n  testing.expectEqual(new8, parent8.children[1]);\n  testing.expectEqual(after8, parent8.children[2]);\n</script>\n\n<!-- Test 9: Replace first child -->\n<div id=\"test9\">\n  <div id=\"parent9\">\n    <div id=\"first9\">First</div>\n    <div id=\"second9\">Second</div>\n  </div>\n</div>\n\n<script id=\"test9-first-child\">\n  const first9 = $('#first9');\n  const parent9 = $('#parent9');\n\n  const new9 = document.createElement('div');\n  new9.id = 'new9';\n\n  first9.replaceWith(new9);\n\n  testing.expectEqual(null, document.getElementById('first9'));\n  testing.expectEqual(new9, parent9.firstElementChild);\n  testing.expectEqual(new9, parent9.children[0]);\n</script>\n\n<!-- Test 10: Replace last child -->\n<div id=\"test10\">\n  <div id=\"parent10\">\n    <div id=\"first10\">First</div>\n    <div id=\"last10\">Last</div>\n  </div>\n</div>\n\n<script id=\"test10-last-child\">\n  const last10 = $('#last10');\n  const parent10 = $('#parent10');\n\n  const new10 = document.createElement('div');\n  new10.id = 'new10';\n\n  last10.replaceWith(new10);\n\n  testing.expectEqual(null, document.getElementById('last10'));\n  testing.expectEqual(new10, parent10.lastElementChild);\n  testing.expectEqual(new10, parent10.children[1]);\n</script>\n\n<!-- Test 11: Replace with empty (no arguments) - effectively removes the element -->\n<div id=\"test11\">\n  <div id=\"parent11\">\n    <div id=\"old11\">Old</div>\n  </div>\n</div>\n\n<script id=\"test11-empty\">\n  const old11 = $('#old11');\n  const parent11 = $('#parent11');\n\n  testing.expectEqual(1, parent11.childElementCount);\n\n  // Calling replaceWith() with no args should just remove the element\n  old11.replaceWith();\n\n  // Element should be removed, leaving parent empty\n  testing.expectEqual(0, parent11.childElementCount);\n  testing.expectEqual(null, document.getElementById('old11'));\n  testing.expectEqual(null, old11.parentElement);\n</script>\n\n<!-- Test 12: Replace and check childElementCount updates -->\n<div id=\"test12\">\n  <div id=\"parent12\">\n    <div id=\"a12\">A</div>\n    <div id=\"b12\">B</div>\n    <div id=\"c12\">C</div>\n  </div>\n</div>\n\n<script id=\"test12-child-count\">\n  const b12 = $('#b12');\n  const parent12 = $('#parent12');\n\n  testing.expectEqual(3, parent12.childElementCount);\n\n  // Replace with 2 elements\n  const new12a = document.createElement('div');\n  new12a.id = 'new12a';\n  const new12b = document.createElement('div');\n  new12b.id = 'new12b';\n\n  b12.replaceWith(new12a, new12b);\n\n  testing.expectEqual(4, parent12.childElementCount);\n  testing.expectEqual(null, document.getElementById('b12'));\n</script>\n\n<!-- Test 13: Replace deeply nested element -->\n<div id=\"test13\">\n  <div id=\"l1\">\n    <div id=\"l2\">\n      <div id=\"l3\">\n        <div id=\"l4\">\n          <div id=\"old13\">Deep</div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n\n<script id=\"test13-deeply-nested\">\n  const old13 = $('#old13');\n  const l4 = $('#l4');\n\n  const new13 = document.createElement('div');\n  new13.id = 'new13';\n\n  old13.replaceWith(new13);\n\n  testing.expectEqual(null, document.getElementById('old13'));\n  testing.expectEqual(new13, document.getElementById('new13'));\n  testing.expectEqual(l4, new13.parentElement);\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/selector_invalid.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"container\">\n    <p>Test</p>\n</div>\n\n<script id=\"empty_functional\">\n{\n    const container = $('#container');\n\n    // Empty functional pseudo-classes should error\n    testing.expectError(\"Error: InvalidPseudoClass\", () => container.querySelector(':has()'));\n    testing.expectError(\"Error: InvalidPseudoClass\", () => container.querySelector(':not()'));\n    testing.expectError(\"Error: InvalidPseudoClass\", () => container.querySelector(':lang()'));\n}\n</script>\n\n<script id=\"invalid_patterns\">\n{\n    const container = $('#container');\n\n    // Invalid nth patterns\n    testing.expectError(\"Error: InvalidNthPattern\", () => container.querySelector(':nth-child(foo)'));\n    testing.expectError(\"Error: InvalidNthPattern\", () => container.querySelector(':nth-child(-)'));\n    testing.expectError(\"Error: InvalidNthPattern\", () => container.querySelector(':nth-child(+)'));\n}\n</script>\n\n<script id=\"unknown_pseudo\">\n{\n    const container = $('#container');\n\n    // Unknown pseudo-classes\n    testing.expectError(\"Error: UnknownPseudoClass\", () => container.querySelector(':unknown'));\n    testing.expectError(\"Error: UnknownPseudoClass\", () => container.querySelector(':not-a-real-pseudo'));\n    testing.expectError(\"Error: UnknownPseudoClass\", () => container.querySelector(':fake(test)'));\n}\n</script>\n\n<script id=\"empty_selector\">\n{\n    const container = $('#container');\n\n    // Empty selectors\n    testing.expectError(\"SyntaxError\", () => container.querySelector(''));\n    testing.expectError(\"SyntaxError\", () => document.querySelectorAll(''));\n}\n</script>\n\n<script id=\"invalid_combinators\">\n{\n    const container = $('#container');\n\n    // Combinators with nothing after\n    testing.expectError(\"Error: InvalidSelector\", () => container.querySelector('p >'));\n    testing.expectError(\"Error: InvalidSelector\", () => container.querySelector('p +'));\n    testing.expectError(\"Error: InvalidSelector\", () => container.querySelector('p ~'));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/styles.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"test-div\"></div>\n\n<script id=\"styles\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  testing.expectEqual(0, div.style.length);\n  testing.expectEqual('', div.style.cssText);\n\n  div.style.setProperty('color', 'red');\n  testing.expectEqual('red', div.style.getPropertyValue('color'));\n  testing.expectEqual('', div.style.getPropertyPriority('color'));\n  testing.expectEqual(1, div.style.length);\n\n  div.style.setProperty('background-color', 'blue', 'important');\n  testing.expectEqual('blue', div.style.getPropertyValue('background-color'));\n  testing.expectEqual('important', div.style.getPropertyPriority('background-color'));\n  testing.expectEqual(2, div.style.length);\n\n  testing.expectEqual('blue', div.style.getPropertyValue('BACKGROUND-COLOR'));\n  testing.expectEqual('blue', div.style.getPropertyValue('Background-Color'));\n\n  div.style.setProperty('font-size', '16px');\n  testing.expectEqual('16px', div.style.getPropertyValue('font-size'));\n  testing.expectEqual(3, div.style.length);\n\n  const cssText = div.style.cssText;\n  testing.expectEqual(true, cssText.includes('color: red;'));\n  testing.expectEqual(true, cssText.includes('background-color: blue !important;'));\n  testing.expectEqual(true, cssText.includes('font-size: 16px;'));\n\n  const removedValue = div.style.removeProperty('color');\n  testing.expectEqual('red', removedValue);\n  testing.expectEqual('', div.style.getPropertyValue('color'));\n  testing.expectEqual(2, div.style.length);\n\n  const notFound = div.style.removeProperty('non-existent');\n  testing.expectEqual('', notFound);\n  testing.expectEqual(2, div.style.length);\n\n  const prop0 = div.style.item(0);\n  const prop1 = div.style.item(1);\n  testing.expectEqual(true, prop0 === 'background-color' || prop0 === 'font-size');\n  testing.expectEqual(true, prop1 === 'background-color' || prop1 === 'font-size');\n  testing.expectEqual(true, prop0 !== prop1);\n  testing.expectEqual('', div.style.item(2));\n  testing.expectEqual('', div.style.item(-1));\n}\n</script>\n\n<script id=\"cssText\">\n{\n  const div = $('#test-div');\n\n  div.style.cssText = '';\n  testing.expectEqual(0, div.style.length);\n  testing.expectEqual('', div.style.cssText);\n\n  div.style.cssText = 'width: 100px; height: 50px !important; margin-top: 10px';\n  testing.expectEqual(3, div.style.length);\n  testing.expectEqual('100px', div.style.getPropertyValue('width'));\n  testing.expectEqual('50px', div.style.getPropertyValue('height'));\n  testing.expectEqual('10px', div.style.getPropertyValue('margin-top'));\n  testing.expectEqual('', div.style.getPropertyPriority('width'));\n  testing.expectEqual('important', div.style.getPropertyPriority('height'));\n  testing.expectEqual('', div.style.getPropertyPriority('margin-top'));\n\n  div.style.cssText = '  padding-left:  5px  ;  border-top-width: 1px  ';\n  testing.expectEqual(2, div.style.length);\n  testing.expectEqual('5px', div.style.getPropertyValue('padding-left'));\n  testing.expectEqual('1px', div.style.getPropertyValue('border-top-width'));\n\n  div.style.cssText = '';\n  testing.expectEqual(0, div.style.length);\n  testing.expectEqual('', div.style.cssText);\n\n  div.style.cssText = 'color: red;';\n  testing.expectEqual(1, div.style.length);\n  testing.expectEqual('red', div.style.getPropertyValue('color'));\n}\n</script>\n\n<script id=\"propertyUpdates\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('color', 'red');\n  testing.expectEqual('red', div.style.getPropertyValue('color'));\n  testing.expectEqual(1, div.style.length);\n\n  div.style.setProperty('color', 'blue');\n  testing.expectEqual('blue', div.style.getPropertyValue('color'));\n  testing.expectEqual(1, div.style.length);\n\n  div.style.setProperty('color', 'green', 'important');\n  testing.expectEqual('green', div.style.getPropertyValue('color'));\n  testing.expectEqual('important', div.style.getPropertyPriority('color'));\n  testing.expectEqual(1, div.style.length);\n\n  div.style.setProperty('color', 'yellow');\n  testing.expectEqual('yellow', div.style.getPropertyValue('color'));\n  testing.expectEqual('', div.style.getPropertyPriority('color'));\n  testing.expectEqual(1, div.style.length);\n\n  div.style.setProperty('color', '');\n  testing.expectEqual('', div.style.getPropertyValue('color'));\n  testing.expectEqual(0, div.style.length);\n}\n</script>\n\n<script id=\"invalidPriority\">\n{\n  const div = $('#test-div');\n  div.style.cssText = '';\n\n  div.style.setProperty('color', 'red', 'invalid');\n  testing.expectEqual('', div.style.getPropertyValue('color'));\n  testing.expectEqual(0, div.style.length);\n\n  div.style.setProperty('color', 'red', 'important');\n  testing.expectEqual('red', div.style.getPropertyValue('color'));\n  testing.expectEqual('important', div.style.getPropertyPriority('color'));\n}\n</script>\n\n<script id=\"defaultVisibilityProperties\">\n{\n  // Test that inline styles return empty string when not explicitly set\n  const div = document.createElement('div');\n  const span = document.createElement('span');\n  const anchor = document.createElement('a');\n\n  // Test that element.style returns empty for unset properties\n  testing.expectEqual('', div.style.getPropertyValue('display'));\n  testing.expectEqual('', span.style.getPropertyValue('display'));\n  testing.expectEqual('', anchor.style.getPropertyValue('display'));\n\n  // Test visibility - also returns empty when not set\n  testing.expectEqual('', div.style.getPropertyValue('visibility'));\n\n  // Test opacity - also returns empty when not set\n  testing.expectEqual('', div.style.getPropertyValue('opacity'));\n\n  // Test that explicit values can be set\n  div.style.setProperty('display', 'flex');\n  testing.expectEqual('flex', div.style.getPropertyValue('display'));\n  testing.expectEqual(1, div.style.length);\n\n  // Test that removing property returns to empty string\n  div.style.setProperty('display', '');\n  testing.expectEqual('', div.style.getPropertyValue('display'));\n  testing.expectEqual(0, div.style.length);\n\n  // Test that other properties also return empty when not set\n  testing.expectEqual('', div.style.getPropertyValue('color'));\n  testing.expectEqual('', div.style.getPropertyValue('width'));\n}\n</script>\n\n<script id=\"computedStyleDefaults\">\n{\n  // Test that getComputedStyle returns default visibility properties\n  const div = document.createElement('div');\n  const span = document.createElement('span');\n  const anchor = document.createElement('a');\n  document.body.appendChild(div);\n  document.body.appendChild(span);\n  document.body.appendChild(anchor);\n\n  // Test element.style returns empty when not explicitly set\n  testing.expectEqual('', div.style.display);\n  testing.expectEqual('', span.style.display);\n  testing.expectEqual('', anchor.style.display);\n  testing.expectEqual('', div.style.visibility);\n  testing.expectEqual('', div.style.opacity);\n\n  // Test getComputedStyle with getPropertyValue\n  const divStyle = window.getComputedStyle(div);\n  const spanStyle = window.getComputedStyle(span);\n  const anchorStyle = window.getComputedStyle(anchor);\n\n  testing.expectEqual('block', divStyle.getPropertyValue('display'));\n  testing.expectEqual('inline', spanStyle.getPropertyValue('display'));\n  testing.expectEqual('inline', anchorStyle.getPropertyValue('display'));\n  testing.expectEqual('visible', divStyle.getPropertyValue('visibility'));\n  testing.expectEqual('1', divStyle.getPropertyValue('opacity'));\n\n  // Test getComputedStyle with named property access (camelCase)\n  testing.expectEqual('block', divStyle.display);\n  testing.expectEqual('inline', spanStyle.display);\n  testing.expectEqual('inline', anchorStyle.display);\n  testing.expectEqual('visible', divStyle.visibility);\n  testing.expectEqual('1', divStyle.opacity);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/element/svg/svg.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<svg id=svg1></svg>\n<sVg id=svg2></sVg>\n<SVG id=svg3></SVG>\n\n<script id=svg>\n{\n  let svg1 = $('#svg1');\n  testing.expectEqual('svg', svg1.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', svg1.namespaceURI);\n\n  let svg2 = $('#svg2');\n  testing.expectEqual('svg', svg2.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', svg2.namespaceURI);\n\n  let svg3 = $('#svg3');\n  testing.expectEqual('svg', svg3.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', svg3.namespaceURI);\n\n  const svg4 = document.createElementNS('http://www.w3.org/2000/svg', 'SvG');\n  testing.expectEqual('SvG', svg4.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', svg4.namespaceURI);\n\n  const svg5 = document.createElement('SvG');\n  testing.expectEqual('SVG', svg5.tagName);\n  testing.expectEqual('http://www.w3.org/1999/xhtml', svg5.namespaceURI);\n}\n</script>\n\n<svg id=lower width=\"200\" height=\"100\" style=\"border:1px solid #ccc\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 200 100\">\n  <rect></rect>\n  <text x=\"100\" y=\"95\" font-size=\"14\" text-anchor=\"middle\">OVER 9000!!</text>\n</svg>\n\n<SVG ID=UPPER WIDTH=\"200\" HEIGHT=\"100\" STYLE=\"BORDER:1PX SOLID #CCC\" XMLNS=\"http://www.w3.org/2000/svg\" VIEWBOX=\"0 0 200 100\">\n  <RECT></RECT>\n  <TEXT X=\"100\" Y=\"95\" FONT-SIZE=\"14\" TEXT-ANCHOR=\"MIDDLE\">OVER 9000!!!</TEXT>\n</SVG>\n\n<script id=casing>\n  testing.expectEqual(false, 'AString' instanceof SVGElement);\n\n  const lower = $('#lower');\n  testing.expectEqual('http://www.w3.org/2000/svg', lower.getAttribute('xmlns'));\n  testing.expectEqual('http://www.w3.org/2000/svg', lower.getAttributeNode('xmlns').value);\n  testing.expectEqual('http://www.w3.org/2000/svg', lower.attributes.getNamedItem('xmlns').value);\n  testing.expectEqual('0 0 200 100', lower.getAttribute('viewBox'));\n  testing.expectEqual('viewBox', lower.getAttributeNode('viewBox').name);\n  testing.expectEqual(true, lower.outerHTML.includes('viewBox'));\n  testing.expectEqual('svg', lower.tagName);\n  testing.expectEqual('rect', lower.querySelector('rect').tagName);\n  testing.expectEqual('text', lower.querySelector('text').tagName);\n\n  const upper = $('#UPPER');\n  testing.expectEqual('http://www.w3.org/2000/svg', upper.getAttribute('xmlns'));\n  testing.expectEqual('http://www.w3.org/2000/svg', upper.getAttributeNode('xmlns').value);\n  testing.expectEqual('http://www.w3.org/2000/svg', upper.attributes.getNamedItem('xmlns').value);\n  testing.expectEqual('0 0 200 100', upper.getAttribute('viewBox'));\n  testing.expectEqual('viewBox', upper.getAttributeNode('viewBox').name);\n  testing.expectEqual(true, upper.outerHTML.includes('viewBox'));\n  testing.expectEqual('svg', upper.tagName);\n  testing.expectEqual('rect', upper.querySelector('rect').tagName);\n  testing.expectEqual('text', upper.querySelector('text').tagName);\n</script>\n"
  },
  {
    "path": "src/browser/tests/encoding/text_decoder.html",
    "content": "<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n\n<script src=\"../testing.js\"></script>\n<script id=decoder>\n  let d1 = new TextDecoder();\n  testing.expectEqual('utf-8', d1.encoding);\n  testing.expectEqual(false, d1.fatal);\n  testing.expectEqual(false, d1.ignoreBOM);\n\n  testing.expectEqual('', d1.decode());\n  testing.expectEqual('香料', d1.decode(new Uint8Array([233, 166, 153, 230, 150, 153])));\n  testing.expectEqual('香料', d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 233, 166, 153, 230, 150, 153])));\n  testing.expectEqual('�4', d1.decode(new Uint8Array([249, 52])));\n\n  {\n    const buffer = new ArrayBuffer(6);\n    const ints = new Uint8Array(buffer)\n    ints[0] = 233;\n    ints[1] = 166;\n    ints[2] = 153;\n    ints[3] = 230;\n    ints[4] = 150;\n    ints[5] = 153;\n    testing.expectEqual('香料', d1.decode(buffer));\n  }\n\n  {\n    const buffer = new ArrayBuffer(6);\n    const dv = new DataView(buffer);\n    dv.setUint8(0, 233);\n    dv.setUint8(1, 166);\n    dv.setUint8(2, 153);\n    dv.setUint8(3, 230);\n    dv.setUint8(4, 150);\n    dv.setUint8(5, 153);\n    testing.expectEqual('香料', d1.decode(dv));\n  }\n\n  let d2 = new TextDecoder('utf8', {fatal: true})\n  testing.expectError('Error: InvalidUtf8', () => {\n    let data  = new Uint8Array([241, 241, 159, 172]);\n    d2.decode(data);\n  });\n</script>\n\n<script id=stream>\n  let d3 = new TextDecoder();\n  testing.expectEqual('', d2.decode(new Uint8Array([226, 153]), { stream: true }));\n  testing.expectEqual('♥', d2.decode(new Uint8Array([165]), { stream: true }));\n</script>\n\n<script id=slice>\n  const buf1 = new ArrayBuffer(7);\n  const arr1 = new Uint8Array(buf1)\n  arr1[0] = 80;\n  arr1[1] = 81;\n  arr1[2] = 82;\n  arr1[3] = 83;\n  arr1[4] = 84;\n  arr1[5] = 85;\n  arr1[6] = 86;\n  testing.expectEqual('RST', d3.decode(new Uint8Array(buf1, 2, 3)));\n</script>\n"
  },
  {
    "path": "src/browser/tests/encoding/text_encoder.html",
    "content": "<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n<script src=\"../testing.js\"></script>\n\n<script id=TextEncoder>\n  var encoder = new TextEncoder();\n  testing.expectEqual('utf-8', encoder.encoding);\n  testing.expectEqual([226, 130, 172], Array.from(encoder.encode('€')));\n  testing.expectEqual([111,118,101,114,32,57,48,48,48], encoder.encode(\"over 9000\"));\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/abort_controller.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=abortControllerBasic>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n\n  testing.expectEqual(false, signal.aborted);\n  testing.expectEqual(undefined, signal.reason);\n}\n</script>\n\n<script id=abortControllerAbort>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n\n  controller.abort();\n\n  testing.expectEqual(true, signal.aborted);\n}\n</script>\n\n<script id=abortControllerAbortWithReason>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n\n  controller.abort(\"Custom abort reason\");\n\n  testing.expectEqual(true, signal.aborted);\n  testing.expectEqual(\"Custom abort reason\", signal.reason);\n}\n</script>\n\n<script id=abortControllerEventListener>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n  let eventFired = false;\n\n  signal.addEventListener('abort', (event) => {\n    eventFired = true;\n    testing.expectEqual('abort', event.type);\n    testing.expectEqual(signal, event.target);\n  });\n\n  controller.abort();\n  testing.expectEqual(true, eventFired);\n}\n</script>\n\n<script id=abortControllerOnabortProperty>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n  let onabortFired = false;\n\n  signal.onabort = (event) => {\n    onabortFired = true;\n    testing.expectEqual('abort', event.type);\n  };\n\n  controller.abort();\n\n  testing.expectEqual(true, onabortFired);\n}\n</script>\n\n<script id=abortControllerMultipleAborts>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n  let eventCount = 0;\n\n  signal.onabort = () => {\n    eventCount++;\n  };\n\n  controller.abort(\"First\");\n  controller.abort(\"Second\");\n  controller.abort(\"Third\");\n\n  testing.expectEqual(1, eventCount);\n  testing.expectEqual(\"First\", signal.reason);\n}\n</script>\n\n<script id=abortControllerBothEventTypes>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n  let listenerFired = false;\n  let onabortFired = false;\n\n  signal.addEventListener('abort', () => {\n    listenerFired = true;\n  });\n\n  signal.onabort = () => {\n    onabortFired = true;\n  };\n\n  controller.abort();\n\n  testing.expectEqual(true, listenerFired);\n  testing.expectEqual(true, onabortFired);\n}\n</script>\n\n<script id=abortSignalStaticAbort>\n{\n  const signal = AbortSignal.abort(\"Already aborted\");\n\n  testing.expectEqual(true, signal.aborted);\n  testing.expectEqual(\"Already aborted\", signal.reason);\n}\n</script>\n\n<script id=abortSignalStaticAbortNoReason>\n{\n  const signal = AbortSignal.abort();\n\n  testing.expectEqual(true, signal.aborted);\n  testing.expectEqual(\"AbortError\", signal.reason);\n}\n</script>\n\n<script id=abortControllerThrowIfAborted>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n\n  // Should not throw when not aborted\n  signal.throwIfAborted();\n\n  controller.abort();\n\n  // Should throw after abort\n  let didThrow = false;\n  try {\n    signal.throwIfAborted();\n  } catch (e) {\n    didThrow = true;\n  }\n\n  testing.expectEqual(true, didThrow);\n}\n</script>\n\n<script id=addEventListenerWithSignal>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n  let eventFired = false;\n\n  window.addEventListener('custom', () => {\n    eventFired = true;\n  }, { signal });\n\n  // Trigger event before abort - should fire\n  window.dispatchEvent(new Event('custom'));\n  testing.expectEqual(true, eventFired);\n\n  // Reset and abort\n  eventFired = false;\n  controller.abort();\n\n  // Trigger event after abort - should not fire\n  window.dispatchEvent(new Event('custom'));\n  testing.expectEqual(false, eventFired);\n}\n</script>\n\n<script id=addEventListenerWithAlreadyAbortedSignal>\n{\n  const abortedSignal = AbortSignal.abort();\n  let eventFired = false;\n\n  // Try to add listener with already-aborted signal\n  window.addEventListener('test', () => {\n    eventFired = true;\n  }, { signal: abortedSignal });\n\n  // Trigger event - should not fire because signal was already aborted\n  window.dispatchEvent(new Event('test'));\n  testing.expectEqual(false, eventFired);\n}\n</script>\n\n<script id=addEventListenerSignalRemovesListener>\n{\n  const controller = new AbortController();\n  const signal = controller.signal;\n  let count = 0;\n\n  window.addEventListener('increment', () => {\n    count++;\n  }, { signal });\n\n  window.dispatchEvent(new Event('increment'));\n  testing.expectEqual(1, count);\n\n  window.dispatchEvent(new Event('increment'));\n  testing.expectEqual(2, count);\n\n  controller.abort();\n\n  window.dispatchEvent(new Event('increment'));\n  testing.expectEqual(2, count); // Still 2, listener was removed\n}\n</script>\n\n<script id=legacy1>\n  var a1 = new AbortController();\n\n  var s1 = a1.signal;\n  testing.expectEqual(undefined, s1.throwIfAborted());\n  testing.expectEqual(undefined, s1.reason);\n\n  let target;;\n  let called = 0;\n  s1.addEventListener('abort', (e) => {\n    called += 1;\n    target = e.target;\n  });\n\n  a1.abort();\n  testing.expectEqual(true, s1.aborted)\n  testing.expectEqual(s1, target)\n  testing.expectEqual('AbortError', s1.reason)\n  testing.expectEqual(1, called)\n</script>\n\n<script id=abortsignal_abort>\n  var s2 = AbortSignal.abort('over 9000');\n  testing.expectEqual(true, s2.aborted);\n  testing.expectEqual('over 9000', s2.reason);\n  testing.expectEqual('AbortError', AbortSignal.abort().reason);\n</script>\n\n<script id=abortsignal_timeout>\n  var s3 = AbortSignal.timeout(10);\n  testing.eventually(() => {\n    testing.expectEqual(true, s3.aborted);\n    testing.expectEqual('TimeoutError', s3.reason);\n    testing.expectError('Error: TimeoutError', () => {\n      s3.throwIfAborted()\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/composition.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=noNata>\n  {\n    let event = new CompositionEvent(\"test\", {});\n    testing.expectEqual(true, event instanceof CompositionEvent);\n    testing.expectEqual(true, event instanceof Event);\n\n    testing.expectEqual(\"test\", event.type);\n    testing.expectEqual(\"\", event.data);\n  }\n</script>\n\n<script id=withData>\n  {\n    let event = new CompositionEvent(\"test2\", {data: \"over 9000!\"});\n    testing.expectEqual(\"test2\", event.type);\n    testing.expectEqual(\"over 9000!\", event.data);\n  }\n</script>\n\n<script id=dispatch>\n  {\n    let called = 0;\n    document.addEventListener('CE', (e) => {\n      testing.expectEqual('test-data', e.data);\n      testing.expectEqual(true, e instanceof CompositionEvent);\n      called += 1\n    });\n\n    document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));\n    testing.expectEqual(1, called);\n  }\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/event/custom_event.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=customEventConstructor>\n{\n  const event = new CustomEvent('test');\n  testing.expectEqual('test', event.type);\n  testing.expectEqual(null, event.detail);\n  testing.expectEqual(false, event.bubbles);\n  testing.expectEqual(false, event.cancelable);\n}\n</script>\n\n<script id=customEventWithDetail>\n{\n  const detail = { foo: 'bar', num: 42 };\n  const event = new CustomEvent('custom', { detail });\n  testing.expectEqual('custom', event.type);\n  testing.expectEqual('bar', event.detail.foo);\n  testing.expectEqual(42, event.detail.num);\n}\n</script>\n\n<script id=customEventWithOptions>\n{\n  const event = new CustomEvent('opts', {\n    bubbles: true,\n    cancelable: true,\n    detail: 'test-detail'\n  });\n  testing.expectEqual(true, event.bubbles);\n  testing.expectEqual(true, event.cancelable);\n  testing.expectEqual('test-detail', event.detail);\n}\n</script>\n\n<script id=customEventDispatch>\n{\n  const target = document.createElement('div');\n  let receivedEvent = null;\n\n  target.addEventListener('myevent', (e) => {\n    receivedEvent = e;\n  });\n\n  const event = new CustomEvent('myevent', {\n    detail: { message: 'hello' }\n  });\n  target.dispatchEvent(event);\n\n  testing.expectEqual('hello', receivedEvent.detail.message);\n}\n</script>\n\n<script id=createEventCustomEvent>\n{\n  const event = document.createEvent('CustomEvent');\n  testing.expectEqual('', event.type);\n  testing.expectEqual(false, event.bubbles);\n  testing.expectEqual(false, event.cancelable);\n  testing.expectEqual(null, event.detail);\n\n  event.initCustomEvent('tea', true, true);\n  testing.expectEqual('tea', event.type);\n  testing.expectEqual(true, event.bubbles);\n  testing.expectEqual(true, event.cancelable);\n}\n</script>\n\n<script id=createEventCaseInsensitive>\n{\n  const event1 = document.createEvent('customevent');\n  testing.expectEqual('', event1.type);\n\n  const event2 = document.createEvent('CUSTOMEVENT');\n  testing.expectEqual('', event2.type);\n\n  const event3 = document.createEvent('CustomEvents');\n  testing.expectEqual('', event3.type);\n}\n</script>\n\n<script id=createEventGeneric>\n{\n  const event1 = document.createEvent('Event');\n  testing.expectEqual('', event1.type);\n\n  const event2 = document.createEvent('Events');\n  testing.expectEqual('', event2.type);\n\n  const event3 = document.createEvent('HTMLEvents');\n  testing.expectEqual('', event3.type);\n}\n</script>\n\n<script id=customEventInheritance>\n{\n  const event = new CustomEvent('inherit-test');\n  testing.expectEqual(Event.NONE, event.eventPhase);\n  testing.expectEqual(false, event.defaultPrevented);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/error.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=errorEventConstructor>\n  const evt = new ErrorEvent('error', {\n    message: 'Test error message',\n    filename: 'test.js',\n    lineno: 42,\n    colno: 10\n  });\n\n  testing.expectEqual('error', evt.type);\n  testing.expectEqual('Test error message', evt.message);\n  testing.expectEqual('test.js', evt.filename);\n  testing.expectEqual(42, evt.lineno);\n  testing.expectEqual(10, evt.colno);\n  testing.expectEqual(false, evt.bubbles);\n  testing.expectEqual(false, evt.cancelable);\n</script>\n\n<script id=errorEventWithBubbles>\n  const evt2 = new ErrorEvent('error', {\n    message: 'Bubbling error',\n    bubbles: true,\n    cancelable: true\n  });\n\n  testing.expectEqual(true, evt2.bubbles);\n  testing.expectEqual(true, evt2.cancelable);\n</script>\n\n<script id=errorEventWithErrorObject>\n  const err = new Error('Original error');\n  const evt3 = new ErrorEvent('error', {\n    message: 'Wrapper error',\n    error: err\n  });\n\n  testing.expectEqual('Wrapper error', evt3.message);\n  testing.expectEqual(err, evt3.error);\n</script>\n\n<script id=errorEventDefaults>\n  const evt4 = new ErrorEvent('error');\n\n  testing.expectEqual('', evt4.message);\n  testing.expectEqual('', evt4.filename);\n  testing.expectEqual(0, evt4.lineno);\n  testing.expectEqual(0, evt4.colno);\n  testing.expectEqual(false, evt4.bubbles);\n  testing.expectEqual(false, evt4.cancelable);\n</script>\n\n<script id=errorEventInheritance>\n  const evt5 = new ErrorEvent('test');\n\n  testing.expectEqual('test', evt5.type);\n  testing.expectEqual(Event.NONE, evt5.eventPhase);\n  testing.expectEqual(false, evt5.defaultPrevented);\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/focus.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=default>\n  let event = new FocusEvent('focus');\n  testing.expectEqual('focus', event.type);\n  testing.expectEqual(true, event instanceof FocusEvent);\n  testing.expectEqual(true, event instanceof UIEvent);\n  testing.expectEqual(true, event instanceof Event);\n  testing.expectEqual(null, event.relatedTarget);\n</script>\n\n<script id=parameters>\n  let div = document.createElement('div');\n  let focusEvent = new FocusEvent('blur', { relatedTarget: div });\n  testing.expectEqual(div, focusEvent.relatedTarget);\n</script>\n\n<script id=createEvent>\n  let evt = document.createEvent('focusevent');\n  testing.expectEqual(true, evt instanceof FocusEvent);\n  testing.expectEqual(true, evt instanceof UIEvent);\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/keyboard.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=default>\n  let event = new KeyboardEvent(\"test\", { key: \"a\" });\n  testing.expectEqual(true, event instanceof KeyboardEvent);\n  testing.expectEqual(true, event instanceof Event);\n\n  testing.expectEqual(\"test\", event.type);\n  testing.expectEqual(\"a\", event.key);\n\n  testing.expectEqual(0, event.location);\n  testing.expectEqual(false, event.repeat);\n  testing.expectEqual(false, event.isComposing);\n\n  testing.expectEqual(false, event.ctrlKey);\n  testing.expectEqual(false, event.shiftKey);\n  testing.expectEqual(false, event.metaKey);\n  testing.expectEqual(false, event.altKey);\n</script>\n\n<script id=getModifierState>\n  event = new KeyboardEvent(\"test\", {\n    altKey: true,\n    shiftKey: true,\n    metaKey: true,\n    ctrlKey: true,\n  });\n\n  testing.expectEqual(true, event.getModifierState(\"Alt\"));\n  testing.expectEqual(true, event.getModifierState(\"AltGraph\"));\n  testing.expectEqual(true, event.getModifierState(\"Control\"));\n  testing.expectEqual(true, event.getModifierState(\"Shift\"));\n  testing.expectEqual(true, event.getModifierState(\"Meta\"));\n  testing.expectEqual(true, event.getModifierState(\"Accel\"));\n</script>\n\n<script id=keyDownListener>\n  event = new KeyboardEvent(\"keydown\", { key: \"z\" });\n  let isKeyDown = false;\n\n  document.addEventListener(\"keydown\", (e) => {\n    isKeyDown = true;\n\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n    testing.expectEqual(true, e instanceof UIEvent);\n    testing.expectEqual(true, e instanceof Event);\n    testing.expectEqual(\"z\", event.key);\n  });\n\n  document.dispatchEvent(event);\n\n  testing.expectEqual(true, isKeyDown);\n</script>\n\n<script id=keyUpListener>\n  event = new KeyboardEvent(\"keyup\", { key: \"x\" });\n  let isKeyUp = false;\n\n  document.addEventListener(\"keyup\", (e) => {\n    isKeyUp = true;\n\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n    testing.expectEqual(true, e instanceof UIEvent);\n    testing.expectEqual(true, e instanceof Event);\n    testing.expectEqual(\"x\", event.key);\n  });\n\n  document.dispatchEvent(event);\n\n  testing.expectEqual(true, isKeyUp);\n</script>\n\n<script id=keyPressListener>\n  event = new KeyboardEvent(\"keypress\", { key: \"w\" });\n  let isKeyPress = false;\n\n  document.addEventListener(\"keypress\", (e) => {\n    isKeyPress = true;\n\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n    testing.expectEqual(true, e instanceof UIEvent);\n    testing.expectEqual(true, e instanceof Event);\n    testing.expectEqual(\"w\", event.key);\n  });\n\n  document.dispatchEvent(event);\n\n  testing.expectEqual(true, isKeyPress);\n</script>\n\n<script id=isTrusted>\n  // Test isTrusted on KeyboardEvent\n  let keyEvent = new KeyboardEvent('keydown', {key: 'a'});\n  testing.expectEqual(false, keyEvent.isTrusted);\n\n  // Test isTrusted on dispatched KeyboardEvent\n  let keyIsTrusted = null;\n  document.addEventListener('keytest', (e) => {\n    keyIsTrusted = e.isTrusted;\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n  });\n  document.dispatchEvent(new KeyboardEvent('keytest', {key: 'b'}));\n  testing.expectEqual(false, keyIsTrusted);\n</script>\n\n<script id=non_keyboard_keydown>\n  // this used to crash\n  {\n    let called = false;\n    const div = document.createElement('div')\n    div.addEventListener('keydown', () => {\n      called = true;\n    });\n    div.dispatchEvent(new Event('keydown'));\n    testing.expectEqual(true, called);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/listener_removal.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=\"removeListenerDuringDispatch\">\nconst target = document.createElement(\"div\");\n\nlet listener1Called = 0;\nlet listener2Called = 0;\nlet listener3Called = 0;\n\nfunction listener1() {\n    listener1Called++;\n    console.warn(\"listener1 called, removing listener2 and adding listener3\");\n    target.removeEventListener(\"foo\", listener2);\n    target.addEventListener(\"foo\", listener3);\n}\n\nfunction listener2() {\n    listener2Called++;\n    console.warn(\"listener2 called (SHOULD NOT HAPPEN)\");\n}\n\nfunction listener3() {\n    listener3Called++;\n    console.warn(\"listener3 called (SHOULD NOT HAPPEN IN FIRST DISPATCH)\");\n}\n\ntarget.addEventListener(\"foo\", listener1);\ntarget.addEventListener(\"foo\", listener2);\n\nconsole.warn(\"Dispatching first event\");\ntarget.dispatchEvent(new Event(\"foo\"));\n\nconsole.warn(\"After first dispatch:\");\nconsole.warn(\"  listener1Called:\", listener1Called);\nconsole.warn(\"  listener2Called:\", listener2Called);\nconsole.warn(\"  listener3Called:\", listener3Called);\n\ntesting.expectEqual(1, listener1Called);\ntesting.expectEqual(0, listener2Called);\ntesting.expectEqual(0, listener3Called);\n\nconsole.warn(\"Dispatching second event\");\ntarget.dispatchEvent(new Event(\"foo\"));\n\nconsole.warn(\"After second dispatch:\");\nconsole.warn(\"  listener1Called:\", listener1Called);\nconsole.warn(\"  listener2Called:\", listener2Called);\nconsole.warn(\"  listener3Called:\", listener3Called);\n\ntesting.expectEqual(2, listener1Called);\ntesting.expectEqual(0, listener2Called);\ntesting.expectEqual(1, listener3Called);\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/message.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=messageEventConstructor>\n{\n  const evt = new MessageEvent('message', {\n    data: { foo: 'bar', count: 42 },\n    origin: 'https://example.com',\n    source: window\n  });\n\n  testing.expectEqual('message', evt.type);\n  testing.expectEqual({ foo: 'bar', count: 42 }, evt.data);\n  testing.expectEqual('https://example.com', evt.origin);\n  testing.expectEqual(window, evt.source);\n  testing.expectEqual(false, evt.bubbles);\n  testing.expectEqual(false, evt.cancelable);\n}\n</script>\n\n<script id=messageEventWithString>\n{\n  const evt2 = new MessageEvent('message', {\n    data: 'Hello, World!',\n    origin: 'https://test.com'\n  });\n\n  testing.expectEqual('Hello, World!', evt2.data);\n  testing.expectEqual('https://test.com', evt2.origin);\n}\n</script>\n\n<script id=messageEventDefaults>\n{\n  const evt3 = new MessageEvent('message');\n\n  testing.expectEqual('', evt3.origin);\n  testing.expectEqual(false, evt3.bubbles);\n  testing.expectEqual(false, evt3.cancelable);\n}\n</script>\n\n<script id=messageEventInheritance>\n{\n  const evt4 = new MessageEvent('custom');\n\n  testing.expectEqual('custom', evt4.type);\n  testing.expectEqual(Event.NONE, evt4.eventPhase);\n  testing.expectEqual(false, evt4.defaultPrevented);\n}\n</script>\n\n<script id=postMessageBasic>\n{\n  let receivedEvent = null;\n\n  const handler = (e) => {\n    receivedEvent = e;\n  };\n  window.addEventListener('message', handler, { once: true });\n\n  window.postMessage('test data', '*');\n\n  testing.eventually(() => {\n    testing.expectEqual('test data', receivedEvent.data);\n    testing.expectEqual(window, receivedEvent.source);\n    testing.expectEqual('message', receivedEvent.type);\n  });\n}\n</script>\n\n<script id=postMessageWithObject>\n{\n  let receivedData = null;\n\n  const handler = (e) => {\n    receivedData = e.data;\n  };\n  window.addEventListener('message', handler, { once: true });\n\n  const testObj = { type: 'test', value: 123, nested: { key: 'value' } };\n  window.postMessage(testObj, '*');\n\n  testing.eventually(() => {\n    testing.expectEqual(testObj, receivedData);\n  });\n}\n</script>\n\n<script id=messageEventWithBubbles>\n{\n  const evt = new MessageEvent('message', {\n    data: 'test',\n    bubbles: true,\n    cancelable: true\n  });\n\n  testing.expectEqual(true, evt.bubbles);\n  testing.expectEqual(true, evt.cancelable);\n}\n</script>\n\n<script id=postMessageWithNumber>\n{\n  let received = null;\n\n  const handler = (e) => {\n    received = e.data;\n  };\n  window.addEventListener('message', handler, { once: true });\n\n  window.postMessage(42, '*');\n\n  testing.eventually(() => {\n    testing.expectEqual(42, received);\n  });\n}\n</script>\n\n<script id=postMessageWithArray>\n{\n  let received = null;\n\n  const handler = (e) => {\n    received = e.data;\n  };\n  window.addEventListener('message', handler, { once: true });\n\n  const arr = [1, 2, 3, 'test'];\n  window.postMessage(arr, '*');\n\n  testing.eventually(() => {\n    testing.expectEqual(arr, received);\n  });\n}\n</script>\n\n<script id=postMessageWithNull>\n{\n  let received = undefined;\n\n  const handler = (e) => {\n    received = e.data;\n  };\n  window.addEventListener('message', handler, { once: true });\n\n  window.postMessage(null, '*');\n\n  testing.eventually(() => {\n    testing.expectEqual(null, received);\n  });\n}\n</script>\n\n<script id=messageEventOriginFromLocation>\n{\n  let receivedOrigin = null;\n\n  const handler = (e) => {\n    receivedOrigin = e.origin;\n  };\n  window.addEventListener('message', handler, { once: true });\n\n  window.postMessage('test', '*');\n\n  testing.eventually(() => {\n    testing.expectEqual('http://127.0.0.1:9582', receivedOrigin);\n  });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/message_multiple_listeners.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=postMessageMultipleListeners>\n{\n  let count = 0;\n\n  const handler1 = () => { count++; };\n  const handler2 = () => { count++; };\n  window.addEventListener('message', handler1, { once: true });\n  window.addEventListener('message', handler2, { once: true });\n\n  window.postMessage('trigger', '*');\n\n  testing.eventually(() => {\n    testing.expectEqual(2, count);\n  });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/mouse.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=default>\n  let event = new MouseEvent('click');\n  testing.expectEqual('click', event.type);\n  testing.expectEqual(true, event instanceof MouseEvent);\n  testing.expectEqual(true, event instanceof UIEvent);\n  testing.expectEqual(true, event instanceof Event);\n  testing.expectEqual(0, event.clientX);\n  testing.expectEqual(0, event.clientY);\n  testing.expectEqual(0, event.screenX);\n  testing.expectEqual(0, event.screenY);\n  testing.expectEqual(0, event.buttons);\n</script>\n\n<script id=parameters>\n  let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20, screenX: 200, screenY: 500, buttons: 5 });\n  testing.expectEqual(0, new_event.button);\n  testing.expectEqual(5, new_event.buttons);\n  testing.expectEqual(10, new_event.x);\n  testing.expectEqual(20, new_event.y);\n  testing.expectEqual(10, new_event.pageX);\n  testing.expectEqual(20, new_event.pageY);\n  testing.expectEqual(200, new_event.screenX);\n  testing.expectEqual(500, new_event.screenY);\n</script>\n\n<script id=listener>\n  let me = new MouseEvent('click');\n  testing.expectEqual(true, me instanceof Event);\n\n  var evt = null;\n  document.addEventListener('click', function (e) {\n    evt = e;\n  });\n  document.dispatchEvent(me);\n  testing.expectEqual('click', evt.type);\n  testing.expectEqual(true, evt instanceof MouseEvent);\n</script>\n\n<script id=isTrusted>\n  // Test isTrusted on MouseEvent\n  let mouseEvent = new MouseEvent('click');\n  testing.expectEqual(false, mouseEvent.isTrusted);\n\n  // Test isTrusted on dispatched MouseEvent\n  let mouseIsTrusted = null;\n  document.addEventListener('mousetest', (e) => {\n    mouseIsTrusted = e.isTrusted;\n    testing.expectEqual(true, e instanceof MouseEvent);\n  });\n  document.dispatchEvent(new MouseEvent('mousetest'));\n  testing.expectEqual(false, mouseIsTrusted);\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/pointer.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=default>\n{\n  let event = new PointerEvent('pointerdown');\n  testing.expectEqual('pointerdown', event.type);\n  testing.expectEqual(true, event instanceof PointerEvent);\n  testing.expectEqual(true, event instanceof MouseEvent);\n  testing.expectEqual(true, event instanceof UIEvent);\n  testing.expectEqual(true, event instanceof Event);\n  testing.expectEqual(0, event.pointerId);\n  testing.expectEqual('', event.pointerType);\n  testing.expectEqual(1.0, event.width);\n  testing.expectEqual(1.0, event.height);\n  testing.expectEqual(0.0, event.pressure);\n  testing.expectEqual(0.0, event.tangentialPressure);\n  testing.expectEqual(0, event.tiltX);\n  testing.expectEqual(0, event.tiltY);\n  testing.expectEqual(0, event.twist);\n  testing.expectEqual(false, event.isPrimary);\n}\n</script>\n\n<script id=parameters>\n{\n  let new_event = new PointerEvent('pointerdown', {\n    pointerId: 42,\n    pointerType: 'pen',\n    width: 10.5,\n    height: 20.5,\n    pressure: 0.75,\n    tangentialPressure: -0.25,\n    tiltX: 30,\n    tiltY: 45,\n    twist: 90,\n    isPrimary: true,\n    clientX: 100,\n    clientY: 200,\n    screenX: 300,\n    screenY: 400\n  });\n  testing.expectEqual(42, new_event.pointerId);\n  testing.expectEqual('pen', new_event.pointerType);\n  testing.expectEqual(10.5, new_event.width);\n  testing.expectEqual(20.5, new_event.height);\n  testing.expectEqual(0.75, new_event.pressure);\n  testing.expectEqual(-0.25, new_event.tangentialPressure);\n  testing.expectEqual(30, new_event.tiltX);\n  testing.expectEqual(45, new_event.tiltY);\n  testing.expectEqual(90, new_event.twist);\n  testing.expectEqual(true, new_event.isPrimary);\n  testing.expectEqual(100, new_event.clientX);\n  testing.expectEqual(200, new_event.clientY);\n  testing.expectEqual(300, new_event.screenX);\n  testing.expectEqual(400, new_event.screenY);\n}\n</script>\n\n<script id=mousePointerType>\n{\n  let mouse_event = new PointerEvent('pointerdown', { pointerType: 'mouse' });\n  testing.expectEqual('mouse', mouse_event.pointerType);\n}\n</script>\n\n<script id=touchPointerType>\n{\n  let touch_event = new PointerEvent('pointerdown', { pointerType: 'touch', pointerId: 1, pressure: 0.5 });\n  testing.expectEqual('touch', touch_event.pointerType);\n  testing.expectEqual(1, touch_event.pointerId);\n  testing.expectEqual(0.5, touch_event.pressure);\n}\n</script>\n\n<script id=listener>\n{\n  let pe = new PointerEvent('pointerdown', { pointerId: 123 });\n  testing.expectEqual(true, pe instanceof PointerEvent);\n  testing.expectEqual(true, pe instanceof MouseEvent);\n  testing.expectEqual(true, pe instanceof Event);\n\n  var evt = null;\n  document.addEventListener('pointerdown', function (e) {\n    evt = e;\n  });\n  document.dispatchEvent(pe);\n  testing.expectEqual('pointerdown', evt.type);\n  testing.expectEqual(true, evt instanceof PointerEvent);\n  testing.expectEqual(123, evt.pointerId);\n}\n</script>\n\n<script id=isTrusted>\n{\n  let pointerEvent = new PointerEvent('pointerup');\n  testing.expectEqual(false, pointerEvent.isTrusted);\n\n  let pointerIsTrusted = null;\n  document.addEventListener('pointertest', (e) => {\n    pointerIsTrusted = e.isTrusted;\n    testing.expectEqual(true, e instanceof PointerEvent);\n  });\n  document.dispatchEvent(new PointerEvent('pointertest'));\n  testing.expectEqual(false, pointerIsTrusted);\n}\n</script>\n\n<script id=eventTypes>\n{\n  let down = new PointerEvent('pointerdown');\n  testing.expectEqual('pointerdown', down.type);\n\n  let up = new PointerEvent('pointerup');\n  testing.expectEqual('pointerup', up.type);\n\n  let move = new PointerEvent('pointermove');\n  testing.expectEqual('pointermove', move.type);\n\n  let enter = new PointerEvent('pointerenter');\n  testing.expectEqual('pointerenter', enter.type);\n\n  let leave = new PointerEvent('pointerleave');\n  testing.expectEqual('pointerleave', leave.type);\n\n  let over = new PointerEvent('pointerover');\n  testing.expectEqual('pointerover', over.type);\n\n  let out = new PointerEvent('pointerout');\n  testing.expectEqual('pointerout', out.type);\n\n  let cancel = new PointerEvent('pointercancel');\n  testing.expectEqual('pointercancel', cancel.type);\n}\n</script>\n\n<script id=inheritedMouseProperties>\n{\n  let pe = new PointerEvent('pointerdown', {\n    button: 2,\n    buttons: 4,\n    altKey: true,\n    ctrlKey: true,\n    shiftKey: true,\n    metaKey: true\n  });\n  testing.expectEqual(2, pe.button);\n  testing.expectEqual(true, pe.altKey);\n  testing.expectEqual(true, pe.ctrlKey);\n  testing.expectEqual(true, pe.shiftKey);\n  testing.expectEqual(true, pe.metaKey);\n}\n</script>\n\n<script id=inheritedUIEventProperties>\n{\n  let pe = new PointerEvent('pointerdown', {\n    detail: 5,\n    bubbles: true,\n    cancelable: true\n  });\n  testing.expectEqual(5, pe.detail);\n  testing.expectEqual(true, pe.bubbles);\n  testing.expectEqual(true, pe.cancelable);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/promise_rejection.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=project_rejection>\n  {\n    let e1 = new PromiseRejectionEvent(\"rejectionhandled\");\n    testing.expectEqual(true, e1 instanceof PromiseRejectionEvent);\n    testing.expectEqual(true, e1 instanceof Event);\n\n    testing.expectEqual(\"rejectionhandled\", e1.type);\n    testing.expectEqual(null, e1.reason);\n    testing.expectEqual(null, e1.promise);\n\n    let e2 = new PromiseRejectionEvent(\"rejectionhandled\", {reason: ['tea']});\n    testing.expectEqual(true, e2 instanceof PromiseRejectionEvent);\n    testing.expectEqual(true, e2 instanceof Event);\n\n    testing.expectEqual(\"rejectionhandled\", e2.type);\n    testing.expectEqual(['tea'], e2.reason);\n    testing.expectEqual(null, e2.promise);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/report_error.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=onerrorFiveArguments>\n  let called = false;\n  let argCount = 0;\n  window.onerror = function() {\n    called = true;\n    argCount = arguments.length;\n    return true; // suppress default\n  };\n  try { undefinedVariable; } catch(e) { window.reportError(e); }\n  testing.expectEqual(true, called);\n  testing.expectEqual(5, argCount);\n  window.onerror = null;\n</script>\n\n<script id=onerrorCalledBeforeEventListener>\n  let callOrder = [];\n  window.onerror = function() { callOrder.push('onerror'); return true; };\n  window.addEventListener('error', function() { callOrder.push('listener'); });\n  try { undefinedVariable; } catch(e) { window.reportError(e); }\n  testing.expectEqual('onerror', callOrder[0]);\n  testing.expectEqual('listener', callOrder[1]);\n  window.onerror = null;\n</script>\n\n<script id=onerrorReturnTrueSuppresses>\n  let listenerCalled = false;\n  window.onerror = function() { return true; };\n  window.addEventListener('error', function(e) {\n    // listener still fires even when onerror returns true\n    listenerCalled = true;\n  });\n  try { undefinedVariable; } catch(e) { window.reportError(e); }\n  testing.expectEqual(true, listenerCalled);\n  window.onerror = null;\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/text.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=createEvent>\n  let evt = document.createEvent('TextEvent');\n  testing.expectEqual(true, evt instanceof TextEvent);\n  testing.expectEqual(true, evt instanceof UIEvent);\n  testing.expectEqual('', evt.data);\n</script>\n\n<script id=initTextEvent>\n  let textEvent = document.createEvent('TextEvent');\n  textEvent.initTextEvent('textInput', true, false, window, 'test data');\n  testing.expectEqual('textInput', textEvent.type);\n  testing.expectEqual('test data', textEvent.data);\n  testing.expectEqual(true, textEvent.bubbles);\n  testing.expectEqual(false, textEvent.cancelable);\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/ui.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=uiEventConstructor>\n    const evt = new UIEvent('click', {\n      detail: 5,\n      bubbles: true,\n      cancelable: true\n    });\n    testing.expectEqual('click', evt.type);\n    testing.expectEqual(5, evt.detail);\n    testing.expectEqual(true, evt.bubbles);\n    testing.expectEqual(true, evt.cancelable);\n    testing.expectEqual(window, evt.view);\n</script>\n\n<script id=uiEventWithView>\n    const evt2 = new UIEvent('mousedown', {\n      detail: 2,\n      view: window\n    });\n    testing.expectEqual('mousedown', evt2.type);\n    testing.expectEqual(2, evt2.detail);\n    testing.expectEqual(window, evt2.view);\n</script>\n\n<script id=uiEventDefaults>\n    const evt3 = new UIEvent('focus');\n    testing.expectEqual('focus', evt3.type);\n    testing.expectEqual(0, evt3.detail);\n    testing.expectEqual(false, evt3.bubbles);\n    testing.expectEqual(false, evt3.cancelable);\n    testing.expectEqual(window, evt3.view);\n</script>\n\n<script id=uiEventInheritance>\n    const evt4 = new UIEvent('blur', { detail: 1 });\n    testing.expectEqual('blur', evt4.type);\n    testing.expectEqual(Event.NONE, evt4.eventPhase);\n    testing.expectEqual(false, evt4.defaultPrevented);\n    testing.expectEqual(1, evt4.detail);\n</script>\n\n<script id=uiEventDetailTypes>\n    const evt5 = new UIEvent('custom', { detail: 0 });\n    const evt6 = new UIEvent('custom', { detail: 100 });\n    testing.expectEqual(0, evt5.detail);\n    testing.expectEqual(100, evt6.detail);\n</script>\n\n<script id=uiEventDetailTypes2>\n    const evt7 = new UIEvent('custom', { bubbles: true });\n    testing.expectEqual(0, evt7.detail);\n</script>\n"
  },
  {
    "path": "src/browser/tests/event/wheel.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=default>\n  let event = new WheelEvent('wheel');\n  testing.expectEqual('wheel', event.type);\n  testing.expectEqual(true, event instanceof WheelEvent);\n  testing.expectEqual(true, event instanceof MouseEvent);\n  testing.expectEqual(true, event instanceof UIEvent);\n  testing.expectEqual(0, event.deltaX);\n  testing.expectEqual(0, event.deltaY);\n  testing.expectEqual(0, event.deltaZ);\n  testing.expectEqual(0, event.deltaMode);\n</script>\n\n<script id=parameters>\n  let wheelEvent = new WheelEvent('wheel', {\n    deltaX: 10,\n    deltaY: 20,\n    deltaMode: WheelEvent.DOM_DELTA_LINE\n  });\n  testing.expectEqual(10, wheelEvent.deltaX);\n  testing.expectEqual(20, wheelEvent.deltaY);\n  testing.expectEqual(1, wheelEvent.deltaMode);\n</script>\n\n<script id=constants>\n  testing.expectEqual(0, WheelEvent.DOM_DELTA_PIXEL);\n  testing.expectEqual(1, WheelEvent.DOM_DELTA_LINE);\n  testing.expectEqual(2, WheelEvent.DOM_DELTA_PAGE);\n</script>\n"
  },
  {
    "path": "src/browser/tests/events.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<div id=parent><p id=child></p></div>\n\n<script id=addEventListener>\n  let child_calls = 0;\n  let parent_calls = 0;\n  let child = $('#child');\n  let parent = $('#parent');\n\n  child.dispatchEvent(new Event('Hello'));\n  parent.dispatchEvent(new Event('Hello'));\n  testing.expectEqual(0, child_calls);\n  testing.expectEqual(0, parent_calls);\n\n  const child_func = (e) => {\n    testing.expectEqual('hello', e.type);\n    child_calls += 1;\n  };\n  child.addEventListener('hello', child_func);\n\n  const parent_func = (e) => {\n    testing.expectEqual('hello', e.type);\n    parent_calls += 1;\n  };\n  parent.addEventListener('hello', parent_func);\n\n  parent.dispatchEvent(new Event('hello'));\n  testing.expectEqual(0, child_calls);\n  testing.expectEqual(1, parent_calls);\n\n  parent.dispatchEvent(new Event('Click'));\n  testing.expectEqual(0, child_calls);\n  testing.expectEqual(1, parent_calls);\n\n  child.dispatchEvent(new Event('hello'));\n  testing.expectEqual(1, child_calls);\n  testing.expectEqual(1, parent_calls);\n\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(2, child_calls);\n  testing.expectEqual(2, parent_calls);\n\n  child.removeEventListener('other', () => {})\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(3, child_calls);\n  testing.expectEqual(3, parent_calls);\n\n  child.removeEventListener('hello', () => {})\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(4, child_calls);\n  testing.expectEqual(4, parent_calls);\n\n  // capture is on, still won't remove\n  child.removeEventListener('hello', child_func, true)\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(5, child_calls);\n  testing.expectEqual(5, parent_calls);\n\n  child.removeEventListener('hello', child_func, false)\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(5, child_calls);\n  testing.expectEqual(6, parent_calls);\n\n  // wrong func\n  parent.removeEventListener('hello', child_func, false)\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(5, child_calls);\n  testing.expectEqual(7, parent_calls);\n\n  parent.removeEventListener('hello', parent_func, false)\n  child.dispatchEvent(new Event('hello', {bubbles: true}));\n  testing.expectEqual(5, child_calls);\n  testing.expectEqual(7, parent_calls);\n</script>\n\n<div id=parent2><div id=child2></div></div>\n<script id=currentTarget>\n  child_calls = 0;\n  parent_calls = 0;\n  child = $('#child2');\n  parent = $('#parent2');\n\n  child.addEventListener('test', (e) => {\n    child_calls += 1;\n    testing.expectEqual(child, e.target);\n    testing.expectEqual(child, e.currentTarget);\n  })\n\n  parent.addEventListener('test', (e) => {\n    parent_calls += 1;\n    testing.expectEqual(child, e.target);\n    testing.expectEqual(parent, e.currentTarget);\n  });\n\n  child.dispatchEvent(new Event('test', {bubbles: true}));\n  testing.expectEqual(1, child_calls);\n  testing.expectEqual(1, parent_calls);\n</script>\n\n<div id=parent3></div>\n<script id=dupe>\n  parent_calls = 0;\n  parent = $('#parent3');\n\n  const fn1 = function() {\n    parent_calls += 100;\n  }\n  const fn2 = function() {\n    parent_calls += 1;\n  }\n  parent.addEventListener('dupe', fn1);\n  parent.addEventListener('dupe', fn1);\n  parent.addEventListener('dupe', fn1);\n  parent.addEventListener('dupe', fn2);\n\n  parent.dispatchEvent(new Event('dupe'));\n  testing.expectEqual(101, parent_calls);\n</script>\n\n<div id=parent4><div id=child4></div></div>\n<script id=stpoPropagation>\n  child_calls = 0;\n  parent_calls = 0;\n  child = $('#child4');\n  parent = $('#parent4');\n\n  parent.addEventListener('propagate', function() {\n    parent_calls += 1;\n  });\n\n  child.addEventListener('propagate', function(e) {\n    child_calls += 1;\n    if (child_calls == 2) {\n      e.stopPropagation();\n    }\n  });\n\n  testing.expectEqual(true, child.dispatchEvent(new Event('propagate', {bubbles: true})));\n  testing.expectEqual(true, child.dispatchEvent(new Event('propagate', {bubbles: true})));\n  testing.expectEqual(true, child.dispatchEvent(new Event('propagate', {bubbles: true})));\n  testing.expectEqual(2, parent_calls);\n  testing.expectEqual(3, child_calls);\n</script>\n\n<div id=grandparent><div id=parent5><div id=child5></div></div></div>\n<script id=eventCapture>\n  // Test event capture phase\n  const order = [];\n  const grandparent = $('#grandparent');\n  const parent5 = $('#parent5');\n  const child5 = $('#child5');\n\n  // Add capture listeners (should fire top-down)\n  grandparent.addEventListener('test', () => order.push('grandparent-capture'), true);\n  parent5.addEventListener('test', () => order.push('parent-capture'), true);\n  child5.addEventListener('test', () => order.push('child-capture'), true);\n\n  // Add bubble listeners (should fire bottom-up)\n  grandparent.addEventListener('test', () => order.push('grandparent-bubble'), false);\n  parent5.addEventListener('test', () => order.push('parent-bubble'), false);\n  child5.addEventListener('test', () => order.push('child-bubble'), false);\n\n  child5.dispatchEvent(new Event('test', {bubbles: true}));\n\n  // Expected order: capture top-down, then target (both), then bubble bottom-up\n  testing.expectEqual('grandparent-capture', order[0]);\n  testing.expectEqual('parent-capture', order[1]);\n  testing.expectEqual('child-capture', order[2]);\n  testing.expectEqual('child-bubble', order[3]);\n  testing.expectEqual('parent-bubble', order[4]);\n  testing.expectEqual('grandparent-bubble', order[5]);\n  testing.expectEqual(6, order.length);\n</script>\n\n<div id=parent6><div id=child6></div></div>\n<script id=eventPhase>\n  // Test eventPhase property\n  const phases = [];\n  const parent6 = $('#parent6');\n  const child6 = $('#child6');\n\n  parent6.addEventListener('phase', (e) => {\n    phases.push({target: 'parent', phase: e.eventPhase, current: e.currentTarget === parent6});\n  }, true);\n\n  child6.addEventListener('phase', (e) => {\n    phases.push({target: 'child', phase: e.eventPhase, current: e.currentTarget === child6});\n  }, true);\n\n  child6.addEventListener('phase', (e) => {\n    phases.push({target: 'child', phase: e.eventPhase, current: e.currentTarget === child6});\n  }, false);\n\n  parent6.addEventListener('phase', (e) => {\n    phases.push({target: 'parent', phase: e.eventPhase, current: e.currentTarget === parent6});\n  }, false);\n\n  child6.dispatchEvent(new Event('phase', {bubbles: true}));\n\n  testing.expectEqual('parent', phases[0].target);\n  testing.expectEqual(Event.CAPTURING_PHASE, phases[0].phase);\n  testing.expectEqual(true, phases[0].current);\n\n  testing.expectEqual('child', phases[1].target);\n  testing.expectEqual(Event.AT_TARGET, phases[1].phase);\n  testing.expectEqual(true, phases[1].current);\n\n  testing.expectEqual('child', phases[2].target);\n  testing.expectEqual(Event.AT_TARGET, phases[2].phase);\n  testing.expectEqual(true, phases[2].current);\n\n  testing.expectEqual('parent', phases[3].target);\n  testing.expectEqual(Event.BUBBLING_PHASE, phases[3].phase);\n  testing.expectEqual(true, phases[3].current);\n</script>\n\n<div id=parent7><div id=child7></div></div>\n<script id=stopImmediatePropagation>\n  // Test stopImmediatePropagation\n  let calls = [];\n  const parent7 = $('#parent7');\n  const child7 = $('#child7');\n\n  child7.addEventListener('immediate', () => calls.push(1));\n  child7.addEventListener('immediate', (e) => {\n    calls.push(2);\n    e.stopImmediatePropagation();\n  });\n  child7.addEventListener('immediate', () => calls.push(3));\n  parent7.addEventListener('immediate', () => calls.push(4));\n\n  child7.dispatchEvent(new Event('immediate', {bubbles: true}));\n\n  // Should only call the first two listeners, then stop\n  testing.expectEqual(1, calls[0]);\n  testing.expectEqual(2, calls[1]);\n  testing.expectEqual(2, calls.length);\n</script>\n\n<div id=parent8><div id=child8></div></div>\n<script id=captureStopPropagation>\n  // Test that stopPropagation during capture prevents target and bubble\n  let capture_calls = [];\n  const parent8 = $('#parent8');\n  const child8 = $('#child8');\n\n  parent8.addEventListener('capture-stop', (e) => {\n    capture_calls.push('parent-capture');\n    e.stopPropagation();\n  }, true);\n\n  child8.addEventListener('capture-stop', () => capture_calls.push('child-capture'), true);\n  child8.addEventListener('capture-stop', () => capture_calls.push('child-bubble'), false);\n  parent8.addEventListener('capture-stop', () => capture_calls.push('parent-bubble'), false);\n\n  child8.dispatchEvent(new Event('capture-stop', {bubbles: true}));\n\n  // Should only call parent capture listener\n  testing.expectEqual(1, capture_calls.length);\n  testing.expectEqual('parent-capture', capture_calls[0]);\n</script>\n\n<div id=child9></div>\n<script id=nonBubblingNoCapture>\n  // Test that non-bubbling events still have capture phase but no bubble phase\n  let non_bubble_calls = [];\n  const child9 = $('#child9');\n\n  window.addEventListener('nobubble', (e) => {\n    non_bubble_calls.push('window-capture');\n    testing.expectEqual(Event.CAPTURING_PHASE, e.eventPhase);\n  }, true);\n  window.addEventListener('nobubble', () => non_bubble_calls.push('window-bubble'), false);\n  child9.addEventListener('nobubble', (e) => {\n    non_bubble_calls.push('child');\n    testing.expectEqual(Event.AT_TARGET, e.eventPhase);\n  });\n\n  child9.dispatchEvent(new Event('nobubble', {bubbles: false}));\n\n  // Should call window-capture (during capture phase) and child, but NOT window-bubble\n  testing.expectEqual(2, non_bubble_calls.length);\n  testing.expectEqual('window-capture', non_bubble_calls[0]);\n  testing.expectEqual('child', non_bubble_calls[1]);\n</script>\n\n<script id=nullCallback>\n  // Test that null callback is handled gracefully\n  let nullTestPassed = false;\n  window.addEventListener('testnull', null);\n  window.removeEventListener('testnull', null);\n  nullTestPassed = true;\n  testing.expectEqual(true, nullTestPassed);\n</script>\n\n<script id=objectCallback>\n  // Test object with handleEvent method\n  let handleEventCalls = 0;\n  const handler = {\n    handleEvent: function(e) {\n      handleEventCalls += 1;\n      testing.expectEqual('customhandler', e.type);\n    }\n  };\n\n  window.addEventListener('customhandler', handler);\n  window.dispatchEvent(new Event('customhandler'));\n  testing.expectEqual(1, handleEventCalls);\n\n  window.dispatchEvent(new Event('customhandler'));\n  testing.expectEqual(2, handleEventCalls);\n\n  // Remove using the same object\n  window.removeEventListener('customhandler', handler);\n  window.dispatchEvent(new Event('customhandler'));\n  testing.expectEqual(2, handleEventCalls); // Should not increment\n</script>\n\n<script id=objectWithoutHandleEvent>\n  // Test that registration succeeds even with invalid handlers\n  // (Dispatch behavior differs: spec-compliant browsers throw, some ignore)\n  const badHandler = { foo: 'bar' };\n  let registrationSucceeded = false;\n  window.addEventListener('testbad', badHandler);\n  registrationSucceeded = true;\n  testing.expectEqual(true, registrationSucceeded);\n\n  // Test object with handleEvent that's not a function\n  const badHandler2 = { handleEvent: 'not a function' };\n  let registrationSucceeded2 = false;\n  window.addEventListener('testbad2', badHandler2);\n  registrationSucceeded2 = true;\n  testing.expectEqual(true, registrationSucceeded2);\n</script>\n\n<script id=passiveDetection>\n  // Test passive event listener detection pattern (common in polyfills)\n  let passiveSupported = false;\n  try {\n    const opts = {};\n    Object.defineProperty(opts, 'passive', {\n      get: function() {\n        passiveSupported = true;\n      }\n    });\n    window.addEventListener('testpassive', opts, opts);\n    window.removeEventListener('testpassive', opts, opts);\n  } catch (e) {\n    passiveSupported = false;\n  }\n\n  testing.expectEqual(true, passiveSupported);\n</script>\n\n<div id=reentrancy_test></div>\n<script id=removeOtherListenerDuringDispatch>\n  // Test that removing another listener during dispatch doesn't crash\n  // This reproduces the segfault bug where n.next becomes invalid\n  const reentrancy_el = $('#reentrancy_test');\n  let reentrancy_calls = [];\n\n  const listener1 = () => {\n    reentrancy_calls.push(1);\n  };\n\n  const listener2 = () => {\n    reentrancy_calls.push(2);\n    // Remove listener3 while we're still iterating the listener list\n    reentrancy_el.removeEventListener('reentrancy', listener3);\n  };\n\n  const listener3 = () => {\n    reentrancy_calls.push(3);\n  };\n\n  const listener4 = () => {\n    reentrancy_calls.push(4);\n  };\n\n  reentrancy_el.addEventListener('reentrancy', listener1);\n  reentrancy_el.addEventListener('reentrancy', listener2);\n  reentrancy_el.addEventListener('reentrancy', listener3);\n  reentrancy_el.addEventListener('reentrancy', listener4);\n\n  reentrancy_el.dispatchEvent(new Event('reentrancy'));\n\n  // listener3 was removed during dispatch by listener2, so it should not fire\n  // But listener4 should still fire\n  testing.expectEqual(1, reentrancy_calls[0]);\n  testing.expectEqual(2, reentrancy_calls[1]);\n  testing.expectEqual(4, reentrancy_calls[2]);\n  testing.expectEqual(3, reentrancy_calls.length);\n</script>\n\n<div id=self_remove_test></div>\n<script id=removeSelfDuringDispatch>\n  // Test that a listener can remove itself during dispatch\n  const self_remove_el = $('#self_remove_test');\n  let self_remove_calls = [];\n\n  const selfRemovingListener = () => {\n    self_remove_calls.push('self');\n    self_remove_el.removeEventListener('selfremove', selfRemovingListener);\n  };\n\n  const otherListener = () => {\n    self_remove_calls.push('other');\n  };\n\n  self_remove_el.addEventListener('selfremove', selfRemovingListener);\n  self_remove_el.addEventListener('selfremove', otherListener);\n\n  // First dispatch - selfRemovingListener should fire and remove itself\n  self_remove_el.dispatchEvent(new Event('selfremove'));\n  testing.expectEqual('self', self_remove_calls[0]);\n  testing.expectEqual('other', self_remove_calls[1]);\n  testing.expectEqual(2, self_remove_calls.length);\n\n  // Second dispatch - only otherListener should fire\n  self_remove_el.dispatchEvent(new Event('selfremove'));\n  testing.expectEqual('other', self_remove_calls[2]);\n  testing.expectEqual(3, self_remove_calls.length);\n</script>\n\n<div id=multi_remove_test></div>\n<script id=removeMultipleListenersDuringDispatch>\n  // Test removing multiple listeners during dispatch (stress test)\n  const multi_el = $('#multi_remove_test');\n  let multi_calls = [];\n\n  const listeners = [];\n  for (let i = 0; i < 10; i++) {\n    const listener = () => {\n      multi_calls.push(i);\n      // Each even-numbered listener removes the next two listeners\n      if (i % 2 === 0 && listeners[i + 1] && listeners[i + 2]) {\n        multi_el.removeEventListener('multiremove', listeners[i + 1]);\n        multi_el.removeEventListener('multiremove', listeners[i + 2]);\n      }\n    };\n    listeners.push(listener);\n    multi_el.addEventListener('multiremove', listener);\n  }\n\n  multi_el.dispatchEvent(new Event('multiremove'));\n\n  // Should see: 0 (removes 1,2), 3 (but 1,2 already removed), 4 (removes 5,6), 7 (but 5,6 already removed), 8 (doesn't remove because listeners[10] doesn't exist), 9 (not removed)\n  // Expected: 0, 3, 4, 7, 8, 9\n  testing.expectEqual(0, multi_calls[0]);\n  testing.expectEqual(3, multi_calls[1]);\n  testing.expectEqual(4, multi_calls[2]);\n  testing.expectEqual(7, multi_calls[3]);\n  testing.expectEqual(8, multi_calls[4]);\n  testing.expectEqual(9, multi_calls[5]);\n  testing.expectEqual(6, multi_calls.length);\n</script>\n\n<div id=nested_dispatch_test></div>\n<script id=nestedDispatchWithRemoval>\n  // Test nested event dispatch with listener removal\n  const nested_el = $('#nested_dispatch_test');\n  let nested_calls = [];\n\n  const inner1 = () => {\n    nested_calls.push('inner1');\n  };\n\n  const inner2 = () => {\n    nested_calls.push('inner2');\n  };\n\n  const outer = () => {\n    nested_calls.push('outer-start');\n    // Dispatch another event in the middle of handling this one\n    nested_el.dispatchEvent(new Event('inner'));\n    nested_calls.push('outer-end');\n    // Remove a listener after the nested dispatch\n    nested_el.removeEventListener('inner', inner2);\n  };\n\n  nested_el.addEventListener('outer', outer);\n  nested_el.addEventListener('inner', inner1);\n  nested_el.addEventListener('inner', inner2);\n\n  nested_el.dispatchEvent(new Event('outer'));\n\n  // Should see outer-start, then both inner listeners, then outer-end\n  testing.expectEqual('outer-start', nested_calls[0]);\n  testing.expectEqual('inner1', nested_calls[1]);\n  testing.expectEqual('inner2', nested_calls[2]);\n  testing.expectEqual('outer-end', nested_calls[3]);\n\n  // Dispatch inner again - inner2 should be gone\n  nested_el.dispatchEvent(new Event('inner'));\n  testing.expectEqual('inner1', nested_calls[4]);\n  testing.expectEqual(5, nested_calls.length);\n</script>\n\n<div id=\"content\"><p id=para></p></div>\n<script id=event_target>\n  {\n    testing.expectEqual('[object EventTarget]', new EventTarget().toString());\n\n    let content = $('#content');\n    let para = $('#para');\n\n    var nb = 0;\n    var evt;\n    var phase;\n    var cur;\n\n    function reset() {\n      nb = 0;\n      evt = undefined;\n      phase = undefined;\n      cur = undefined;\n    }\n\n    function cbk(event) {\n      evt = event;\n      phase = event.eventPhase;\n      cur = event.currentTarget;\n      nb++;\n    }\n\n    content.addEventListener('basic', cbk);\n    content.dispatchEvent(new Event('basic'));\n    testing.expectEqual(1, nb);\n    testing.expectEqual(true, evt instanceof Event);\n    testing.expectEqual('basic', evt.type);\n    testing.expectEqual(2, phase);\n    testing.expectEqual('content', cur.getAttribute('id'));\n\n    reset();\n    para.dispatchEvent(new Event('basic'))\n\n    // handler is not called, no capture, not the targeno bubbling\n    testing.expectEqual(0, nb);\n    testing.expectEqual(undefined, evt);\n\n    reset();\n    content.addEventListener('basic', cbk);\n    content.dispatchEvent(new Event('basic'))\n    testing.expectEqual(1, nb);\n\n    reset();\n    content.addEventListener('basic', cbk, true);\n    content.dispatchEvent(new Event('basic'));\n    testing.expectEqual(2, nb);\n\n    reset()\n    content.removeEventListener('basic', cbk);\n    content.dispatchEvent(new Event('basic'));\n    testing.expectEqual(1, nb);\n\n    reset();\n    content.removeEventListener('basic', cbk, {capture: true});\n    content.dispatchEvent(new Event('basic'));\n    testing.expectEqual(0, nb);\n\n    reset();\n    content.addEventListener('capture', cbk, true);\n    content.dispatchEvent(new Event('capture'));\n    testing.expectEqual(1, nb);\n    testing.expectEqual(true, evt instanceof Event);\n    testing.expectEqual('capture', evt.type);\n    testing.expectEqual(2, phase);\n    testing.expectEqual('content', cur.getAttribute('id'));\n\n    reset();\n    para.dispatchEvent(new Event('capture'));\n    testing.expectEqual(1, nb);\n    testing.expectEqual(true, evt instanceof Event);\n    testing.expectEqual('capture', evt.type);\n    testing.expectEqual(1, phase);\n    testing.expectEqual('content', cur.getAttribute('id'));\n\n    reset();\n    content.addEventListener('bubbles', cbk);\n    content.dispatchEvent(new Event('bubbles', {bubbles: true}));\n    testing.expectEqual(1, nb);\n    testing.expectEqual(true, evt instanceof Event);\n    testing.expectEqual('bubbles', evt.type);\n    testing.expectEqual(2, phase);\n    testing.expectEqual('content', cur.getAttribute('id'));\n\n    reset();\n    para.dispatchEvent(new Event('bubbles', {bubbles: true}));\n    testing.expectEqual(1, nb);\n    testing.expectEqual(true, evt instanceof Event);\n    testing.expectEqual('bubbles', evt.type);\n    testing.expectEqual(3, phase);\n    testing.expectEqual('content', cur.getAttribute('id'));\n\n\n    const obj1 = {\n      calls: 0,\n      handleEvent: function() { this.calls += 1 }\n    };\n    content.addEventListener('he', obj1);\n    content.dispatchEvent(new Event('he'));\n    testing.expectEqual(1, obj1.calls);\n\n    content.removeEventListener('he', obj1);\n    content.dispatchEvent(new Event('he'));\n    testing.expectEqual(1, obj1.calls);\n\n    // doesn't crash on null receiver\n    content.addEventListener('he2', null);\n    content.dispatchEvent(new Event('he2'));\n  }\n</script>\n\n<script id=isTrusted>\n  // Test isTrusted property on generic Event\n  let untrustedEvent = new Event('test');\n  testing.expectEqual(false, untrustedEvent.isTrusted);\n\n  // Test isTrusted on dispatched events\n  let isTrustedValue = null;\n  document.addEventListener('trusttest', (e) => {\n    isTrustedValue = e.isTrusted;\n  });\n  document.dispatchEvent(new Event('trusttest'));\n  testing.expectEqual(false, isTrustedValue);\n\n  // Test isTrusted with bubbling events\n  let bubbledEvent = new Event('bubble', {bubbles: true});\n  testing.expectEqual(false, bubbledEvent.isTrusted);\n</script>\n\n<script id=emptyMessageEvent>\n  // https://github.com/lightpanda-io/browser/pull/1316\n  testing.expectError('TypeError', () => MessageEvent(''));\n</script>\n\n<div id=inline_parent><div id=inline_child></div></div>\n<script id=inlineHandlerReceivesEvent>\n  // Test that inline onclick handler receives the event object\n  {\n    const inline_child = $('#inline_child');\n    let receivedType = null;\n    let receivedTarget = null;\n    let receivedCurrentTarget = null;\n\n    inline_child.onclick = function(e) {\n      // Capture values DURING handler execution\n      receivedType = e.type;\n      receivedTarget = e.target;\n      receivedCurrentTarget = e.currentTarget;\n    };\n\n    inline_child.click();\n\n    testing.expectEqual('click', receivedType);\n    testing.expectEqual(inline_child, receivedTarget);\n    testing.expectEqual(inline_child, receivedCurrentTarget);\n  }\n</script>\n\n<div id=inline_order_parent><div id=inline_order_child></div></div>\n<script id=inlineHandlerOrder>\n  // Test that inline handler executes in proper order with addEventListener\n  {\n    const inline_order_child = $('#inline_order_child');\n    const inline_order_parent = $('#inline_order_parent');\n    const order = [];\n\n    // Capture listener on parent\n    inline_order_parent.addEventListener('click', () => order.push('parent-capture'), true);\n\n    // Inline handler on child (should execute at target phase)\n    inline_order_child.onclick = () => order.push('child-onclick');\n\n    // addEventListener on child (should execute at target phase, after onclick)\n    inline_order_child.addEventListener('click', () => order.push('child-listener'));\n\n    // Bubble listener on parent\n    inline_order_parent.addEventListener('click', () => order.push('parent-bubble'));\n\n    inline_order_child.click();\n\n    // Expected order: capture, then onclick, then addEventListener, then bubble\n    testing.expectEqual('parent-capture', order[0]);\n    testing.expectEqual('child-onclick', order[1]);\n    testing.expectEqual('child-listener', order[2]);\n    testing.expectEqual('parent-bubble', order[3]);\n    testing.expectEqual(4, order.length);\n  }\n</script>\n\n<div id=inline_prevent><div id=inline_prevent_child></div></div>\n<script id=inlineHandlerPreventDefault>\n  // Test that inline handler can preventDefault and it affects addEventListener listeners\n  {\n    const inline_prevent_child = $('#inline_prevent_child');\n    let preventDefaultCalled = false;\n    let listenerSawPrevented = false;\n\n    inline_prevent_child.onclick = function(e) {\n      e.preventDefault();\n      preventDefaultCalled = true;\n    };\n\n    inline_prevent_child.addEventListener('click', (e) => {\n      listenerSawPrevented = e.defaultPrevented;\n    });\n\n    const result = inline_prevent_child.dispatchEvent(new MouseEvent('click', {\n      bubbles: true,\n      cancelable: true\n    }));\n\n    testing.expectEqual(true, preventDefaultCalled);\n    testing.expectEqual(true, listenerSawPrevented);\n    testing.expectEqual(false, result); // dispatchEvent returns false when prevented\n  }\n</script>\n\n<div id=inline_stop_parent><div id=inline_stop_child></div></div>\n<script id=inlineHandlerStopPropagation>\n  // Test that inline handler can stopPropagation\n  {\n    const inline_stop_child = $('#inline_stop_child');\n    const inline_stop_parent = $('#inline_stop_parent');\n    let childCalled = false;\n    let parentCalled = false;\n\n    inline_stop_child.onclick = function(e) {\n      childCalled = true;\n      e.stopPropagation();\n    };\n\n    inline_stop_parent.addEventListener('click', () => {\n      parentCalled = true;\n    });\n\n    inline_stop_child.click();\n\n    testing.expectEqual(true, childCalled);\n    testing.expectEqual(false, parentCalled); // Should not bubble to parent\n  }\n</script>\n\n<div id=inline_replace_test></div>\n<script id=inlineHandlerReplacement>\n  // Test that setting onclick property replaces previous handler\n  {\n    const inline_replace_test = $('#inline_replace_test');\n    let calls = [];\n\n    inline_replace_test.onclick = () => calls.push('first');\n    inline_replace_test.click();\n\n    inline_replace_test.onclick = () => calls.push('second');\n    inline_replace_test.click();\n\n    testing.expectEqual('first', calls[0]);\n    testing.expectEqual('second', calls[1]);\n    testing.expectEqual(2, calls.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/file.html",
    "content": "<!DOCTYPE html>\n<head id=\"the_head\">\n  <title>Test Document Title</title>\n  <script src=\"./testing.js\"></script>\n</head>\n\n<script id=file>\n  const file = new File();\n\n  testing.expectEqual(true, file instanceof File);\n  testing.expectEqual(true, file instanceof Blob);\n</script>\n"
  },
  {
    "path": "src/browser/tests/file_reader.html",
    "content": "<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n<script src=\"./testing.js\"></script>\n\n<script id=basic>\n  {\n    const reader = new FileReader();\n    testing.expectEqual(0, reader.readyState);\n    testing.expectEqual(null, reader.result);\n    testing.expectEqual(null, reader.error);\n  }\n\n  // Constants\n  testing.expectEqual(0, FileReader.EMPTY);\n  testing.expectEqual(1, FileReader.LOADING);\n  testing.expectEqual(2, FileReader.DONE);\n</script>\n\n<script id=readAsText>\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob = new Blob([\"Hello, World!\"], { type: \"text/plain\" });\n\n    let loadstartFired = false;\n    let progressFired = false;\n    let loadFired = false;\n    let loadendFired = false;\n\n    const promise = new Promise((resolve) => {\n      reader.onloadstart = function(e) {\n        loadstartFired = true;\n        testing.expectEqual(\"loadstart\", e.type);\n        testing.expectEqual(1, reader.readyState);\n      };\n\n      reader.onprogress = function(e) {\n        progressFired = true;\n        testing.expectEqual(13, e.loaded);\n        testing.expectEqual(13, e.total);\n      };\n\n      reader.onload = function(e) {\n        loadFired = true;\n        testing.expectEqual(2, reader.readyState);\n        testing.expectEqual(\"Hello, World!\", reader.result);\n      };\n\n      reader.onloadend = function(e) {\n        loadendFired = true;\n        testing.expectEqual(true, loadstartFired);\n        testing.expectEqual(true, progressFired);\n        testing.expectEqual(true, loadFired);\n        resolve();\n      };\n    });\n\n    reader.readAsText(blob);\n    await promise;\n  });\n</script>\n\n<script id=readAsDataURL>\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob = new Blob([\"test\"], { type: \"text/plain\" });\n\n    const promise = new Promise((resolve) => {\n      reader.onload = function() {\n        testing.expectEqual(\"data:text/plain;base64,dGVzdA==\", reader.result);\n        resolve();\n      };\n    });\n\n    reader.readAsDataURL(blob);\n    await promise;\n  });\n\n  // Empty MIME type\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob = new Blob([\"test\"]);\n\n    const promise = new Promise((resolve) => {\n      reader.onload = function() {\n        testing.expectEqual(\"data:application/octet-stream;base64,dGVzdA==\", reader.result);\n        resolve();\n      };\n    });\n\n    reader.readAsDataURL(blob);\n    await promise;\n  });\n</script>\n\n<script id=readAsArrayBuffer>\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob = new Blob([new Uint8Array([65, 66, 67])]);\n\n    const promise = new Promise((resolve) => {\n      reader.onload = function() {\n        const result = reader.result;\n        testing.expectEqual(true, result instanceof ArrayBuffer);\n        testing.expectEqual(3, result.byteLength);\n\n        const view = new Uint8Array(result);\n        testing.expectEqual(65, view[0]);\n        testing.expectEqual(66, view[1]);\n        testing.expectEqual(67, view[2]);\n        resolve();\n      };\n    });\n\n    reader.readAsArrayBuffer(blob);\n    await promise;\n  });\n</script>\n\n<script id=readAsBinaryString>\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob = new Blob([\"ABC\"]);\n\n    const promise = new Promise((resolve) => {\n      reader.onload = function() {\n        testing.expectEqual(\"ABC\", reader.result);\n        resolve();\n      };\n    });\n\n    reader.readAsBinaryString(blob);\n    await promise;\n  });\n</script>\n\n<script id=abort>\n  // Test aborting when not loading (should do nothing)\n  {\n    const reader = new FileReader();\n    reader.abort(); // Should not throw\n    testing.expectEqual(0, reader.readyState);\n  }\n\n  // Note: Testing abort during read is implementation-dependent.\n  // In synchronous implementations (like ours), the read completes before abort can be called.\n  // In async implementations (like Firefox), you can abort during the read.\n  // We test that abort() at least doesn't throw and maintains correct state.\n</script>\n\n<script id=multipleReads>\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob1 = new Blob([\"first\"]);\n    const blob2 = new Blob([\"second\"]);\n\n    // First read\n    const promise1 = new Promise((resolve) => {\n      reader.onload = function() {\n        testing.expectEqual(\"first\", reader.result);\n        resolve();\n      };\n    });\n    reader.readAsText(blob1);\n    await promise1;\n\n    // Second read - should work after first completes\n    const promise2 = new Promise((resolve) => {\n      reader.onload = function() {\n        testing.expectEqual(\"second\", reader.result);\n        resolve();\n      };\n    });\n    reader.readAsText(blob2);\n    await promise2;\n  });\n</script>\n\n<script id=addEventListener>\n  testing.async(async () => {\n    const reader = new FileReader();\n    const blob = new Blob([\"test\"]);\n\n    let loadFired = false;\n\n    const promise = new Promise((resolve) => {\n      reader.addEventListener(\"load\", function() {\n        loadFired = true;\n        resolve();\n      });\n    });\n\n    reader.readAsText(blob);\n    await promise;\n\n    testing.expectEqual(true, loadFired);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/frames/frames.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script>\n  function frame1Onload() {\n    window.f1_onload = 'f1_onload_loaded';\n  }\n</script>\n\n<iframe id=f0></iframe>\n<iframe id=f1 onload=\"frame1Onload()\" src=\"support/sub 1.html\"></iframe>\n<iframe id=f2 src=\"support/sub2.html\"></iframe>\n\n<script id=empty>\n  {\n    const blank = document.createElement('iframe');\n    testing.expectEqual(null, blank.contentDocument);\n    document.documentElement.appendChild(blank);\n    testing.expectEqual('<html><head></head><body></body></html>', blank.contentDocument.documentElement.outerHTML);\n\n    const f0 = $('#f0')\n    testing.expectEqual('<html><head></head><body></body></html>', f0.contentDocument.documentElement.outerHTML);\n  }\n</script>\n\n<script id=\"basic\">\n   // reload it\n  $('#f2').src = 'support/sub2.html';\n  testing.expectEqual(true, true);\n\n  testing.eventually(() => {\n    testing.expectEqual(undefined, window[20]);\n\n    testing.expectEqual(window, window[1].top);\n    testing.expectEqual(window, window[1].parent);\n    testing.expectEqual(false, window === window[1]);\n\n    testing.expectEqual(window, window[2].top);\n    testing.expectEqual(window, window[2].parent);\n    testing.expectEqual(false, window === window[2]);\n    testing.expectEqual(false, window[1] === window[2]);\n\n    testing.expectEqual(0, $('#f1').childNodes.length);\n\n    testing.expectEqual(testing.BASE_URL + 'frames/support/sub%201.html', $('#f1').src);\n    testing.expectEqual(window[1], $('#f1').contentWindow);\n    testing.expectEqual(window[2], $('#f2').contentWindow);\n\n    testing.expectEqual(window[1].document, $('#f1').contentDocument);\n    testing.expectEqual(window[2].document, $('#f2').contentDocument);\n\n    // sibling frames share the same top\n    testing.expectEqual(window[1].top, window[2].top);\n\n    // child frames have no sub-frames\n    testing.expectEqual(0, window[1].length);\n    testing.expectEqual(0, window[2].length);\n\n    // self and window are self-referential on child frames\n    testing.expectEqual(window[1], window[1].self);\n    testing.expectEqual(window[1], window[1].window);\n    testing.expectEqual(window[2], window[2].self);\n\n    // child frame's top.parent is itself (root has no parent)\n    testing.expectEqual(window, window[0].top.parent);\n\n    // Cross-frame property access\n    testing.expectEqual(true, window.sub1_loaded);\n    testing.expectEqual(true, window.sub2_loaded);\n    testing.expectEqual(1, window.sub1_count);\n    // depends on how far the initial load got before it was cancelled.\n    testing.expectEqual(true, window.sub2_count == 1 || window.sub2_count == 2);\n  });\n</script>\n\n<script id=onload>\n  {\n    let f3_load_event = false;\n    let f3 = document.createElement('iframe');\n    f3.id = 'f3';\n    f3.addEventListener('load', () => {\n      f3_load_event = true;\n    });\n    f3.src = 'invalid'; // still fires load!\n    document.documentElement.appendChild(f3);\n\n    testing.eventually(() => {\n      testing.expectEqual('f1_onload_loaded', window.f1_onload);\n      testing.expectEqual(true, f3_load_event);\n    });\n  }\n</script>\n\n<script id=about_blank>\n  {\n    let f4 = document.createElement('iframe');\n    f4.id = 'f4';\n    f4.src = \"about:blank\";\n    document.documentElement.appendChild(f4);\n\n    testing.eventually(() => {\n      testing.expectEqual(\"<html><head></head><body></body></html>\", f4.contentDocument.documentElement.outerHTML);\n    });\n  }\n</script>\n\n<script id=about_blank_renavigate>\n  {\n    let f5 = document.createElement('iframe');\n    f5.id = 'f5';\n    f5.src = \"support/page.html\";\n    document.documentElement.appendChild(f5);\n    f5.src = \"about:blank\";\n\n    testing.eventually(() => {\n      testing.expectEqual(\"<html><head></head><body></body></html>\", f5.contentDocument.documentElement.outerHTML);\n    });\n  }\n</script>\n\n<script id=link_click>\n  testing.async(async (restore) => {\n    await new Promise((resolve) => {\n      let count = 0;\n      let f6 = document.createElement('iframe');\n      f6.id = 'f6';\n      f6.addEventListener('load', () => {\n        if (++count == 2) {\n          resolve();\n          return;\n        }\n        f6.contentDocument.querySelector('#link').click();\n      });\n      f6.src = \"support/with_link.html\";\n      document.documentElement.appendChild(f6);\n    });\n    restore();\n    testing.expectEqual(\"<html><head></head><body>It was clicked!\\n</body></html>\", f6.contentDocument.documentElement.outerHTML);\n  });\n</script>\n\n<script id=count>\n  testing.eventually(() => {\n    testing.expectEqual(8, window.length);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/frames/post_message.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<iframe id=\"receiver\"></iframe>\n\n<script id=\"messages\">\n{\n  let reply = null;\n  window.addEventListener('message', (e) => {\n    console.warn('reply')\n    reply = e.data;\n  });\n\n  const iframe = $('#receiver');\n  iframe.src = 'support/message_receiver.html';\n  iframe.addEventListener('load', () => {\n    iframe.contentWindow.postMessage('ping', '*');\n  });\n\n  testing.eventually(() => {\n    testing.expectEqual('pong', reply.data);\n    testing.expectEqual(testing.ORIGIN, reply.origin);\n  });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/frames/support/after_link.html",
    "content": "<!DOCTYPE html>\nIt was clicked!\n"
  },
  {
    "path": "src/browser/tests/frames/support/message_receiver.html",
    "content": "<!DOCTYPE html>\n<script>\n  window.addEventListener('message', (e) => {\n    console.warn('Frame Message', e.data);\n    if (e.data === 'ping') {\n      window.top.postMessage({data: 'pong', origin: e.origin}, '*');\n    }\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/frames/support/page.html",
    "content": "<!DOCTYPE html>\na-page\n"
  },
  {
    "path": "src/browser/tests/frames/support/sub 1.html",
    "content": "<!DOCTYPE html>\n<div id=div-1>sub1 div1</div>\n<script>\n  // should not have access to the parent's JS context\n  window.top.sub1_loaded = window.testing == undefined;\n  window.top.sub1_count = (window.top.sub1_count || 0) + 1;\n</script>\n"
  },
  {
    "path": "src/browser/tests/frames/support/sub2.html",
    "content": "<!DOCTYPE html>\n<div id=div-1>sub2 div1</div>\n\n<script>\n  // should not have access to the parent's JS context\n  window.top.sub2_loaded = window.testing == undefined;\n  window.top.sub2_count = (window.top.sub2_count || 0) + 1;\n</script>\n"
  },
  {
    "path": "src/browser/tests/frames/support/with_link.html",
    "content": "<!DOCTYPE html>\n<a href=\"support/after_link.html\" id=link>a link</a>\n"
  },
  {
    "path": "src/browser/tests/frames/target.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<iframe name=f1 id=frame1></iframe>\n<a id=l1 target=f1 href=support/page.html></a>\n<script id=anchor>\n  $('#l1').click();\n  testing.eventually(() => {\n    testing.expectEqual('<html><head></head><body>a-page\\n</body></html>', $('#frame1').contentDocument.documentElement.outerHTML);\n  });\n</script>\n\n<script id=form>\n  {\n    let frame2 = document.createElement('iframe');\n    frame2.name = 'frame2';\n    document.documentElement.appendChild(frame2);\n\n    let form = document.createElement('form');\n    form.target = 'frame2';\n    form.action = 'support/page.html';\n    form.submit();\n\n    testing.eventually(() => {\n      testing.expectEqual('<html><head></head><body>a-page\\n</body></html>', frame2.contentDocument.documentElement.outerHTML);\n    });\n  }\n</script>\n\n<iframe name=frame3 id=f3></iframe>\n<form target=\"_top\" action=\"support/page.html\">\n  <input type=submit id=submit1 formtarget=\"frame3\">\n</form>\n\n<script id=formtarget>\n  {\n    $('#submit1').click();\n    testing.eventually(() => {\n      testing.expectEqual('<html><head></head><body>a-page\\n</body></html>', $('#f3').contentDocument.documentElement.outerHTML);\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/history.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=history>\n  // This test is a bit wonky. But it's trying to test navigation, which is\n  // something we can't do in the main page (we can't navigate away from this\n  // page and still assertOk in the test runner).\n  // If support/history.html has a failed assertion, it'll log the error and\n  // stop the script. If it succeeds, it'll set support_history_completed\n  // which we can use here to assume everything passed.\n  testing.eventually(() => {\n    testing.expectEqual(true, window.support_history_completed);\n    testing.expectEqual(true, window.support_history_popstateEventFired);\n    testing.expectEqual({testInProgress: true }, window.support_history_popstateEventState);\n  });\n</script>\n\n<iframe id=frame src=\"support/history.html\"></iframe>\n"
  },
  {
    "path": "src/browser/tests/history_after_nav.skip.html",
    "content": "<!DOCTYPE html>\n"
  },
  {
    "path": "src/browser/tests/image_data.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=constructor-basic>\n  {\n    const img = new ImageData(10, 20);\n    testing.expectEqual(10, img.width);\n    testing.expectEqual(20, img.height);\n    testing.expectEqual(\"srgb\", img.colorSpace);\n    testing.expectEqual(\"rgba-unorm8\", img.pixelFormat);\n  }\n</script>\n\n<script id=data-property>\n  {\n    const img = new ImageData(2, 3);\n    const data = img.data;\n    testing.expectEqual(true, data instanceof Uint8ClampedArray);\n    // 2 * 3 * 4 (RGBA) = 24 bytes\n    testing.expectEqual(24, data.length);\n  }\n</script>\n\n<script id=data-initialized-to-zero>\n  {\n    const img = new ImageData(2, 2);\n    const data = img.data;\n    for (let i = 0; i < data.length; i++) {\n      testing.expectEqual(0, data[i]);\n    }\n  }\n</script>\n\n<script id=data-mutability>\n  {\n    const img = new ImageData(1, 1);\n    const data = img.data;\n    // Set pixel to red (RGBA)\n    data[0] = 255;\n    data[1] = 0;\n    data[2] = 0;\n    data[3] = 255;\n\n    // Read back through the same accessor\n    const data2 = img.data;\n    testing.expectEqual(255, data2[0]);\n    testing.expectEqual(0, data2[1]);\n    testing.expectEqual(0, data2[2]);\n    testing.expectEqual(255, data2[3]);\n  }\n</script>\n\n<script id=constructor-with-settings>\n  {\n    const img = new ImageData(5, 5, { colorSpace: \"srgb\" });\n    testing.expectEqual(5, img.width);\n    testing.expectEqual(5, img.height);\n    testing.expectEqual(\"srgb\", img.colorSpace);\n  }\n</script>\n\n</script>\n\n<script id=single-pixel>\n  {\n    const img = new ImageData(1, 1);\n    testing.expectEqual(4, img.data.length);\n    testing.expectEqual(1, img.width);\n    testing.expectEqual(1, img.height);\n  }\n</script>\n\n<script id=too-large>\n  testing.expectError(\"IndexSizeError\", () => new ImageData(2_147_483_648, 2_147_483_648));\n</script>\n"
  },
  {
    "path": "src/browser/tests/integration/custom_element_composition.html",
    "content": "<!DOCTYPE html>\n<body></body>\n<script src=\"../testing.js\"></script>\n\n<!-- Test complex component composition patterns like React/Next.js -->\n\n<script id=\"card_component_with_slots\">\n{\n    // Define a card component with header, body, footer slots (like React children props)\n    customElements.define('app-card', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <div class=\"card\">\n                    <div class=\"card-header\">\n                        <slot name=\"header\">Default Header</slot>\n                    </div>\n                    <div class=\"card-body\">\n                        <slot></slot>\n                    </div>\n                    <div class=\"card-footer\">\n                        <slot name=\"footer\"></slot>\n                    </div>\n                </div>\n            `;\n        }\n    });\n\n    // Create card with mixed content\n    const card = document.createElement('app-card');\n\n    const header = document.createElement('h2');\n    header.setAttribute('slot', 'header');\n    header.textContent = 'My Card';\n    card.appendChild(header);\n\n    const body = document.createElement('p');\n    body.textContent = 'Card content';\n    card.appendChild(body);\n\n    const footer = document.createElement('span');\n    footer.setAttribute('slot', 'footer');\n    footer.textContent = 'Footer text';\n    card.appendChild(footer);\n\n    document.body.appendChild(card);\n\n    // Verify slot assignments\n    const headerSlot = card.shadowRoot.querySelector('slot[name=\"header\"]');\n    const defaultSlot = card.shadowRoot.querySelectorAll('slot:not([name])')[0];\n    const footerSlot = card.shadowRoot.querySelector('slot[name=\"footer\"]');\n\n    testing.expectEqual(1, headerSlot.assignedElements().length);\n    testing.expectEqual(1, defaultSlot.assignedElements().length);\n    testing.expectEqual(1, footerSlot.assignedElements().length);\n\n    testing.expectTrue(headerSlot.assignedElements()[0] === header);\n    testing.expectTrue(defaultSlot.assignedElements()[0] === body);\n    testing.expectTrue(footerSlot.assignedElements()[0] === footer);\n}\n</script>\n\n<script id=\"nested_components_with_slots\">\n{\n    // Like React HOCs or wrapper components\n    customElements.define('app-wrapper', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <div class=\"wrapper\">\n                    <slot></slot>\n                </div>\n            `;\n        }\n    });\n\n    customElements.define('app-inner', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <div class=\"inner\">\n                    <slot></slot>\n                </div>\n            `;\n        }\n    });\n\n    // Nest components\n    const wrapper = document.createElement('app-wrapper');\n    const inner = document.createElement('app-inner');\n    const content = document.createElement('span');\n    content.textContent = 'Deeply nested';\n\n    inner.appendChild(content);\n    wrapper.appendChild(inner);\n    document.body.appendChild(wrapper);\n\n    // Verify outer wrapper slot\n    const wrapperSlot = wrapper.shadowRoot.querySelector('slot');\n    const wrapperAssigned = wrapperSlot.assignedElements();\n    testing.expectEqual(1, wrapperAssigned.length);\n    testing.expectTrue(wrapperAssigned[0] === inner);\n\n    // Verify inner component slot\n    const innerSlot = inner.shadowRoot.querySelector('slot');\n    const innerAssigned = innerSlot.assignedElements();\n    testing.expectEqual(1, innerAssigned.length);\n    testing.expectTrue(innerAssigned[0] === content);\n\n    // With flatten, outer slot should see through to content\n    const flatAssigned = wrapperSlot.assignedElements({ flatten: true });\n    testing.expectEqual(1, flatAssigned.length);\n}\n</script>\n\n<script id=\"tab_component_pattern\">\n{\n    // Tabs pattern - common in UI libraries\n    customElements.define('app-tabs', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <div class=\"tabs\">\n                    <div class=\"tab-buttons\">\n                        <slot name=\"tabs\"></slot>\n                    </div>\n                    <div class=\"tab-content\">\n                        <slot name=\"panels\"></slot>\n                    </div>\n                </div>\n            `;\n        }\n    });\n\n    const tabs = document.createElement('app-tabs');\n\n    // Add tab buttons\n    for (let i = 1; i <= 3; i++) {\n        const button = document.createElement('button');\n        button.setAttribute('slot', 'tabs');\n        button.textContent = `Tab ${i}`;\n        tabs.appendChild(button);\n    }\n\n    // Add panels\n    for (let i = 1; i <= 3; i++) {\n        const panel = document.createElement('div');\n        panel.setAttribute('slot', 'panels');\n        panel.textContent = `Panel ${i}`;\n        tabs.appendChild(panel);\n    }\n\n    document.body.appendChild(tabs);\n\n    const tabSlot = tabs.shadowRoot.querySelector('slot[name=\"tabs\"]');\n    const panelSlot = tabs.shadowRoot.querySelector('slot[name=\"panels\"]');\n\n    testing.expectEqual(3, tabSlot.assignedElements().length);\n    testing.expectEqual(3, panelSlot.assignedElements().length);\n}\n</script>\n\n<script id=\"layout_component_all_slots\">\n{\n    // App layout pattern from Next.js/React\n    customElements.define('app-layout', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <div class=\"layout\">\n                    <header><slot name=\"header\"></slot></header>\n                    <aside><slot name=\"sidebar\"></slot></aside>\n                    <main><slot></slot></main>\n                    <footer><slot name=\"footer\"></slot></footer>\n                </div>\n            `;\n        }\n    });\n\n    const layout = document.createElement('app-layout');\n\n    const header = document.createElement('div');\n    header.setAttribute('slot', 'header');\n    header.textContent = 'Header';\n    layout.appendChild(header);\n\n    const sidebar = document.createElement('nav');\n    sidebar.setAttribute('slot', 'sidebar');\n    sidebar.textContent = 'Nav';\n    layout.appendChild(sidebar);\n\n    const main = document.createElement('article');\n    main.textContent = 'Main content';\n    layout.appendChild(main);\n\n    const footer = document.createElement('div');\n    footer.setAttribute('slot', 'footer');\n    footer.textContent = 'Footer';\n    layout.appendChild(footer);\n\n    document.body.appendChild(layout);\n\n    // All slots should be filled\n    const slots = layout.shadowRoot.querySelectorAll('slot');\n    testing.expectEqual(4, slots.length);\n\n    for (const slot of slots) {\n        testing.expectTrue(slot.assignedNodes().length > 0);\n    }\n}\n</script>\n\n<script id=\"dynamic_slot_reassignment\">\n{\n    // Simulating conditional rendering like React\n    customElements.define('app-container', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <slot name=\"primary\"></slot>\n                <slot name=\"secondary\"></slot>\n            `;\n        }\n    });\n\n    const container = document.createElement('app-container');\n    const elem = document.createElement('div');\n    elem.textContent = 'Content';\n    elem.setAttribute('slot', 'primary');\n    container.appendChild(elem);\n    document.body.appendChild(container);\n\n    const primarySlot = container.shadowRoot.querySelector('slot[name=\"primary\"]');\n    const secondarySlot = container.shadowRoot.querySelector('slot[name=\"secondary\"]');\n\n    // Initially in primary\n    testing.expectEqual(1, primarySlot.assignedElements().length);\n    testing.expectEqual(0, secondarySlot.assignedElements().length);\n\n    // Move to secondary (like React re-rendering with different props)\n    elem.setAttribute('slot', 'secondary');\n\n    // Should now be in secondary\n    testing.expectEqual(0, primarySlot.assignedElements().length);\n    testing.expectEqual(1, secondarySlot.assignedElements().length);\n}\n</script>\n\n<script id=\"list_rendering_pattern\">\n{\n    // List rendering like map() in React\n    customElements.define('app-list', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <ul><slot></slot></ul>\n            `;\n        }\n    });\n\n    customElements.define('app-list-item', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <li><slot></slot></li>\n            `;\n        }\n    });\n\n    const list = document.createElement('app-list');\n\n    const items = ['Item 1', 'Item 2', 'Item 3'];\n    items.forEach(text => {\n        const item = document.createElement('app-list-item');\n        const span = document.createElement('span');\n        span.textContent = text;\n        item.appendChild(span);\n        list.appendChild(item);\n    });\n\n    document.body.appendChild(list);\n\n    // List should have 3 items assigned\n    const listSlot = list.shadowRoot.querySelector('slot');\n    testing.expectEqual(3, listSlot.assignedElements().length);\n\n    // Each item should have content\n    const itemElements = listSlot.assignedElements();\n    itemElements.forEach(item => {\n        const itemSlot = item.shadowRoot.querySelector('slot');\n        testing.expectEqual(1, itemSlot.assignedElements().length);\n    });\n}\n</script>\n\n<script id=\"fallback_content_pattern\">\n{\n    // Default/fallback content like React default props\n    customElements.define('app-message', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <div class=\"message\">\n                    <slot>No message provided</slot>\n                </div>\n            `;\n        }\n    });\n\n    // Without content - should use fallback\n    const empty = document.createElement('app-message');\n    document.body.appendChild(empty);\n    const emptySlot = empty.shadowRoot.querySelector('slot');\n    testing.expectEqual(0, emptySlot.assignedNodes().length);\n\n    // With content - should override fallback\n    const filled = document.createElement('app-message');\n    const text = document.createTextNode('Custom message');\n    filled.appendChild(text);\n    document.body.appendChild(filled);\n    const filledSlot = filled.shadowRoot.querySelector('slot');\n    testing.expectEqual(1, filledSlot.assignedNodes().length);\n}\n</script>\n\n<script id=\"form_custom_input\">\n{\n    // Custom form input component\n    customElements.define('app-input', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <label>\n                    <slot name=\"label\">Label</slot>\n                    <input type=\"text\">\n                </label>\n            `;\n        }\n    });\n\n    const input = document.createElement('app-input');\n    const label = document.createElement('span');\n    label.setAttribute('slot', 'label');\n    label.textContent = 'Username:';\n    input.appendChild(label);\n\n    document.body.appendChild(input);\n\n    const labelSlot = input.shadowRoot.querySelector('slot[name=\"label\"]');\n    testing.expectEqual(1, labelSlot.assignedElements().length);\n    testing.expectEqual('Username:', labelSlot.assignedElements()[0].textContent);\n}\n</script>\n\n<script id=\"deeply_nested_slot_chain\">\n{\n    // Three levels of nesting with slots\n    customElements.define('app-outer', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `<div class=\"outer\"><slot></slot></div>`;\n        }\n    });\n\n    customElements.define('app-middle', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `<div class=\"middle\"><slot></slot></div>`;\n        }\n    });\n\n    customElements.define('app-leaf', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `<div class=\"leaf\"><slot></slot></div>`;\n        }\n    });\n\n    const outer = document.createElement('app-outer');\n    const middle = document.createElement('app-middle');\n    const leaf = document.createElement('app-leaf');\n    const content = document.createElement('span');\n    content.textContent = 'Deep content';\n\n    leaf.appendChild(content);\n    middle.appendChild(leaf);\n    outer.appendChild(middle);\n    document.body.appendChild(outer);\n\n    // Each level should see one element\n    const outerSlot = outer.shadowRoot.querySelector('slot');\n    testing.expectEqual(1, outerSlot.assignedElements().length);\n    testing.expectTrue(outerSlot.assignedElements()[0] === middle);\n\n    const middleSlot = middle.shadowRoot.querySelector('slot');\n    testing.expectEqual(1, middleSlot.assignedElements().length);\n    testing.expectTrue(middleSlot.assignedElements()[0] === leaf);\n\n    const leafSlot = leaf.shadowRoot.querySelector('slot');\n    testing.expectEqual(1, leafSlot.assignedElements().length);\n    testing.expectTrue(leafSlot.assignedElements()[0] === content);\n\n    // Flatten should not traverse through non-slot elements\n    const outerFlat = outerSlot.assignedElements({ flatten: true });\n    testing.expectEqual(1, outerFlat.length);\n}\n</script>\n\n<script id=\"mixed_slotted_and_unslotted\">\n{\n    // Some children slotted, some not (edge case)\n    customElements.define('app-mixed', class extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = `\n                <slot name=\"named\"></slot>\n                <slot></slot>\n            `;\n        }\n    });\n\n    const mixed = document.createElement('app-mixed');\n\n    const named = document.createElement('div');\n    named.setAttribute('slot', 'named');\n    mixed.appendChild(named);\n\n    const unnamed1 = document.createElement('span');\n    mixed.appendChild(unnamed1);\n\n    const unnamed2 = document.createElement('p');\n    mixed.appendChild(unnamed2);\n\n    document.body.appendChild(mixed);\n\n    const namedSlot = mixed.shadowRoot.querySelector('slot[name=\"named\"]');\n    const defaultSlot = mixed.shadowRoot.querySelectorAll('slot:not([name])')[0];\n\n    testing.expectEqual(1, namedSlot.assignedElements().length);\n    testing.expectEqual(2, defaultSlot.assignedElements().length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/intersection_observer/basic.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"target\" style=\"width: 100px; height: 100px;\">Target Element</div>\n\n<script id=\"basic\">\n    const target = document.getElementById('target');\n    let callbackCalled = false;\n    let entries = null;\n\n    const observer = new IntersectionObserver((observerEntries, obs) => {\n        callbackCalled = true;\n        entries = observerEntries;\n    });\n\n    observer.observe(target);\n\n    testing.eventually(() => {\n        testing.expectEqual(true, callbackCalled);\n        testing.expectEqual(1, entries.length);\n\n        const entry = entries[0];\n\n        testing.expectEqual(target, entry.target);\n        testing.expectEqual('boolean', typeof entry.isIntersecting);\n        testing.expectEqual('number', typeof entry.intersectionRatio);\n        testing.expectEqual('object', typeof entry.boundingClientRect);\n        testing.expectEqual('object', typeof entry.intersectionRect);\n        testing.expectEqual('number', typeof entry.time);\n        testing.expectEqual(true, entry.time > 0);\n\n        observer.disconnect();\n    });\n</script>\n\n<script id=detached>\n  {\n    // never attached\n    let count = 0;\n    const div = document.createElement('div');\n    new IntersectionObserver((entries) => {\n      count += 1;\n    }).observe(div);\n\n    testing.eventually(() => {\n      testing.expectEqual(0, count);\n    });\n  }\n\n  {\n    // not connected, but has parent\n    let count = 0;\n    const div1 = document.createElement('div');\n    const div2 = document.createElement('div');\n    new IntersectionObserver((entries) => {\n      count += 1;\n    }).observe(div1);\n\n    div2.appendChild(div1);\n    testing.eventually(() => {\n      testing.expectEqual(1, count);\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/intersection_observer/disconnect.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"target\" style=\"width: 100px; height: 100px;\">Target Element</div>\n\n<script id=\"disconnect\">\n    const target = document.getElementById('target');\n    let callCount = 0;\n\n    const observer = new IntersectionObserver(() => {\n        callCount++;\n    });\n\n    observer.observe(target);\n\n    testing.eventually(() => {\n        testing.expectEqual(1, callCount);\n\n        observer.disconnect();\n\n        // Create a new observer to trigger another check\n        // If disconnect worked, the old observer won't fire\n        const observer2 = new IntersectionObserver(() => {});\n        observer2.observe(target);\n\n        testing.eventually(() => {\n            observer2.disconnect();\n            testing.expectEqual(1, callCount);\n        });\n    });\n</script>\n"
  },
  {
    "path": "src/browser/tests/intersection_observer/multiple_targets.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"target1\" style=\"width: 100px; height: 100px;\">Target 1</div>\n<div id=\"target2\" style=\"width: 100px; height: 100px;\">Target 2</div>\n\n<script id=\"multiple\">\n    const target1 = document.getElementById('target1');\n    const target2 = document.getElementById('target2');\n    let entryCount = 0;\n    const seenTargets = new Set();\n\n    const observer = new IntersectionObserver((entries) => {\n        entries.forEach(entry => {\n            entryCount++;\n            seenTargets.add(entry.target);\n        });\n    });\n\n    observer.observe(target1);\n    observer.observe(target2);\n\n    testing.eventually(() => {\n        testing.expectEqual(2, entryCount);\n        testing.expectTrue(seenTargets.has(target1));\n        testing.expectTrue(seenTargets.has(target2));\n\n        observer.disconnect();\n    });\n</script>\n"
  },
  {
    "path": "src/browser/tests/intersection_observer/unobserve.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"target1\" style=\"width: 100px; height: 100px;\">Target 1</div>\n<div id=\"target2\" style=\"width: 100px; height: 100px;\">Target 2</div>\n\n<script id=\"unobserve\">\n    const target1 = document.getElementById('target1');\n    const target2 = document.getElementById('target2');\n    const seenTargets = [];\n\n    const observer = new IntersectionObserver((entries) => {\n        entries.forEach(entry => {\n            seenTargets.push(entry.target);\n        });\n    });\n\n    // Observe target1, unobserve it, then observe target2\n    // We should only get a callback for target2\n    observer.observe(target1);\n    observer.unobserve(target1);\n    observer.observe(target2);\n\n    testing.eventually(() => {\n        // Should only see target2, not target1\n        testing.expectEqual(1, seenTargets.length);\n        testing.expectEqual(target2, seenTargets[0]);\n\n        observer.disconnect();\n    });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/browser.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<script id=intl>\n  var te = new TextEncoder();\n  testing.expectEqual('utf-8', te.encoding);\n  testing.expectEqual([226, 130, 172], Array.from(te.encode('€')));\n\n  // this will crash if ICU isn't properly configured / ininitialized\n  testing.expectEqual(\"[object Intl.DateTimeFormat]\", new Intl.DateTimeFormat().toString());\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/crypto.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<script id=crypto>\n  const a = crypto.randomUUID();\n  const b = crypto.randomUUID();\n  testing.expectEqual(36, a.length);\n  testing.expectEqual(36, b.length);\n  testing.expectEqual(false, a == b)\n\n  testing.expectError('Error: QuotaExceededError', () => {\n    crypto.getRandomValues(new BigUint64Array(8193));\n  });\n\n  let r1 = new Int32Array(5)\n  let r2 = crypto.getRandomValues(r1)\n  testing.expectEqual(5, new Set(r1).size);\n  testing.expectEqual(5, new Set(r2).size);\n  testing.expectEqual(true, r1.every((v, i) => v === r2[i]));\n\n  var r3 = new Uint8Array(16);\n  let r4 = crypto.getRandomValues(r3);\n\n  r4[6] = 10;\n  testing.expectEqual(10, r4[6]);\n  testing.expectEqual(10, r3[6]);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/css.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<script id=support>\n  testing.expectEqual(true, CSS.supports('display: flex'));\n  testing.expectEqual(true, CSS.supports('text-decoration-style', 'blink'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/cssom/css_style_declaration.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=css_style_declaration>\n  let style = document.createElement('div').style;\n  style.cssText = 'color: red; font-size: 12px; margin: 5px !important;';\n  testing.expectEqual(3, style.length);\n\n  testing.expectEqua;('red', style.getPropertyValue('color'));\n  testing.expectEqua;('12px', style.getPropertyValue('font-size'));\n  testing.expectEqua;('', style.getPropertyValue('unknown-property'));\n\n  testing.expectEqual('important', style.getPropertyPriority('margin'));\n  testing.expectEqual('', style.getPropertyPriority('color'));\n  testing.expectEqual('', style.getPropertyPriority('unknown-property'));\n\n  testing.expectEqual('color', style.item(0));\n  testing.expectEqual('font-size', style.item(1));\n  testing.expectEqual('margin', style.item(2));\n  testing.expectEqual('', style.item(3));\n\n  style.setProperty('background-color', 'blue');\n  testing.expectEqual('blue', style.getPropertyValue('background-color'));\n  testing.expectEqual(4, style.length);\n\n  style.setProperty('color', 'green');\n  testing.expectEqual('green', style.color);\n  testing.expectEqual('green', style.getPropertyValue('color'));\n  testing.expectEqual(4, style.length);\n\n  style.setProperty('padding', '10px', 'important');\n  testing.expectEqual('10px', style.getPropertyValue('padding'));\n  testing.expectEqual('important', style.getPropertyPriority('padding'));\n\n  style.setProperty('border', '1px solid black', 'IMPORTANT');\n  testing.expectEqual('important', style.getPropertyPriority('border'));\n</script>\n\n<script id=removeProperty>\n  testing.expectEqual('green', style.removeProperty('color'));\n  testing.expectEqual('', style.getPropertyValue('color'));\n  testing.expectEqual(5, style.length)\n\n  testing.expectEqual('', style.removeProperty('unknown-property'));\n</script>\n\n<script id=includes>\n  testing.expectEqual(false, style.cssText.includes('font-size: 10px;'));\n  testing.expectEqual(true, style.cssText.includes('font-size: 12px;'));\n  testing.expectEqual(true, style.cssText.includes('margin: 5px !important;'));\n  testing.expectEqual(true, style.cssText.includes('padding: 10px !important;'));\n  testing.expectEqual(true, style.cssText.includes('border: 1px solid black !important;'));\n</script>\n\n<script id=special_char\">\n  style.cssText = 'color: purple; text-align: center;';\n  testing.expectEqual(2, style.length);\n  testing.expectEqual('purple', style.getPropertyValue('color'));\n  testing.expectEqual('center', style.getPropertyValue('text-align'));\n  testing.expectEqual('', style.getPropertyValue('font-size'));\n\n  style.setProperty('cont', 'Hello; world!');\n  testing.expectEqual('Hello; world!', style.getPropertyValue('cont'));\n\n  style.cssText = 'content: \"Hello; world!\"; background-image: url(\"test.png\");';\n  testing.expectEqual('\"Hello; world!\"', style.getPropertyValue('content'));\n  testing.expectEqual('url(\"test.png\")', style.getPropertyValue('background-image'));\n</script>\n\n<script id=cssFloat\">\n  testing.expectEqual('', style.cssFloat);\n  style.cssFloat = 'left';\n  testing.expectEqual('left', style.cssFloat);\n  testing.expectEqual('left', style.getPropertyValue('float'));\n\n  style.cssFloat = 'right';\n  testing.expectEqual('right', style.cssFloat);\n  testing.expectEqual('right', style.getPropertyValue('float'));\n\n  style.cssFloat = null;\n  testing.expectEqual('', style.cssFloat);\n  testing.expectEqual('', style.getPropertyValue('float'));\n</script>\n\n<script id=misc>\n  style.setProperty('display', '');\n  testing.expectEqual('', style.getPropertyValue('display'));\n\n  // style.cssText = '  color  :  purple  ;  margin  :  10px  ;  ';\n  // testing.expectEqual('purple', style.getPropertyValue('color'));\n  // testing.expectEqual('10px', style.getPropertyValue('margin'));\n\n  // style.setProperty('border-bottom-left-radius', '5px');\n  // testing.expectEqual('5px', style.getPropertyValue('border-bottom-left-radius'));\n\n  // testing.expectEqual('visible', style.visibility);\n  // testing.expectEqual('visible', style.getPropertyValue('visibility'));\n\n  // testing.expectEqual('10px', style.margin);\n  // style.margin = 'auto';\n  // testing.expectEqual('auto', style.margin);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/cssom/css_stylesheet.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=css_stylesheet>\n  let css = new CSSStyleSheet()\n  testing.expectEqual(true, css instanceof CSSStyleSheet);\n  testing.expectEqual(0, css.cssRules.length);\n  testing.expectEqual(null, css.ownerRule);\n\n  let index1 = css.insertRule('body { color: red; }', 0);\n  testing.expectEqual(0, index1);\n  testing.expectEqual(0, css.cssRules.length);\n\n  let replaced = false;\n  css.replace('body{}').then(() => replaced = true);\n  testing.eventually(() => testing.expectEqual(true, replaced));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/animation.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=animation>\n  let a1 = document.createElement('div').animate(null, null);\n  testing.expectEqual('finished', a1.playState);\n\n  let cb = [];\n  a1.ready.then(() => { cb.push('ready') });\n  a1.finished.then((x) => {\n    cb.push('finished');\n    cb.push(x == a1);\n  });\n  testing.eventually(() => testing.expectEqual(['finished', true], cb));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/attribute.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n\n<script id=attribute>\n  let a = document.createAttributeNS('foo', 'bar');\n  testing.expectEqual('foo', a.namespaceURI);\n  testing.expectEqual(null, a.prefix);\n  testing.expectEqual('bar', a.localName);\n  testing.expectEqual('bar', a.name);\n  testing.expectEqual('', a.value);\n\n  // TODO: libdom has a bug here: the created attr has no parent, it\n  // causes a panic w/ libdom when setting the value.\n  //.{ \"a.value = 'nok'\", \"nok\" },\n  testing.expectEqual(null, a.ownerElement);\n\n  let b = document.getElementById('link').getAttributeNode('class');\n  testing.expectEqual('class', b.name);\n  testing.expectEqual('ok', b.value);\n\n  b.value = 'nok';\n  testing.expectEqual('nok', b.value)\n\n  b.value = null;\n  testing.expectEqual('null', b.value);\n\n  b.value = 'ok';\n  testing.expectEqual('ok', b.value);\n\n  testing.expectEqual('link', b.ownerElement.id);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/character_data.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n\n<script id=character_data>\n  let link = document.getElementById('link');\n  let cdata = link.firstChild;\n  testing.expectEqual('OK', cdata.data);\n\n  cdata.data = 'OK modified';\n  testing.expectEqual('OK modified', cdata.data);\n  cdata.data = 'OK';\n\n  testing.expectEqual(2, cdata.length);\n\n  testing.expectEqual(true, cdata.nextElementSibling === null);\n\n  // create a next element\n  let next = document.createElement('a');\n  testing.expectEqual(true, link.appendChild(next, cdata) !== undefined);\n  testing.expectEqual(true, cdata.nextElementSibling.localName === 'a');\n\n  testing.expectEqual(true, cdata.previousElementSibling === null);\n\n  // create a prev element\n  let prev = document.createElement('div');\n  testing.expectEqual(true, link.insertBefore(prev, cdata) !== undefined);\n  testing.expectEqual('div', cdata.previousElementSibling.localName);\n\n  cdata.appendData(' modified');\n  testing.expectEqual('OK modified', cdata.data);\n\n  cdata.deleteData('OK'.length, ' modified'.length);\n  testing.expectEqual('OK', cdata.data)\n\n  cdata.insertData('OK'.length-1, 'modified');\n  testing.expectEqual('OmodifiedK', cdata.data);\n\n  cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced');\n  testing.expectEqual('OreplacedK', cdata.data);\n\n  testing.expectEqual('replaced', cdata.substringData('OK'.length-1, 'replaced'.length));\n  testing.expectEqual('', cdata.substringData('OK'.length-1, 0));\n\n  testing.expectEqual('replaced', cdata.substringData('OK'.length-1, 'replaced'.length));\n  testing.expectEqual('', cdata.substringData('OK'.length-1, 0));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/comment.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=comment>\n  let comment = new Comment('foo');\n  testing.expectEqual('foo', comment.data);\n\n  let empty = new Comment()\n  testing.expectEqual('', empty.data);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/document.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"content\">\n  <a id=\"a1\" href=\"foo\" class=\"ok\">OK</a>\n  <p id=\"p1\" class=\"ok empty\">\n    <span id=\"s1\"></span>\n  </p>\n  <p id=\"p2\"> And</p>\n</div>\n\n<script id=document>\n  testing.expectEqual('Document', document.__proto__.__proto__.constructor.name);\n  testing.expectEqual('Node', document.__proto__.__proto__.__proto__.constructor.name);\n  testing.expectEqual('EventTarget', document.__proto__.__proto__.__proto__.__proto__.constructor.name);\n\n  let newdoc = new Document();\n  testing.expectEqual(null, newdoc.documentElement);\n  testing.expectEqual(0, newdoc.children.length);\n  testing.expectEqual(0, newdoc.getElementsByTagName('*').length);\n  testing.expectEqual(null, newdoc.getElementsByTagName('*').item(0));\n  testing.expectEqual(true, newdoc.inputEncoding === document.inputEncoding);\n  testing.expectEqual(true, newdoc.documentURI === document.documentURI);\n  testing.expectEqual(true, newdoc.URL === document.URL);\n  testing.expectEqual(true, newdoc.compatMode === document.compatMode);\n  testing.expectEqual(true, newdoc.characterSet === document.characterSet);\n  testing.expectEqual(true, newdoc.charset === document.charset);\n\n  testing.expectEqual('HTML', document.documentElement.tagName);\n\n  testing.expectEqual('UTF-8', document.characterSet);\n  testing.expectEqual('UTF-8', document.charset);\n  testing.expectEqual('UTF-8', document.inputEncoding);\n  testing.expectEqual('CSS1Compat', document.compatMode);\n  testing.expectEqual('text/html', document.contentType);\n\n  testing.expectEqual('http://localhost:9589/dom/document.html', document.documentURI);\n  testing.expectEqual('http://localhost:9589/dom/document.html', document.URL);\n\n  testing.expectEqual(document.body, document.activeElement);\n\n  $('#a1').focus();\n  testing.expectEqual($('#a1'), document.activeElement);\n\n  testing.expectEqual(0, document.styleSheets.length);\n</script>\n\n<script id=getElementById>\n  let divById = document.getElementById('content');\n  testing.expectEqual('HTMLDivElement', divById.constructor.name);\n  testing.expectEqual('div', divById.localName);\n</script>\n\n<script id=getElementsByTagName>\n  let byTagName = document.getElementsByTagName('p');\n  testing.expectEqual(2, byTagName.length)\n  testing.expectEqual('p1', byTagName.item(0).id);\n  testing.expectEqual('p2', byTagName.item(1).id);\n\n  let byTagNameAll = document.getElementsByTagName('*');\n  // If you add a script block (or change the HTML in any other way on this\n  // page), this test will break. Adjust it accordingly.\n  testing.expectEqual(12, byTagNameAll.length);\n  testing.expectEqual('html', byTagNameAll.item(0).localName);\n  testing.expectEqual('SCRIPT', byTagNameAll.item(11).tagName);\n\n  testing.expectEqual('s1', byTagNameAll.namedItem('s1').id);\n</script>\n\n<script id=getElementByClassName>\n  let ok = document.getElementsByClassName('ok');\n  testing.expectEqual(2, ok.length);\n\n  let empty = document.getElementsByClassName('empty');\n  testing.expectEqual(1, empty.length);\n\n  let emptyok = document.getElementsByClassName('empty ok');\n  testing.expectEqual(1, emptyok.length);\n</script>\n\n<script id=createXYZ>\n  var v = document.createDocumentFragment();\n  testing.expectEqual('#document-fragment', v.nodeName);\n\n  v = document.createTextNode('foo');\n  testing.expectEqual('#text', v.nodeName);\n\n  v = document.createAttribute('foo');\n  testing.expectEqual('foo', v.nodeName);\n\n  v = document.createComment('foo');\n  testing.expectEqual('#comment', v.nodeName);\n  v.cloneNode(); // doesn't crash, (I guess that's the point??)\n\n  let pi = document.createProcessingInstruction('foo', 'bar')\n  testing.expectEqual('foo', pi.target);\n  pi.cloneNode(); // doesn't crash (I guess that's the point??)\n</script>\n\n<script id=importNode>\n  let nimp = document.getElementById('content');\n  var v = document.importNode(nimp);\n  testing.expectEqual('DIV', v.nodeName);\n</script>\n\n<script id=children>\n  testing.expectEqual(1, document.children.length);\n  testing.expectEqual('HTML', document.children.item(0).nodeName)\n  testing.expectEqual('HTML', document.firstElementChild.nodeName);\n  testing.expectEqual('HTML', document.lastElementChild.nodeName);\n  testing.expectEqual(1, document.childElementCount);\n\n  let nd = new Document();\n  testing.expectEqual(0, nd.children.length);\n  testing.expectEqual(null, nd.children.item(0));\n  testing.expectEqual(null, nd.firstElementChild);\n  testing.expectEqual(null, nd.lastElementChild);\n  testing.expectEqual(0, nd.childElementCount);\n</script>\n\n<!-- <script id=createElement>\n  let emptydoc = document.createElement('html');\n  emptydoc.prepend(document.createElement('html'));\n\n  let emptydoc2 = document.createElement('html');\n  emptydoc2.append(document.createElement('html'));\n\n  // Not sure what the above are testing, I just copied and pasted them.\n  // Maybe that something doesn't crash?\n  // Adding this so that the test runner doesn't complain;\n  testing.skip();\n</script> -->\n\n<script id=querySelector>\n  testing.expectEqual('HTML', document.querySelector('*').nodeName);\n  testing.expectEqual('content', document.querySelector('#content').id);\n  testing.expectEqual('p1', document.querySelector('#p1').id);\n  testing.expectEqual('a1', document.querySelector('.ok').id);\n  testing.expectEqual('p1', document.querySelector('a ~ p').id);\n  testing.expectEqual('HTML', document.querySelector(':root').nodeName);\n\n  testing.expectEqual(2, document.querySelectorAll('p').length);\n\n  testing.expectEqual([''],\n    Array.from(document.querySelectorAll('#content > p#p1'))\n        .map(row => row.querySelector('span').textContent)\n  );\n\n  testing.expectEqual(0, document.querySelectorAll('.\\\\:popover-open').length);\n  testing.expectEqual(0, document.querySelectorAll('.foo\\\\:bar').length);\n</script>\n\n<script id=adoptNode>\n// this test breaks the doc structure, keep it at the end\n  let nadop = document.getElementById('content')\n  var v = document.adoptNode(nadop);\n  testing.expectEqual('DIV', v.nodeName);\n</script>\n\n<script id=adoptedStyleSheets>\n  const acss = document.adoptedStyleSheets;\n  testing.expectEqual(0, acss.length);\n  acss.push(new CSSStyleSheet());\n  testing.expectEqual(1, acss.length);\n</script>\n\n<script id=createEvent>\n  const event = document.createEvent(\"Event\");\n\n  testing.expectEqual(true, event instanceof Event);\n  testing.expectEqual(\"\", event.type);\n\n  const customEvent = document.createEvent(\"CustomEvent\");\n  customEvent.initCustomEvent(\"hey\", false, false);\n\n  testing.expectEqual(true, customEvent instanceof CustomEvent);\n  testing.expectEqual(true, customEvent instanceof Event);\n\n  testing.withError(\n      (err) => {\n          // TODO: Check the error type.\n      },\n      () => document.createEvent(\"InvalidType\"),\n  );\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/document_fragment.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=documentFragement>\n  testing.expectEqual('DocumentFragment', new DocumentFragment().constructor.name);\n\n  const dc1 = new DocumentFragment();\n  testing.expectEqual(true, dc1.isEqualNode(dc1))\n\n  const dc2 = new DocumentFragment();\n  testing.expectEqual(true, dc1.isEqualNode(dc2))\n\n  let f = document.createDocumentFragment();\n  let d = document.createElement('div');\n  testing.expectEqual(0, d.childElementCount);\n\n  d.id = 'x';\n  testing.expectEqual(null, $('#x'));\n\n  f.append(d);\n  testing.expectEqual(1, f.childElementCount)\n  testing.expectEqual('x', f.children[0].id);\n  testing.expectEqual(null, $('#x'));\n\n  document.getElementsByTagName('body')[0].append(f.cloneNode(true));\n  testing.expectEqual(true, $('#x') != null);\n\n  testing.expectEqual(null, document.querySelector('.hello'));\n  testing.expectEqual(0, document.querySelectorAll('.hello').length);\n\n  testing.expectEqual('x', document.querySelector('#x').id);\n  testing.expectEqual(['x'], Array.from(document.querySelectorAll('#x')).map((n) => n.id));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/document_type.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=documentType>\n  let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');\n  let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');\n  let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');\n  testing.expectEqual(true, dt1.isEqualNode(dt1));\n  testing.expectEqual(true, dt1.isEqualNode(dt3));\n  testing.expectEqual(false, dt1.isEqualNode(dt2));\n  testing.expectEqual(false, dt2.isEqualNode(dt3));\n  testing.expectEqual(false, dt1.isEqualNode(document));\n  testing.expectEqual(false, document.isEqualNode(dt1));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/dom_parser.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=domParser>\n  const dp = new DOMParser();;\n  const parsed = dp.parseFromString('<div>abc</div>', 'text/html');\n  testing.expectEqual('[object HTMLDocument]', parsed.toString());\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/element.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"content\" dir=\"ltr\">\n  <a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n  <p id=\"para-empty\" class=\"ok empty\">\n    <span id=\"para-empty-child\"></span>\n  </p>\n  <p id=\"para\"> And</p>\n  <!--comment-->\n</div>\n\n<script id=element>\n  let content = document.getElementById('content');\n  testing.expectEqual('http://www.w3.org/1999/xhtml', content.namespaceURI);\n  testing.expectEqual(null, content.prefix);\n  testing.expectEqual('div', content.localName);\n  testing.expectEqual('DIV', content.tagName);\n  testing.expectEqual('content', content.id);\n  testing.expectEqual('ltr', content.dir);\n\n  content.id = 'foo';\n  testing.expectEqual('foo', content.id);\n\n  content.id = 'content';\n  testing.expectEqual('', content.className);\n\n  let p1 = document.getElementById('para-empty');\n  testing.expectEqual('ok empty', p1.className);\n  testing.expectEqual('', p1.dir);\n\n  p1.className = 'foo bar baz';\n  testing.expectEqual('foo bar baz', p1.className);\n\n  p1.className = 'ok empty';\n  testing.expectEqual(2, p1.classList.length);\n</script>\n\n<script id=closest>\n  const el2 = document.createElement('div');\n  el2.id = 'closest';\n  el2.className = 'ok';\n\n  testing.expectEqual(el2, el2.closest('#closest'));\n  testing.expectEqual(el2, el2.closest('.ok'));\n  testing.expectEqual(null, el2.closest('.notok'));\n\n  const sp = document.createElement('span');\n  el2.appendChild(sp);\n  testing.expectEqual(el2, sp.closest('#closest'));\n</script>\n\n<script id=attributes>\n  testing.expectEqual(true, content.hasAttributes());\n  testing.expectEqual(2, content.attributes.length);\n  testing.expectEqual(['id', 'dir'], content.getAttributeNames());\n  testing.expectEqual('content', content.getAttribute('id'));\n  testing.expectEqual('content', content.attributes['id'].value);\n\n  let x = '';\n  for (const attr of content.attributes) {\n    x += attr.name + '=' + attr.value + ',';\n  }\n  testing.expectEqual('id=content,dir=ltr,', x);\n\n  testing.expectEqual(false, content.hasAttribute('foo'));\n  testing.expectEqual(null, content.getAttribute('foo'));\n\n  content.setAttribute('foo', 'bar');\n  testing.expectEqual(true, content.hasAttribute('foo'));\n  testing.expectEqual('bar', content.getAttribute('foo'));\n  testing.expectEqual(['id', 'dir', 'foo'], content.getAttributeNames());\n\n  testing.expectError('InvalidCharacterError: Invalid Character', () => {\n    content.setAttribute('.foo', 'invalid')\n  });\n\n  content.setAttribute('foo', 'baz');\n  testing.expectEqual(true, content.hasAttribute('foo'));\n  testing.expectEqual('baz', content.getAttribute('foo'));\n\n  content.removeAttribute('foo');\n  testing.expectEqual(false, content.hasAttribute('foo'));\n  testing.expectEqual(null, content.getAttribute('foo'));\n\n  let b = document.getElementById('content');\n  testing.expectEqual(true, b.toggleAttribute('foo'));\n  testing.expectEqual(true, b.hasAttribute('foo'));\n  testing.expectEqual('', b.getAttribute('foo'));\n\n  testing.expectEqual(false, b.toggleAttribute('foo'));\n  testing.expectEqual(false, b.hasAttribute('foo'));\n\n  testing.expectEqual(false, document.createElement('a').hasAttributes());\n\n  const div2 = document.createElement('div');\n  div2.innerHTML = '<p id=1 .lit$id=9>a</p>';\n  testing.expectEqual('<p id=\"1\" .lit$id=\"9\">a</p>', div2.innerHTML);\n  testing.expectEqual(['id', '.lit$id'], div2.childNodes[0].getAttributeNames());\n</script>\n\n<script id=children>\n  testing.expectEqual(3, content.children.length);\n  testing.expectEqual('A', content.firstElementChild.nodeName);\n  testing.expectEqual('P', content.lastElementChild.nodeName);\n  testing.expectEqual(3, content.childElementCount);\n</script>\n\n<script id=sibling>\n  content.prepend(document.createTextNode('foo'));\n  content.append(document.createTextNode('bar'));\n\n  let d = document.getElementById('para');\n  testing.expectEqual('P', d.previousElementSibling.nodeName);\n  testing.expectEqual(null, d.nextElementSibling);\n</script>\n\n<script id=querySelector>\n  testing.expectEqual(null, content.querySelector('foo'));\n  testing.expectEqual(null, content.querySelector('#foo'));\n  testing.expectEqual('link', content.querySelector('#link').id);\n  testing.expectEqual('para', content.querySelector('#para').id);\n  testing.expectEqual('link', content.querySelector('*').id);\n  testing.expectEqual('link', content.querySelector('*').id);\n  testing.expectEqual(null, content.querySelector('#content'));\n  testing.expectEqual('para', content.querySelector('#para').id);\n  testing.expectEqual('link', content.querySelector('.ok').id);\n  testing.expectEqual('para-empty', content.querySelector('a ~ p').id);\n\n  testing.expectEqual(0, content.querySelectorAll('foo').length);\n  testing.expectEqual(0, content.querySelectorAll('#foo').length);\n  testing.expectEqual(1, content.querySelectorAll('#link').length);\n  testing.expectEqual('link', content.querySelectorAll('#link').item(0).id);\n  testing.expectEqual(1, content.querySelectorAll('#para').length);\n  testing.expectEqual('para', content.querySelectorAll('#para').item(0).id);\n  testing.expectEqual(4, content.querySelectorAll('*').length);\n  testing.expectEqual(2, content.querySelectorAll('p').length);\n  testing.expectEqual('link', content.querySelectorAll('.ok').item(0).id);\n</script>\n\n<script id=createdAttributes>\n  let ff = document.createAttribute('foo');\n  content.setAttributeNode(ff);\n  testing.expectEqual('foo', content.getAttributeNode('foo').name);\n  testing.expectEqual('foo', content.removeAttributeNode(ff).name);\n  testing.expectEqual(null, content.getAttributeNode('bar'));\n</script>\n\n<script id=innerHTML>\n  testing.expectEqual(' And', document.getElementById('para').innerHTML);\n  testing.expectEqual('<span id=\"para-empty-child\"></span>', $('#para-empty').innerHTML.trim());\n\n  let h = $('#para-empty');\n  const prev = h.innerHTML;\n  h.innerHTML = '<p id=\"hello\">hello world</p>';\n  testing.expectEqual('<p id=\"hello\">hello world</p>', h.innerHTML);\n  testing.expectEqual('P', h.firstChild.nodeName);\n  testing.expectEqual('hello', h.firstChild.id);\n  testing.expectEqual('hello world', h.firstChild.textContent);\n\n  h.innerHTML = prev;\n  testing.expectEqual('<span id=\"para-empty-child\"></span>', $('#para-empty').innerHTML.trim());\n  testing.expectEqual('<p id=\"para\"> And</p>', $('#para').outerHTML);\n</script>\n\n<script id=matches>\n  const el = document.createElement('div');\n  el.id = 'matches';\n  el.className = 'ok';\n  testing.expectEqual(true, el.matches('#matches'));\n  testing.expectEqual(true, el.matches('.ok'));\n  testing.expectEqual(false, el.matches('.notok'));\n</script>\n\n<script id=scroll>\n  const el3 = document.createElement('div');\n  el3.scrollIntoViewIfNeeded();\n  el3.scrollIntoViewIfNeeded(false);\n  // doesn't throw is good enough\n  testing.skip();\n</script>\n\n<script id=before>\n  const before_container = document.createElement('div');\n  document.append(before_container);\n\n  const b1 = document.createElement('div');\n  before_container.append(b1);\n\n  const b1_a = document.createElement('p');\n  b1.before(b1_a, 'over 9000');\n  testing.expectEqual('<p></p>over 9000<div></div>', before_container.innerHTML);\n</script>\n\n<script id=after>\n  const after_container = document.createElement('div');\n  document.append(after_container);\n  const a1 = document.createElement('div');\n  after_container.append(a1);\n\n  const a1_a = document.createElement('p');\n  a1.after('over 9000', a1_a);\n  testing.expectEqual('<div></div>over 9000<p></p>', after_container.innerHTML);\n</script>\n\n<script id=getElementsByTagName>\n  var div1 = document.createElement('div');\n  div1.innerHTML = \"  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\";\n  testing.expectEqual(1, div1.getElementsByTagName('a').length);\n</script>\n\n<script id=outerHTML>\n  let fc = document.createElement('div')\n  fc.innerHTML = '<script><\\/script>';\n  testing.expectEqual('<div><script><\\/script></div>', fc.outerHTML);\n\n  fc = document.createElement('div')\n  fc.innerHTML = '<script><\\/script><p>hello</p>';\n  testing.expectEqual('<div><script><\\/script><p>hello</p></div>', fc.outerHTML);\n</script>\n\n<script id=remove>\n  const rm = document.createElement('div');\n  testing.expectEqual([], rm.getAttributeNames());\n  rm.id = 'to-remove';\n\n  document.getElementsByTagName('body')[0].appendChild(rm);\n  $('#to-remove').remove();\n  testing.expectEqual(null, $('#to-remove'));\n</script>\n\n<script id=elementDir>\n  const divElement = document.createElement(\"div\");\n  // Always initialized with empty string if `dir` attribute not provided.\n  testing.expectEqual(\"\", divElement.dir);\n\n  divElement.dir = \"ltr\";\n  testing.expectEqual(\"ltr\", divElement.dir);\n\n  divElement.dir = \"rtl\";\n  testing.expectEqual(\"rtl\", divElement.dir);\n\n  divElement.dir = \"auto\";\n  testing.expectEqual(\"auto\", divElement.dir);\n</script>\n\n<script id=linkRel>\n  const linkElement = document.createElement(\"link\");\n  // A newly created link element must have it's rel set to empty string.\n  testing.expectEqual(\"\", linkElement.rel);\n\n  linkElement.rel = \"stylesheet\";\n  testing.expectEqual(\"stylesheet\", linkElement.rel);\n</script>\n\n<!-- This structure will get mutated by insertAdjacentHTML test -->\n<div id=\"insert-adjacent-html-outer-wrapper\">\n    <div id=\"insert-adjacent-html-inner-wrapper\">\n        <span></span>\n        <p>content</p>\n    </div>\n</div>\n\n<script id=insertAdjacentHTML>\n  // Insert \"beforeend\".\n  const wrapper = $(\"#insert-adjacent-html-inner-wrapper\");\n  wrapper.insertAdjacentHTML(\"beforeend\", \"<h1>title</h1>\");\n  let newElement = wrapper.lastElementChild;\n  testing.expectEqual(\"H1\", newElement.tagName);\n  testing.expectEqual(\"title\", newElement.innerText);\n\n  // Insert \"beforebegin\".\n  wrapper.insertAdjacentHTML(\"beforebegin\", \"<h2>small title</h2>\");\n  newElement = wrapper.previousElementSibling;\n  testing.expectEqual(\"H2\", newElement.tagName);\n  testing.expectEqual(\"small title\", newElement.innerText);\n\n  // Insert \"afterend\".\n  wrapper.insertAdjacentHTML(\"afterend\", \"<div id=\\\"afterend\\\">after end</div>\");\n  newElement = wrapper.nextElementSibling;\n  testing.expectEqual(\"DIV\", newElement.tagName);\n  testing.expectEqual(\"after end\", newElement.innerText);\n  testing.expectEqual(\"afterend\", newElement.id);\n\n  // Insert \"afterbegin\".\n  wrapper.insertAdjacentHTML(\"afterbegin\", \"<div class=\\\"afterbegin\\\">after begin</div><yy></yy>\");\n  newElement = wrapper.firstElementChild;\n  testing.expectEqual(\"DIV\", newElement.tagName);\n  testing.expectEqual(\"after begin\", newElement.innerText);\n  testing.expectEqual(\"afterbegin\", newElement.className);\n</script>\n\n<script id=nonBreakingSpace>\n  // Test non-breaking space encoding (critical for React hydration)\n  const div = document.createElement('div');\n  div.innerHTML = 'hello\\xa0world';\n  testing.expectEqual('hello\\xa0world', div.textContent);\n  testing.expectEqual('hello&nbsp;world', div.innerHTML);\n\n  // Test that outerHTML also encodes non-breaking spaces correctly\n  const p = document.createElement('p');\n  p.textContent = 'XAnge\\xa0Privacy';\n  testing.expectEqual('<p>XAnge&nbsp;Privacy</p>', p.outerHTML);\n</script> -->\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/event_target.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"content\"><p id=para></p></div>\n\n<script id=eventTarget>\n  testing.expectEqual('[object EventTarget]', new EventTarget().toString());\n\n  let content = $('#content');\n  let para = $('#para');\n\n  var nb = 0;\n  var evt;\n  var phase;\n  var cur;\n\n  function reset() {\n    nb = 0;\n    evt = undefined;\n    phase = undefined;\n    cur = undefined;\n  }\n\n  function cbk(event) {\n    evt = event;\n    phase = event.eventPhase;\n    cur = event.currentTarget;\n    nb++;\n  }\n\n  content.addEventListener('basic', cbk);\n  content.dispatchEvent(new Event('basic'));\n  testing.expectEqual(1, nb);\n  testing.expectEqual(true, evt instanceof Event);\n  testing.expectEqual('basic', evt.type);\n  testing.expectEqual(2, phase);\n  testing.expectEqual('content', cur.getAttribute('id'));\n\n  reset();\n  para.dispatchEvent(new Event('basic'))\n\n  // handler is not called, no capture, not the targeno bubbling\n  testing.expectEqual(0, nb);\n  testing.expectEqual(undefined, evt);\n\n  reset();\n  content.addEventListener('basic', cbk);\n  content.dispatchEvent(new Event('basic'))\n  testing.expectEqual(1, nb);\n\n  reset();\n  content.addEventListener('basic', cbk, true);\n  content.dispatchEvent(new Event('basic'));\n  testing.expectEqual(2, nb);\n\n  reset()\n  content.removeEventListener('basic', cbk);\n  content.dispatchEvent(new Event('basic'));\n  testing.expectEqual(1, nb);\n\n  reset();\n  content.removeEventListener('basic', cbk, {capture: true});\n  content.dispatchEvent(new Event('basic'));\n  testing.expectEqual(0, nb);\n\n  reset();\n  content.addEventListener('capture', cbk, true);\n  content.dispatchEvent(new Event('capture'));\n  testing.expectEqual(1, nb);\n  testing.expectEqual(true, evt instanceof Event);\n  testing.expectEqual('capture', evt.type);\n  testing.expectEqual(2, phase);\n  testing.expectEqual('content', cur.getAttribute('id'));\n\n  reset();\n  para.dispatchEvent(new Event('capture'));\n  testing.expectEqual(1, nb);\n  testing.expectEqual(true, evt instanceof Event);\n  testing.expectEqual('capture', evt.type);\n  testing.expectEqual(1, phase);\n  testing.expectEqual('content', cur.getAttribute('id'));\n\n  reset();\n  content.addEventListener('bubbles', cbk);\n  content.dispatchEvent(new Event('bubbles', {bubbles: true}));\n  testing.expectEqual(1, nb);\n  testing.expectEqual(true, evt instanceof Event);\n  testing.expectEqual('bubbles', evt.type);\n  testing.expectEqual(2, phase);\n  testing.expectEqual('content', cur.getAttribute('id'));\n\n  reset();\n  para.dispatchEvent(new Event('bubbles', {bubbles: true}));\n  testing.expectEqual(1, nb);\n  testing.expectEqual(true, evt instanceof Event);\n  testing.expectEqual('bubbles', evt.type);\n  testing.expectEqual(3, phase);\n  testing.expectEqual('content', cur.getAttribute('id'));\n\n\n  const obj1 = {\n    calls: 0,\n    handleEvent: function() { this.calls += 1 }\n  };\n  content.addEventListener('he', obj1);\n  content.dispatchEvent(new Event('he'));\n  testing.expectEqual(1, obj1.calls);\n\n  content.removeEventListener('he', obj1);\n  content.dispatchEvent(new Event('he'));\n  testing.expectEqual(1, obj1.calls);\n\n  // doesn't crash on null receiver\n  content.addEventListener('he2', null);\n  content.dispatchEvent(new Event('he2'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/exceptions.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"content\">\n  <a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n</div>\n\n<script id=exceptions>\n  let content = $('#content');\n  let link = $('#link');\n\n  testing.withError((err) => {\n    testing.expectEqual(3, err.code);\n    testing.expectEqual('Hierarchy Error', err.message);\n    testing.expectEqual('HierarchyRequestError: Hierarchy Error', err.toString());\n    testing.expectEqual(true, err instanceof DOMException);\n    testing.expectEqual(true, err instanceof Error);\n  }, () => link.appendChild(content));\n</script>\n\n<script id=constructor>\n  let exc0 = new DOMException();\n  testing.expectEqual('Error', exc0.name);\n  testing.expectEqual(0, exc0.code);\n  testing.expectEqual('', exc0.message);\n  testing.expectEqual('Error', exc0.toString());\n\n  let exc1 = new DOMException('Sandwich malfunction');\n  testing.expectEqual('Error', exc1.name);\n  testing.expectEqual(0, exc1.code);\n  testing.expectEqual('Sandwich malfunction', exc1.message);\n  testing.expectEqual('Error: Sandwich malfunction', exc1.toString());\n\n  let exc2 = new DOMException('Caterpillar turned into a butterfly', 'NoModificationAllowedError');\n  testing.expectEqual('NoModificationAllowedError', exc2.name);\n  testing.expectEqual(7, exc2.code);\n  testing.expectEqual('Caterpillar turned into a butterfly', exc2.message);\n  testing.expectEqual('NoModificationAllowedError: Caterpillar turned into a butterfly', exc2.toString());\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/html_collection.html",
    "content": "<!DOCTYPE html>\n<body>\n  <div id=\"content\">\n    <a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n    <p id=\"para-empty\" class=\"ok empty\">\n      <span id=\"para-empty-child\"></span>\n    </p>\n    <p id=\"para\"> And</p>\n    <!--comment-->\n  </div>\n</body>\n\n<script src=\"../testing.js\"></script>\n<script id=caseInsensitve>\n    const Ptags = document.getElementsByTagName('P');\n    testing.expectEqual(2, Ptags.length);\n    testing.expectEqual('p', Ptags.item(0).localName);\n    testing.expectEqual('p', Ptags.item(1).localName);\n</script>\n\n<script id=all>\n  let allTags = document.getElementsByTagName('*');\n  testing.expectEqual(11, allTags.length);\n  testing.expectEqual('html', allTags.item(0).localName);\n  testing.expectEqual('html', allTags.item(0).localName);\n  testing.expectEqual('head', allTags.item(1).localName);\n  testing.expectEqual('html', allTags.item(0).localName);\n  testing.expectEqual('body', allTags.item(2).localName);\n  testing.expectEqual('div', allTags.item(3).localName);\n  testing.expectEqual('p', allTags.item(7).localName);\n  testing.expectEqual('span', allTags.namedItem('para-empty-child').localName);\n\n\n  // array like\n  testing.expectEqual('html', allTags[0].localName);\n  testing.expectEqual('p', allTags[7].localName);\n  testing.expectEqual(undefined, allTags[14]);\n  testing.expectEqual('span', allTags['para-empty-child'].localName);\n  testing.expectEqual(undefined, allTags['foo']);\n</script>\n\n<script id=element>\n  let content = $('#content');\n  testing.expectEqual(4, content.getElementsByTagName('*').length);\n  testing.expectEqual(2, content.getElementsByTagName('p').length);\n  testing.expectEqual(0, content.getElementsByTagName('div').length);\n\n  testing.expectEqual(1, document.children.length);\n  testing.expectEqual(3, content.children.length);\n</script>\n\n<script id=liveness>\n  const ptags = document.getElementsByTagName('p');\n  testing.expectEqual(2, ptags.length);\n  testing.expectEqual(' And', ptags.item(1).textContent);\n\n  let p = document.createElement('p');\n  p.textContent = 'OK live';\n  // hasn't been added, still 2\n  testing.expectEqual(2, ptags.length);\n\n  testing.expectEqual(true, content.appendChild(p) != undefined);\n  testing.expectEqual(3, ptags.length);\n  testing.expectEqual('OK live', ptags.item(2).textContent);\n  testing.expectEqual(true, content.insertBefore(p, $('#para-empty')) != undefined);\n  testing.expectEqual('OK live', ptags.item(0).textContent);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/implementation.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=implementation>\n  let impl = document.implementation;\n  testing.expectEqual(\"[object HTMLDocument]\", impl.createHTMLDocument().toString());;\n\n  const doc = impl.createHTMLDocument('foo');\n  testing.expectEqual(\"[object HTMLDocument]\", doc.toString());\n  testing.expectEqual(\"foo\", doc.title);\n  testing.expectEqual(\"[object HTMLBodyElement]\", doc.body.toString());\n  testing.expectEqual(\"[object XMLDocument]\", impl.createDocument(null, 'foo').toString());\n  testing.expectEqual(\"[object DocumentType]\", impl.createDocumentType('foo', 'bar', 'baz').toString());\n  testing.expectEqual(true, impl.hasFeature());\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/intersection_observer.html",
    "content": "<!DOCTYPE html>\n<body></body>\n<script src=\"../testing.js\"></script>\n\n<script id=intersectionObserver>\n  {\n    // never attached\n    let count = 0;\n    const div = document.createElement('div');\n    new IntersectionObserver((entries) => {\n      count += 1;\n    }).observe(div);\n\n    testing.eventually(() => {\n      testing.expectEqual(0, count);\n    });\n  }\n\n  {\n    // not connected, but has parent\n    let count = 0;\n    const div1 = document.createElement('div');\n    const div2 = document.createElement('div');\n    new IntersectionObserver((entries) => {\n      console.log(entries[0]);\n      count += 1;\n    }).observe(div1);\n\n    div2.appendChild(div1);\n    testing.eventually(() => {\n      testing.expectEqual(1, count);\n    });\n  }\n</script>\n\n<script id=reobserve>\n  {\n    let count = 0;\n    let observer = new IntersectionObserver(entries => {\n      count += entries.length;\n    });\n\n    const div1 = document.createElement('div');\n    document.body.appendChild(div1);\n\n    // cannot fire synchronously, must be on the next tick\n    testing.expectEqual(0, count);\n    observer.observe(div1);\n    testing.expectEqual(0, count);\n    observer.observe(div1);\n    observer.observe(div1);\n    testing.expectEqual(0, count);\n\n    testing.eventually(() => {\n      testing.expectEqual(1, count);\n    });\n  }\n</script>\n\n<script id=unobserve>\n  {\n    let count = 0;\n    let observer = new IntersectionObserver(entries => {\n      count += entries.length;\n    });\n\n    const div1 = document.createElement('div');\n    document.body.appendChild(div1);\n\n    testing.expectEqual(0, count);\n    observer.observe(div1);\n    testing.expectEqual(0, count);\n    observer.observe(div1);\n    observer.observe(div1);\n    testing.expectEqual(0, count);\n\n    observer.unobserve(div1);\n    testing.eventually(() => {\n      testing.expectEqual(0, count);\n    });\n  }\n</script>\n\n<script id=disconnect>\n  {\n    let count = 0;\n    let observer = new IntersectionObserver(entries => {\n      count += entries.length;\n    });\n\n    const div1 = document.createElement('div');\n    document.body.appendChild(div1);\n\n    // cannot fire synchronously, must be on the next tick\n    testing.expectEqual(0, count);\n    observer.observe(div1);\n    testing.expectEqual(0, count);\n    observer.observe(div1);\n    observer.observe(div1);\n    testing.expectEqual(0, count);\n    observer.disconnect();\n\n    testing.eventually(() => {\n      testing.expectEqual(0, count);\n    });\n  }\n</script>\n\n<script id=entry>\n  {\n    let entry = null;\n    let observer = new IntersectionObserver(entries => {\n      entry = entries[0];\n    });\n\n    let div1 = document.createElement('div');\n    document.body.appendChild(div1);\n    new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);\n\n    testing.eventually(() => {\n      testing.expectEqual(125, entry.boundingClientRect.x);\n      testing.expectEqual(1, entry.intersectionRatio);\n      testing.expectEqual(125, entry.intersectionRect.x);\n      testing.expectEqual(125, entry.intersectionRect.y);\n      testing.expectEqual(5, entry.intersectionRect.width);\n      testing.expectEqual(5, entry.intersectionRect.height);\n      testing.expectEqual(true, entry.isIntersecting);\n      testing.expectEqual(0, entry.rootBounds.x);\n      testing.expectEqual(0, entry.rootBounds.y);\n      testing.expectEqual(1920, entry.rootBounds.width);\n      testing.expectEqual(1080, entry.rootBounds.height);\n      testing.expectEqual('[object HTMLDivElement]', entry.target.toString());\n    });\n  }\n</script>\n\n<script id=timing>\n  {\n    const capture = [];\n    const observer = new IntersectionObserver(() => {\n      capture.push('callback');\n    });\n\n    const div = document.createElement('div');\n    capture.push('pre-append');\n    document.body.appendChild(div);\n    capture.push('post-append');\n\n    capture.push('pre-observe');\n    observer.observe(div);\n    capture.push('post-observe');\n\n    testing.eventually(() => {\n      testing.expectEqual([\n        'pre-append',\n        'post-append',\n        'pre-observe',\n        'post-observe',\n        'callback',\n      ], capture)\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/named_node_map.html",
    "content": "<!DOCTYPE html>\n<div id=\"content\"></div>\n\n<script src=\"../testing.js\"></script>\n<script id=namedNodeMap>\n  let a = document.getElementById('content').attributes;\n  testing.expectEqual(1, a.length);\n  testing.expectEqual('[object Attr]', a.item(0).toString());\n  testing.expectEqual(null, a.item(1));\n  testing.expectEqual('[object Attr]', a.getNamedItem('id').toString());\n  testing.expectEqual(null, a.getNamedItem('foo'));\n  testing.expectEqual('[object Attr]', a.setNamedItem(a.getNamedItem('id')).toString());\n\n  testing.expectEqual('id', a['id'].name);\n  testing.expectEqual('content', a['id'].value);\n  testing.expectEqual(undefined, a['other']);\n  a[0].value = 'abc123';\n  testing.expectEqual('abc123', a[0].value);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/node_filter.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<!-- Test fixture -->\n<div id=\"container\">\n  <!-- comment1 -->\n  <div id=\"outer\">\n    <!-- comment2 -->\n    <span id=\"inner\">\n      <!-- comment3 -->\n      Text content\n      <!-- comment4 -->\n    </span>\n    <!-- comment5 -->\n  </div>\n  <!-- comment6 -->\n</div>\n\n<script id=nodeFilter>\n  testing.expectEqual(1, NodeFilter.FILTER_ACCEPT);\n  testing.expectEqual(2, NodeFilter.FILTER_REJECT);\n  testing.expectEqual(3, NodeFilter.FILTER_SKIP);\n  testing.expectEqual(4294967295, NodeFilter.SHOW_ALL);\n  testing.expectEqual(129, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);\n</script>\n\n<script id=treeWalkerComments>\n  {\n    const container = $('#container');\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_COMMENT,\n      null,\n      false\n    );\n\n    const comments = [];\n    let node;\n    while (node = walker.nextNode()) {\n      comments.push(node.data.trim());\n    }\n\n    // Should find all 6 comments, including those nested inside elements\n    testing.expectEqual(6, comments.length);\n    testing.expectEqual('comment1', comments[0]);\n    testing.expectEqual('comment2', comments[1]);\n    testing.expectEqual('comment3', comments[2]);\n    testing.expectEqual('comment4', comments[3]);\n    testing.expectEqual('comment5', comments[4]);\n    testing.expectEqual('comment6', comments[5]);\n  }\n</script>\n\n<script id=treeWalkerElements>\n  {\n    const container = $('#container');\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      null,\n      false\n    );\n\n    const elements = [];\n    let node;\n    while (node = walker.nextNode()) {\n      if (node.id) {\n        elements.push(node.id);\n      }\n    }\n\n    // Should find the 2 nested elements (outer and inner)\n    testing.expectEqual(2, elements.length);\n    testing.expectEqual('outer', elements[0]);\n    testing.expectEqual('inner', elements[1]);\n  }\n</script>\n\n<script id=treeWalkerAll>\n  {\n    const container = $('#container');\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ALL,\n      null,\n      false\n    );\n\n    let commentCount = 0;\n    let elementCount = 0;\n    let textCount = 0;\n\n    let node;\n    while (node = walker.nextNode()) {\n      if (node.nodeType === 8) commentCount++;      // Comment\n      else if (node.nodeType === 1) elementCount++;  // Element\n      else if (node.nodeType === 3) textCount++;     // Text\n    }\n\n    testing.expectEqual(6, commentCount);\n    testing.expectEqual(2, elementCount);\n    testing.expectEqual(true, textCount > 0);\n  }\n</script>\n\n<script id=treeWalkerCombined>\n  {\n    const container = $('#container');\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,\n      null,\n      false\n    );\n\n    let commentCount = 0;\n    let elementCount = 0;\n\n    let node;\n    while (node = walker.nextNode()) {\n      if (node.nodeType === 8) commentCount++;      // Comment\n      else if (node.nodeType === 1) elementCount++;  // Element\n    }\n\n    // Should find 6 comments and 2 elements, but no text nodes\n    testing.expectEqual(6, commentCount);\n    testing.expectEqual(2, elementCount);\n  }\n</script>\n\n<script id=treeWalkerCustomFilter>\n  {\n    const container = $('#container');\n\n    // Filter that accepts only elements with id\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      {\n        acceptNode: function(node) {\n          return node.id ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n        }\n      },\n      false\n    );\n\n    const elements = [];\n    let node;\n    while (node = walker.nextNode()) {\n      elements.push(node.id);\n    }\n\n    // Should find only elements with id (outer and inner)\n    testing.expectEqual(2, elements.length);\n    testing.expectEqual('outer', elements[0]);\n    testing.expectEqual('inner', elements[1]);\n  }\n</script>\n\n<script id=nodeIteratorComments>\n  {\n    const container = $('#container');\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_COMMENT,\n      null,\n      false\n    );\n\n    const comments = [];\n    let node;\n    while (node = iterator.nextNode()) {\n      comments.push(node.data.trim());\n    }\n\n    // Should find all 6 comments, including those nested inside elements\n    testing.expectEqual(6, comments.length);\n    testing.expectEqual('comment1', comments[0]);\n    testing.expectEqual('comment2', comments[1]);\n    testing.expectEqual('comment3', comments[2]);\n    testing.expectEqual('comment4', comments[3]);\n    testing.expectEqual('comment5', comments[4]);\n    testing.expectEqual('comment6', comments[5]);\n  }\n</script>\n\n<script id=reactLikeScenario>\n  {\n    // Test a React-like scenario with comment markers\n    const div = document.createElement('div');\n    div.innerHTML = `\n      <a href=\"/\">\n        <!--$-->\n        <svg viewBox=\"0 0 10 10\">\n          <path d=\"M0,0 L10,10\" />\n        </svg>\n        <!--/$-->\n      </a>\n    `;\n\n    const walker = document.createTreeWalker(\n      div,\n      NodeFilter.SHOW_COMMENT,\n      null,\n      false\n    );\n\n    const comments = [];\n    let node;\n    while (node = walker.nextNode()) {\n      comments.push(node.data);\n    }\n\n    // Should find both React markers even though they're nested inside <a>\n    testing.expectEqual(2, comments.length);\n    testing.expectEqual('$', comments[0]);\n    testing.expectEqual('/$', comments[1]);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/node_list.html",
    "content": "<!DOCTYPE html>\n<div id=\"content\">\n  <a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n  <p id=\"para-empty\" class=\"ok empty\">\n    <span id=\"para-empty-child\"></span>\n  </p>\n  <p id=\"para\"> And</p>\n  <!--comment-->\n</div>\n\n<script src=\"../testing.js\"></script>\n<script id=nodeList>\n  let list = document.getElementById('content').childNodes;\n  testing.expectEqual(9, list.length);\n  testing.expectEqual('Text', list[0].__proto__.constructor.name);\n  let i = 0;\n  list.forEach(function (n, idx) { i += idx; });\n  testing.expectEqual(36, i);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/node_owner.html",
    "content": "<!DOCTYPE html>\n<div id=\"target-container\">\n  <p id=\"reference-node\">\n  I am the original reference node.\n  </p>\n</div>\n\n<script src=\"../testing.js\"></script>\n<script id=nodeOwner>\n  const parser = new DOMParser();\n  const newDoc = parser.parseFromString('<div id=\"new-node\"><p>Hey</p><span>Marked</span></div>', 'text/html');\n  const newNode = newDoc.getElementById('new-node');\n  const parent = $('#target-container');\n  const referenceNode = $('#reference-node');\n\n  parent.insertBefore(newNode, referenceNode);\n  const k = $('#new-node');\n  const ptag = k.querySelector('p');\n  const spanTag = k.querySelector('span');\n  const anotherDoc = parser.parseFromString('<div id=\"another-new-node\"></div>', 'text/html');\n  const anotherNewNode = anotherDoc.getElementById('another-new-node');\n  testing.expectEqual('[object HTMLDivElement]', parent.appendChild(anotherNewNode).toString());\n\n\n  testing.expectEqual(newNode.ownerDocument, parent.ownerDocument);\n  testing.expectEqual(anotherNewNode.ownerDocument, parent.ownerDocument);\n  testing.expectEqual('P', newNode.firstChild.nodeName);\n  testing.expectEqual(parent.ownerDocument, ptag.ownerDocument);\n  testing.expectEqual(parent.ownerDocument, spanTag.ownerDocument);\n  testing.expectEqual(true, parent.contains(newNode));\n  testing.expectEqual(true, parent.contains(anotherNewNode));\n  testing.expectEqual(false, anotherDoc.contains(anotherNewNode));\n  testing.expectEqual(false, newDoc.contains(newNode));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/performance.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=performance>\n  let performance = window.performance;\n  testing.expectEqual(true, performance instanceof Performance);\n\n  let mark1 = performance.mark(\"start\");\n  testing.expectEqual(true, mark1 instanceof PerformanceMark);\n  testing.expectEqual('start', mark1.name);\n  testing.expectEqual('mark', mark1.entryType);\n  testing.expectEqual(0, mark1.duration);\n  testing.expectEqual(null, mark1.detail);\n\n  let mark2 = performance.mark(\"start\", {startTime: 32939393.9});\n  testing.expectEqual(32939393.9, mark2.startTime);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/performance_observer.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=performanceObserver>\n  testing.expectEqual(0, PerformanceObserver.supportedEntryTypes.length);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/processing_instruction.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=processingInstruction>\n  let pi = document.createProcessingInstruction('foo', 'bar');\n  testing.expectEqual('foo', pi.target);\n  testing.expectEqual('bar', pi.data);\n  pi.data = 'foo';\n  testing.expectEqual('foo', pi.data);\n\n  let pi2 = pi.cloneNode();\n  testing.expectEqual(7, pi2.nodeType);\n\n  let pi11 = document.createProcessingInstruction('target1', 'data1');\n  let pi12 = document.createProcessingInstruction('target2', 'data2');\n  let pi13 = document.createProcessingInstruction('target1', 'data1');\n  testing.expectEqual(true, pi11.isEqualNode(pi11));\n  testing.expectEqual(true, pi11.isEqualNode(pi13));\n  testing.expectEqual(false, pi11.isEqualNode(pi12));\n  testing.expectEqual(false, pi12.isEqualNode(pi13));\n  testing.expectEqual(false, pi11.isEqualNode(document));\n  testing.expectEqual(false, document.isEqualNode(pi11));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/range.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=content><!-- hello world --><p>over 9000</p></div>\n\n<script id=\"constructor\">\n  let range = new Range();\n  testing.expectEqual(true, range instanceof Range);\n  testing.expectEqual(true, range instanceof AbstractRange);\n\n// Test initial state - collapsed range\n  testing.expectEqual(true, range.collapsed);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(0, range.endOffset);\n  testing.expectEqual(true, range.startContainer instanceof HTMLDocument);\n  testing.expectEqual(true, range.endContainer instanceof HTMLDocument);\n</script>\n\n<script id=\"createRange\">\n  let docRange = document.createRange();\n  testing.expectEqual(true, docRange instanceof Range);\n  testing.expectEqual(true, docRange.collapsed);\n</script>\n\n<script id=textRange>\n  const container = $('#content');\n  const commentNode = container.childNodes[0];\n  testing.expectEqual(' hello world ', commentNode.nodeValue);\n\n  const textRange = document.createRange();\n  textRange.selectNodeContents(commentNode);\n  testing.expectEqual(0, textRange.startOffset);\n  testing.expectEqual(13, textRange.endOffset); // length of comment\n</script>\n\n<script id=nodeRange>\n  const nodeRange = document.createRange();\n  nodeRange.selectNodeContents(container);\n  testing.expectEqual(0, nodeRange.startOffset);\n  testing.expectEqual(2, nodeRange.endOffset); // length of container.childNodes\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/text.html",
    "content": "<!DOCTYPE html>\n<a id=\"link\" href=\"foo\" class=\"ok\">OK</a>\n\n<script src=\"../testing.js\"></script>\n<script id=text>\n  let t = new Text('foo');\n  testing.expectEqual('foo', t.data);\n\n  let emptyt = new Text();\n  testing.expectEqual('', emptyt.data);\n\n  let text = $('#link').firstChild;\n  testing.expectEqual('OK', text.wholeText);\n\n  text.data = 'OK modified';\n  let split = text.splitText('OK'.length);\n  testing.expectEqual(' modified', split.data);\n  testing.expectEqual('OK', text.data);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/dom/token_list.html",
    "content": "<!DOCTYPE html>\n<p id=\"para-empty\" class=\"ok empty\">\n\n<script src=\"../testing.js\"></script>\n<script id=tokenList>\n  let gs = $('#para-empty');\n\n  let cl = gs.classList;\n  testing.expectEqual('ok empty', gs.className);\n  testing.expectEqual('ok empty', cl.value);\n  testing.expectEqual(2, cl.length);\n\n  gs.className = 'foo bar baz';\n  testing.expectEqual('foo bar baz', gs.className);\n  testing.expectEqual(3, cl.length);\n\n  gs.className = 'ok empty';\n  testing.expectEqual(2, cl.length);\n\n  let cl2 = gs.classList;\n  testing.expectEqual(2, cl2.length);\n  testing.expectEqual('ok', cl2.item(0));\n  testing.expectEqual('empty', cl2.item(1));\n  testing.expectEqual(true, cl2.contains('ok'));\n  testing.expectEqual(false, cl2.contains('nok'));\n\n  cl2.add('foo', 'bar', 'baz');\n  testing.expectEqual(5, cl2.length);\n\n  cl2.remove('foo', 'bar', 'baz');\n  testing.expectEqual(2, cl2.length);\n\n  let cl3 = gs.classList;\n  testing.expectEqual(false, cl3.toggle('ok'));\n  testing.expectEqual(true, cl3.toggle('ok'));\n  testing.expectEqual(2, cl3.length);\n\n  let cl4 = gs.classList;\n  testing.expectEqual(true, cl4.replace('ok', 'nok'));\n  testing.expectEqual(\"empty nok\", cl4.value);\n  testing.expectEqual(true, cl4.replace('nok', 'ok'));\n  testing.expectEqual(\"empty ok\", cl4.value);\n\n  let cl5 = gs.classList;\n  let keys = [...cl5.keys()];\n  testing.expectEqual(2, keys.length);\n  testing.expectEqual(0, keys[0]);\n  testing.expectEqual(1, keys[1]);\n\n  let values = [...cl5.values()];\n  testing.expectEqual(2, values.length);\n  testing.expectEqual('empty', values[0]);\n  testing.expectEqual('ok', values[1]);\n\n  let entries = [...cl5.entries()];\n  testing.expectEqual(2, entries.length);\n  testing.expectEqual([0, 'empty'], entries[0]);\n  testing.expectEqual([1, 'ok'], entries[1]);\n\n  let cl6 = gs.classList;\n  cl6.value = 'a  b  ccc';\n  testing.expectEqual('a  b  ccc', cl6.value);\n  testing.expectEqual('a  b  ccc', cl6.toString());\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/encoding/decoder.html",
    "content": "<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n\n<script src=\"../testing.js\"></script>\n<script id=decoder>\n  let d1 = new TextDecoder();\n  testing.expectEqual('utf-8', d1.encoding);\n  testing.expectEqual(false, d1.fatal);\n  testing.expectEqual(false, d1.ignoreBOM);\n\n  testing.expectEqual('', d1.decode());\n  testing.expectEqual('𠮷', d1.decode(new Uint8Array([240, 160, 174, 183])));\n  testing.expectEqual('𠮷', d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183])));\n  testing.expectEqual('�2', d1.decode(new Uint8Array([249, 50])));\n\n  {\n    const buffer = new ArrayBuffer(4);\n    const ints = new Uint8Array(buffer)\n    ints[0] = 240;\n    ints[1] = 160;\n    ints[2] = 174;\n    ints[3] = 183;\n    testing.expectEqual('𠮷', d1.decode(buffer));\n  }\n\n  {\n    const buffer = new ArrayBuffer(4);\n    const dv = new DataView(buffer);\n    dv.setUint8(0, 240);\n    dv.setUint8(1, 160);\n    dv.setUint8(2, 174);\n    dv.setUint8(3, 183);\n    testing.expectEqual('𠮷', d1.decode(dv));\n  }\n\n  let d2 = new TextDecoder('utf8', {fatal: true})\n  testing.expectError('Error: InvalidUtf8', () => {\n    let data  = new Uint8Array([240, 240, 160, 174, 183]);\n    d2.decode(data);\n  });\n</script>\n\n<script id=stream>\n  let d3 = new TextDecoder();\n  testing.expectEqual('', d2.decode(new Uint8Array([226, 153]), { stream: true }));\n  testing.expectEqual('♥', d2.decode(new Uint8Array([165]), { stream: true }));\n</script>\n\n<script id=slice>\n    const buf1 = new ArrayBuffer(7);\n    const arr1 = new Uint8Array(buf1)\n    arr1[0] = 80;\n    arr1[1] = 81;\n    arr1[2] = 82;\n    arr1[3] = 83;\n    arr1[4] = 84;\n    arr1[5] = 85;\n    arr1[6] = 86;\n    testing.expectEqual('RST', d3.decode(new Uint8Array(buf1, 2, 3)));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/encoding/encoder.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=encoder>\n  var encoder = new TextEncoder();\n  testing.expectEqual('utf-8', encoder.encoding);\n  testing.expectEqual([226, 130, 172], Array.from(encoder.encode('€')));\n\n  // Invalid utf-8 sequence.\n  // Browsers give a different result for this, they decode it to:\n  //    50, 50, 54, 44, 52, 48, 44, 49, 54, 49\n  testing.expectError('Error: InvalidUtf8', () => {\n    encoder.encode(new Uint8Array([0xE2,0x28,0xA1]));\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/events/composition.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=noNata>\n  {\n    let event = new CompositionEvent(\"test\", {});\n    testing.expectEqual(true, event instanceof CompositionEvent);\n    testing.expectEqual(true, event instanceof Event);\n\n    testing.expectEqual(\"test\", event.type);\n    testing.expectEqual(\"\", event.data);\n  }\n</script>\n\n<script id=withData>\n  {\n    let event = new CompositionEvent(\"test2\", {data: \"over 9000!\"});\n    testing.expectEqual(\"test2\", event.type);\n    testing.expectEqual(\"over 9000!\", event.data);\n  }\n</script>\n\n<script id=dispatch>\n  {\n    let called = 0;\n    document.addEventListener('CE', (e) => {\n      testing.expectEqual('test-data', e.data);\n      testing.expectEqual(true, e instanceof CompositionEvent);\n      called += 1\n    });\n\n    document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));\n    testing.expectEqual(1, called);\n  }\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/legacy/events/custom.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=custom>\n  let capture = null;\n  const el = document.createElement('div');\n  el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)});\n  el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)});\n\n  el.dispatchEvent(new CustomEvent('c1'));\n  testing.expectEqual(\"c1-null\", capture);\n\n  el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));\n  testing.expectEqual(\"c1-123\", capture);\n\n  el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));\n  testing.expectEqual(\"c2-9000\", capture);\n\n  let window_calls = 0;\n  window.addEventListener('c1', () => {\n    window_calls += 1;\n  });\n\n  el.dispatchEvent(new CustomEvent('c1', {bubbles: true}));\n  testing.expectEqual(1, window_calls);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/events/event.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=content>\n  <p id=\"para\"></p>\n</div>\n\n<script id=dispatch>\n  const startTime =  new Event('x').timeStamp;\n\n  let content = $('#content');\n  // let para = document.getElementById('para');\n  var nb = 0;\n  var evt = null;\n\n  const incrementCallback = function(e) {\n    evt = e;\n    nb += 1;\n    e.preventDefault();\n  }\n\n  content.addEventListener('dispatch', incrementCallback);\n\n  content.dispatchEvent(new Event('dispatch', {bubbles: true, cancelable: true}));\n  testing.expectEqual(1, nb);\n  testing.expectEqual(content, evt.target);\n  testing.expectEqual(true, evt.bubbles);\n  testing.expectEqual(true, evt.cancelable);\n  testing.expectEqual(true, evt.defaultPrevented);\n  testing.expectEqual(false, evt.isTrusted);\n  testing.expectEqual(true, evt.timeStamp >= Math.floor(startTime));\n</script>\n\n<script id=propogate>\n  nb = 0;\n  let para = $('#para');\n  // the stop listener is capturing, so it propogates down\n  content.addEventListener('stop',function(e) {\n    e.stopPropagation();\n    nb += 1;\n   }, true)\n\n  para.addEventListener('stop',function(e) {\n    nb += 10;\n  });\n\n  para.dispatchEvent(new Event('stop'));\n  // didn't propogate down (because of capturing) to para handler\n  testing.expectEqual(1, nb);\n</script>\n\n<script id=immediate>\n  nb = 0;\n\n  content.addEventListener('immediate', function(e) {\n    e.stopImmediatePropagation();\n    nb += 1;\n  });\n\n  // the following event listener will not be invoked\n  content.addEventListener('immediate', function(e) {\n    nb += 10;\n  });\n\n  content.dispatchEvent(new Event('immediate'));\n  testing.expectEqual(1, nb);\n</script>\n\n<script id=legacy>\n  nb = 0;\n  content.addEventListener('legacy', incrementCallback);\n  let evtLegacy = document.createEvent('Event');\n  evtLegacy.initEvent('legacy');\n  content.dispatchEvent(evtLegacy);\n  testing.expectEqual(1, nb);\n</script>\n\n<script id=removeListener>\n  nb = 0;\n  document.addEventListener('count', incrementCallback);\n  document.removeEventListener('count', incrementCallback);\n  document.dispatchEvent(new Event('count'));\n  testing.expectEqual(0, nb);\n</script>\n\n<script id=once>\n  document.addEventListener('count', incrementCallback, {once: true});\n  document.dispatchEvent(new Event('count'));\n  document.dispatchEvent(new Event('count'));\n  document.dispatchEvent(new Event('count'));\n  testing.expectEqual(1, nb);\n</script>\n\n<script id=abortController>\n  nb = 0;\n\n  let ac = new AbortController()\n  document.addEventListener('count', incrementCallback, {signal: ac.signal})\n  document.dispatchEvent(new Event('count'));\n  document.dispatchEvent(new Event('count'));\n  ac.abort();\n  document.dispatchEvent(new Event('count'));\n  testing.expectEqual(2, nb);\n  document.removeEventListener('count', incrementCallback);\n</script>\n\n<script id=composedPath>\n  testing.expectEqual([], new Event('').composedPath());\n\n  let div1 = document.createElement('div');\n  let sr1 = div1.attachShadow({mode: 'open'});\n  sr1.innerHTML = \"<p id=srp1></p>\";\n  document.getElementsByTagName('body')[0].appendChild(div1);\n\n  let cp = null;\n  const shadowCallback = function(e) {\n    cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString());\n  }\n\n  div1.addEventListener('click', shadowCallback);\n  sr1.getElementById('srp1').click();\n  testing.expectEqual(\n    ['srp1', '#document-fragment', 'DIV', 'BODY', 'HTML', '#document', '[object Window]'],\n    cp\n  );\n\n  let div2 = document.createElement('div');\n  let sr2 = div2.attachShadow({mode: 'closed'});\n  sr2.innerHTML = \"<p id=srp2></p>\";\n  document.getElementsByTagName('body')[0].appendChild(div2);\n\n  cp = null;\n  div2.addEventListener('click', shadowCallback);\n  sr2.getElementById('srp2').click();\n  testing.expectEqual(\n    ['DIV', 'BODY', 'HTML', '#document', '[object Window]'],\n    cp\n  );\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/events/keyboard.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=default>\n  let event = new KeyboardEvent(\"test\", { key: \"a\" });\n  testing.expectEqual(true, event instanceof KeyboardEvent);\n  testing.expectEqual(true, event instanceof Event);\n\n  testing.expectEqual(\"test\", event.type);\n  testing.expectEqual(\"a\", event.key);\n\n  testing.expectEqual(0, event.location);\n  testing.expectEqual(false, event.repeat);\n  testing.expectEqual(false, event.isComposing);\n\n  testing.expectEqual(false, event.ctrlKey);\n  testing.expectEqual(false, event.shiftKey);\n  testing.expectEqual(false, event.metaKey);\n  testing.expectEqual(false, event.altKey);\n</script>\n\n<script id=getModifierState>\n  event = new KeyboardEvent(\"test\", {\n    altKey: true,\n    shiftKey: true,\n    metaKey: true,\n    ctrlKey: true,\n  });\n\n  testing.expectEqual(true, event.getModifierState(\"Alt\"));\n  testing.expectEqual(true, event.getModifierState(\"AltGraph\"));\n  testing.expectEqual(true, event.getModifierState(\"Control\"));\n  testing.expectEqual(true, event.getModifierState(\"Shift\"));\n  testing.expectEqual(true, event.getModifierState(\"Meta\"));\n  testing.expectEqual(false, event.getModifierState(\"OS\"));\n  testing.expectEqual(true, event.getModifierState(\"Accel\"));\n</script>\n\n<script id=keyDownListener>\n  event = new KeyboardEvent(\"keydown\", { key: \"z\" });\n  let isKeyDown = false;\n\n  document.addEventListener(\"keydown\", (e) => {\n    isKeyDown = true;\n\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n    testing.expectEqual(true, e instanceof Event);\n    testing.expectEqual(\"z\", event.key);\n  });\n\n  document.dispatchEvent(event);\n\n  testing.expectEqual(true, isKeyDown);\n</script>\n\n<script id=keyUpListener>\n  event = new KeyboardEvent(\"keyup\", { key: \"x\" });\n  let isKeyUp = false;\n\n  document.addEventListener(\"keyup\", (e) => {\n    isKeyUp = true;\n\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n    testing.expectEqual(true, e instanceof Event);\n    testing.expectEqual(\"x\", event.key);\n  });\n\n  document.dispatchEvent(event);\n\n  testing.expectEqual(true, isKeyUp);\n</script>\n\n<script id=keyPressListener>\n  event = new KeyboardEvent(\"keypress\", { key: \"w\" });\n  let isKeyPress = false;\n\n  document.addEventListener(\"keypress\", (e) => {\n    isKeyPress = true;\n\n    testing.expectEqual(true, e instanceof KeyboardEvent);\n    testing.expectEqual(true, e instanceof Event);\n    testing.expectEqual(\"w\", event.key);\n  });\n\n  document.dispatchEvent(event);\n\n  testing.expectEqual(true, isKeyPress);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/events/mouse.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=default>\n  let event = new MouseEvent('click');\n  testing.expectEqual('click', event.type);\n  testing.expectEqual(true, event instanceof MouseEvent);\n  testing.expectEqual(true, event instanceof Event);\n  testing.expectEqual(0, event.clientX);\n  testing.expectEqual(0, event.clientY);\n  testing.expectEqual(0, event.screenX);\n  testing.expectEqual(0, event.screenY);\n</script>\n\n<script id=parameters>\n  let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 });\n  testing.expectEqual(0, new_event.button);\n  testing.expectEqual(10, new_event.x);\n  testing.expectEqual(20, new_event.y);\n  testing.expectEqual(0, new_event.screenX);\n  testing.expectEqual(0, new_event.screenY);\n</script>\n\n<script id=listener>\n  let me = new MouseEvent('click');\n  testing.expectEqual(true, me instanceof Event);\n\n  var evt = null;\n  document.addEventListener('click', function (e) {\n    evt = e;\n  });\n  document.dispatchEvent(me);\n  testing.expectEqual('click', evt.type);\n  testing.expectEqual(true, evt instanceof MouseEvent);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/fetch/fetch.html",
    "content": "<script src=\"../testing.js\"></script>\n<script id=fetch type=module>\n  const promise1 = new Promise((resolve) => {\n    fetch('http://127.0.0.1:9589/xhr/json')\n    .then((res) => {\n      testing.expectEqual('cors', res.type);\n      return res.json()\n    })\n    .then((json) => {\n      resolve(json);\n    });\n  });\n\n  testing.async(promise1, (json) => {\n    testing.expectEqual({over: '9000!!!'}, json);\n  });\n</script>\n\n<script id=same-origin type=module>\n  const promise1 = new Promise((resolve) => {\n    fetch('http://localhost:9589/xhr/json')\n    .then((res) => {\n      testing.expectEqual('basic', res.type);\n      return res.json()\n    })\n    .then((json) => {\n      resolve(json);\n    });\n  });\n\n  testing.async(promise1, (json) => {\n    testing.expectEqual({over: '9000!!!'}, json);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/fetch/headers.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=headers>\n  let headers = new Headers({\"Set-Cookie\": \"name=world\"});\n  testing.expectEqual(\"name=world\", headers.get(\"set-cookie\"));\n\n  let myHeaders = new Headers();\n  myHeaders.append(\"Content-Type\", \"image/jpeg\"),\n  testing.expectEqual(false, myHeaders.has(\"Picture-Type\"));\n  testing.expectEqual(\"image/jpeg\", myHeaders.get(\"Content-Type\"));\n\n  myHeaders.append(\"Content-Type\", \"image/png\");\n  testing.expectEqual(\"image/jpeg, image/png\", myHeaders.get(\"Content-Type\"));\n\n  myHeaders.delete(\"Content-Type\");\n  testing.expectEqual(null, myHeaders.get(\"Content-Type\"));\n\n  myHeaders.set(\"Picture-Type\", \"image/svg\")\n  testing.expectEqual(\"image/svg\", myHeaders.get(\"Picture-Type\"));\n  testing.expectEqual(true, myHeaders.has(\"Picture-Type\"))\n\n  const originalHeaders = new Headers([[\"Content-Type\", \"application/json\"], [\"Authorization\", \"Bearer token123\"]]);\n  testing.expectEqual(\"application/json\", originalHeaders.get(\"Content-Type\"));\n  testing.expectEqual(\"Bearer token123\", originalHeaders.get(\"Authorization\"));\n\n  const newHeaders = new Headers(originalHeaders);\n  testing.expectEqual(\"application/json\", newHeaders.get(\"Content-Type\"));\n  testing.expectEqual(\"Bearer token123\" ,newHeaders.get(\"Authorization\"));\n  testing.expectEqual(true ,newHeaders.has(\"Content-Type\"));\n  testing.expectEqual(true ,newHeaders.has(\"Authorization\"));\n  testing.expectEqual(false, newHeaders.has(\"X-Custom\"));\n\n  newHeaders.set(\"X-Custom\", \"test-value\");\n  testing.expectEqual(\"test-value\", newHeaders.get(\"X-Custom\"));\n  testing.expectEqual(null, originalHeaders.get(\"X-Custom\"));\n  testing.expectEqual(false, originalHeaders.has(\"X-Custom\"));\n</script>\n\n<script id=keys>\n  const testKeyHeaders = new Headers();\n  testKeyHeaders.set(\"Content-Type\", \"application/json\");\n  testKeyHeaders.set(\"Authorization\", \"Bearer token123\");\n  testKeyHeaders.set(\"X-Custom\", \"test-value\");\n\n  const keys = [];\n  for (const key of testKeyHeaders.keys()) {\n      keys.push(key);\n  }\n\n  testing.expectEqual(3, keys.length);\n  testing.expectEqual(true, keys.includes(\"content-type\"));\n  testing.expectEqual(true, keys.includes(\"authorization\"));\n  testing.expectEqual(true, keys.includes(\"x-custom\"));\n</script>\n\n<script id=values>\n  const testValuesHeaders = new Headers();\n  testValuesHeaders.set(\"Content-Type\", \"application/json\");\n  testValuesHeaders.set(\"Authorization\", \"Bearer token123\");\n  testValuesHeaders.set(\"X-Custom\", \"test-value\");\n\n  const values = [];\n  for (const value of testValuesHeaders.values()) {\n      values.push(value);\n  }\n\n  testing.expectEqual(3, values.length);\n  testing.expectEqual(true, values.includes(\"application/json\"));\n  testing.expectEqual(true, values.includes(\"Bearer token123\"));\n  testing.expectEqual(true, values.includes(\"test-value\"));\n</script>\n\n<script id=entries>\n  const testEntriesHeaders = new Headers();\n  testEntriesHeaders.set(\"Content-Type\", \"application/json\");\n  testEntriesHeaders.set(\"Authorization\", \"Bearer token123\");\n  testEntriesHeaders.set(\"X-Custom\", \"test-value\");\n\n  const entries = [];\n  for (const entry of testEntriesHeaders.entries()) {\n      entries.push(entry);\n  }\n        \n  testing.expectEqual(3, entries.length);\n\n  const entryMap = new Map(entries);\n  testing.expectEqual(\"application/json\", entryMap.get(\"content-type\"));\n  testing.expectEqual(\"Bearer token123\", entryMap.get(\"authorization\"));\n  testing.expectEqual(\"test-value\", entryMap.get(\"x-custom\"));\n  \n  const entryKeys = Array.from(entryMap.keys());\n  testing.expectEqual(3, entryKeys.length);\n  testing.expectEqual(true, entryKeys.includes(\"content-type\"));\n  testing.expectEqual(true, entryKeys.includes(\"authorization\"));\n  testing.expectEqual(true, entryKeys.includes(\"x-custom\"));\n  \n  const entryValues = Array.from(entryMap.values());\n  testing.expectEqual(3, entryValues.length);\n  testing.expectEqual(true, entryValues.includes(\"application/json\"));\n  testing.expectEqual(true, entryValues.includes(\"Bearer token123\"));\n  testing.expectEqual(true, entryValues.includes(\"test-value\"))\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/fetch/request.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=request>\n  let request = new Request(\"flower.png\");\n  testing.expectEqual(\"http://localhost:9589/fetch/flower.png\", request.url);\n  testing.expectEqual(\"GET\", request.method);\n\n  let request2 = new Request(\"https://google.com\", {\n    method: \"POST\",\n    body: \"Hello, World\",\n    cache: \"reload\",\n    credentials: \"omit\",\n    headers: { \"Sender\": \"me\", \"Target\": \"you\" }\n    }\n  );\n  testing.expectEqual(\"https://google.com\", request2.url);\n  testing.expectEqual(\"POST\", request2.method);\n  testing.expectEqual(\"omit\", request2.credentials);\n  testing.expectEqual(\"reload\", request2.cache);\n  testing.expectEqual(\"me\", request2.headers.get(\"SeNdEr\"));\n  testing.expectEqual(\"you\", request2.headers.get(\"target\"));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/fetch/response.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=response>\n  let response = new Response(\"Hello, World!\");\n  testing.expectEqual(200, response.status);\n  testing.expectEqual(\"\", response.statusText);\n  testing.expectEqual(true, response.ok);\n  testing.expectEqual(\"\", response.url);\n  testing.expectEqual(false, response.redirected);\n\n  let response2 = new Response(\"Error occurred\", {\n    status: 404,\n    statusText: \"Not Found\",\n    headers: {\n        \"Content-Type\": \"text/plain\",\n        \"X-Custom\": \"test-value\",\n        \"Cache-Control\": \"no-cache\"\n    }\n  });\n  testing.expectEqual(404, response2.status);\n  testing.expectEqual(\"Not Found\", response2.statusText);\n  testing.expectEqual(false, response2.ok);\n  testing.expectEqual(\"text/plain\", response2.headers.get(\"Content-Type\"));\n  testing.expectEqual(\"test-value\", response2.headers.get(\"X-Custom\"));\n  testing.expectEqual(\"no-cache\", response2.headers.get(\"cache-control\"));\n        \n  let response3 = new Response(\"Created\", { status: 201, statusText: \"Created\" });\n  testing.expectEqual(\"basic\", response3.type);\n  testing.expectEqual(201, response3.status);\n  testing.expectEqual(\"Created\", response3.statusText);\n  testing.expectEqual(true, response3.ok);\n\n  let nullResponse = new Response(null);\n  testing.expectEqual(200, nullResponse.status);\n  testing.expectEqual(\"\", nullResponse.statusText);\n\n  let emptyResponse = new Response(\"\");\n  testing.expectEqual(200, emptyResponse.status);\n</script>\n\n<script id=json type=module>\n  const promise1 = new Promise((resolve) => {\n    let response = new Response('[]');\n    response.json().then(resolve)\n  });\n\n  testing.async(promise1, (json) => {\n    testing.expectEqual([], json);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/file/blob.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=Blob/Blob.text>\n  {\n    const parts = [\"\\r\\nthe quick brown\\rfo\\rx\\r\", \"\\njumps over\\r\\nthe\\nlazy\\r\", \"\\ndog\"];\n    // \"transparent\" ending should not modify the final buffer.\n    const blob = new Blob(parts, { type: \"text/html\" });\n\n    const expected = parts.join(\"\");\n    testing.expectEqual(expected.length, blob.size);\n    testing.expectEqual(\"text/html\", blob.type);\n    testing.async(blob.text(), result => testing.expectEqual(expected, result));\n  }\n\n  {\n    const parts = [\"\\rhello\\r\", \"\\nwor\\r\\nld\"];\n    // \"native\" ending should modify the final buffer.\n    const blob = new Blob(parts, { endings: \"native\" });\n\n    const expected = \"\\nhello\\n\\nwor\\nld\";\n    testing.expectEqual(expected.length, blob.size);\n    testing.async(blob.text(), result => testing.expectEqual(expected, result));\n\n    testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));\n  }\n</script>\n\n<script id=Blob.stream>\n  {\n    const parts = [\"may\", \"thy\", \"knife\", \"chip\", \"and\", \"shatter\"];\n    const blob = new Blob(parts);\n    const reader = blob.stream().getReader();\n\n    testing.async(reader.read(), ({ done, value }) => {\n      const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110,\n                                       105, 102, 101, 99, 104, 105, 112, 97,\n                                       110, 100, 115, 104, 97, 116, 116, 101,\n                                       114]);\n      testing.expectEqual(false, done);\n      testing.expectEqual(true, value instanceof Uint8Array);\n      testing.expectEqual(expected, value);\n    });\n  }\n</script>\n\n<script id=Blob.arrayBuffer/Blob.slice>\n  {\n    const parts = [\"la\", \"symphonie\", \"des\", \"éclairs\"];\n    const blob = new Blob(parts);\n    testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));\n\n    let temp = blob.slice(0);\n    testing.expectEqual(blob.size, temp.size);\n    testing.async(temp.text(), result => {\n      testing.expectEqual(\"lasymphoniedeséclairs\", result);\n    });\n\n    temp = blob.slice(-4, -2, \"custom\");\n    testing.expectEqual(2, temp.size);\n    testing.expectEqual(\"custom\", temp.type);\n    testing.async(temp.text(), result => testing.expectEqual(\"ai\", result));\n\n    temp = blob.slice(14);\n    testing.expectEqual(8, temp.size);\n    testing.async(temp.text(), result => testing.expectEqual(\"éclairs\", result));\n\n    temp = blob.slice(6, -10, \"text/eclair\");\n    testing.expectEqual(6, temp.size);\n    testing.expectEqual(\"text/eclair\", temp.type);\n    testing.async(temp.text(), result => testing.expectEqual(\"honied\", result));\n  }\n</script>\n\n<!-- Firefox and Safari only -->\n<script id=Blob.bytes>\n  {\n    const parts = [\"light \", \"panda \", \"rocks \", \"!\"];\n    const blob = new Blob(parts);\n\n    testing.async(blob.bytes(), result => {\n      const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97,\n                                       110, 100, 97, 32, 114, 111, 99, 107, 115,\n                                       32, 33]);\n      testing.expectEqual(true, result instanceof Uint8Array);\n      testing.expectEqual(expected, result);\n    });\n  }\n\n  // Test for SIMD.\n  {\n    const parts = [\n      \"\\rThe opened package\\r\\nof potato\\nchi\\rps\",\n      \"held the\\r\\nanswer to the\\r mystery. Both det\\rectives looke\\r\\rd\\r\",\n      \"\\rat it but failed to realize\\nit was\\r\\nthe\\rkey\\r\\n\",\n      \"\\r\\nto solve the \\rcrime.\\r\"\n    ];\n\n    const blob = new Blob(parts, { type: \"text/html\", endings: \"native\" });\n    testing.expectEqual(161, blob.size);\n    testing.expectEqual(\"text/html\", blob.type);\n    testing.async(blob.bytes(), result => {\n      const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110,\n                                       101, 100, 32, 112, 97, 99, 107, 97, 103,\n                                       101, 10, 111, 102, 32, 112, 111, 116, 97,\n                                       116, 111, 10, 99, 104, 105, 10, 112, 115,\n                                       104, 101, 108, 100, 32, 116, 104, 101, 10,\n                                       97, 110, 115, 119, 101, 114, 32, 116, 111,\n                                       32, 116, 104, 101, 10, 32, 109, 121, 115,\n                                       116, 101, 114, 121, 46, 32, 66, 111, 116,\n                                       104, 32, 100, 101, 116, 10, 101, 99, 116,\n                                       105, 118, 101, 115, 32, 108, 111, 111, 107,\n                                       101, 10, 10, 100, 10, 10, 97, 116, 32, 105,\n                                       116, 32, 98, 117, 116, 32, 102, 97, 105, 108,\n                                       101, 100, 32, 116, 111, 32, 114, 101, 97,\n                                       108, 105, 122, 101, 10, 105, 116, 32, 119, 97,\n                                       115, 10, 116, 104, 101, 10, 107, 101, 121,\n                                       10, 10, 116, 111, 32, 115, 111, 108, 118, 101,\n                                       32, 116, 104, 101, 32, 10, 99, 114, 105, 109,\n                                       101, 46, 10]);\n      testing.expectEqual(true, result instanceof Uint8Array);\n      testing.expectEqual(expected, result);\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/file/file.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=file>\n  let f = new File();\n  testing.expectEqual(true, f instanceof File);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/abort_controller.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=abortController>\n  var a1 = new AbortController();\n\n  var s1 = a1.signal;\n  testing.expectEqual(undefined, s1.throwIfAborted());\n  testing.expectEqual(undefined, s1.reason);\n\n  let target;;\n  let called = 0;\n  s1.addEventListener('abort', (e) => {\n    called += 1;\n    target = e.target;\n  });\n\n  a1.abort();\n  testing.expectEqual(true, s1.aborted)\n  testing.expectEqual(s1, target)\n  testing.expectEqual('AbortError', s1.reason)\n  testing.expectEqual(1, called)\n</script>\n\n<script id=abort>\n  var s2 = AbortSignal.abort('over 9000');\n  testing.expectEqual(true, s2.aborted);\n  testing.expectEqual('over 9000', s2.reason);\n  testing.expectEqual('AbortError', AbortSignal.abort().reason);\n</script>\n\n<script id=timeout>\n  var s3 = AbortSignal.timeout(10);\n  testing.eventually(() => {\n    testing.expectEqual(true, s3.aborted);\n    testing.expectEqual('TimeoutError', s3.reason);\n    testing.expectError('Error: TimeoutError', () => {\n      s3.throwIfAborted()\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/canvas.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=canvas>\n{\n  const element = document.createElement(\"canvas\");\n  const ctx = element.getContext(\"2d\");\n  testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);\n  // We can't really test this but let's try to call it at least.\n  ctx.fillRect(0, 0, 0, 0);\n}\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/legacy/html/dataset.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=x data-power=\"over 9000\" data-empty data-some-long-key=ok></div>\n\n<script id=dataset>\n  let el1 = document.createElement('div');\n  testing.expectEqual(undefined, el1.dataset.x);\n\n  el1.dataset.x = '123';\n  testing.expectEqual(true, delete el1.dataset.x);\n  testing.expectEqual(undefined, el1.dataset.x);\n  // yes, this is right\n  testing.expectEqual(true, delete el1.dataset.other);\n\n  let ds1 = el1.dataset;\n  ds1.helloWorld = 'yes';\n  testing.expectEqual('yes', el1.getAttribute('data-hello-world'));\n  el1.setAttribute('data-this-will-work', 'positive');\n  testing.expectEqual('positive', ds1.thisWillWork);\n</script>\n\n<script id=element>\n  let div = $('#x');\n  testing.expectEqual(undefined, div.dataset.nope);\n  testing.expectEqual('over 9000', div.dataset.power);\n  testing.expectEqual('', div.dataset.empty);\n  testing.expectEqual('ok', div.dataset.someLongKey);\n  testing.expectEqual(true, delete div.dataset.power);\n  testing.expectEqual(undefined, div.dataset.power);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/document.html",
    "content": "<!DOCTYPE html>\n<html>\n  <body>\n    <div id=content><a id=link href=#></a></div>\n  </body>\n</html>\n\n<script src=\"../testing.js\"></script>\n<applet></applet>\n\n<script id=document>\n  testing.expectEqual('HTMLDocument', document.__proto__.constructor.name);\n  testing.expectEqual('Document', document.__proto__.__proto__.constructor.name);\n  testing.expectEqual('body', document.body.localName);\n\n  testing.expectEqual('localhost', document.domain);\n  testing.expectEqual('', document.referrer);\n  testing.expectEqual('', document.title);\n  testing.expectEqual('body', document.body.localName);\n  testing.expectEqual('head', document.head.localName);\n  testing.expectEqual(0, document.images.length);\n  testing.expectEqual(0, document.embeds.length);\n  testing.expectEqual(0, document.plugins.length);\n  testing.expectEqual(2, document.scripts.length);\n  testing.expectEqual(0, document.forms.length);\n  testing.expectEqual(1, document.links.length);\n  testing.expectEqual(0, document.applets.length); // deprecated, always returns 0\n  testing.expectEqual(0, document.anchors.length);\n  testing.expectEqual(7, document.all.length);\n  testing.expectEqual('document', document.currentScript.id);\n\n  document.title = 'foo';\n  testing.expectEqual('foo', document.title);\n  document.title = '';\n\n  document.getElementById('link').setAttribute('name', 'foo');\n  let list = document.getElementsByName('foo');\n  testing.expectEqual(1, list.length);\n\n  testing.expectEqual('', document.cookie);\n  document.cookie = 'name=Oeschger;';\n  document.cookie = 'favorite_food=tripe;';\n\n  testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);\n  // \"\" should be returned, but the framework overrules it atm\n  document.cookie = 'IgnoreMy=Ghost; HttpOnly';\n  testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);\n\n  // Return null since we only return elements when they have previously been localized\n  testing.expectEqual(null, document.elementFromPoint(2.5, 2.5));\n  testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));\n\n  let div1 = document.createElement('div');\n  document.body.appendChild(div1);\n  div1.getClientRects(); // clal this to position it\n\n  let a = document.createElement('a');\n  a.href = \"https://lightpanda.io\";\n  document.body.appendChild(a);\n  // Note this will be placed after the div of previous test\n  a.getClientRects();\n\n  testing.expectEqual(true, !document.all);\n  testing.expectEqual(false, !!document.all);\n  testing.expectEqual('[object HTMLScriptElement]', document.all(6).toString());\n  testing.expectEqual('[object HTMLDivElement]', document.all('content').toString());\n\n  testing.expectEqual(document, document.defaultView.document );\n  testing.expectEqual('loading', document.readyState);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/element.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=content>a<strong>b</strong>cc</div>\n<script id=inner>\n  const content = $('#content');\n  testing.expectEqual('a<strong>b</strong>cc', content.innerHTML);\n  testing.expectEqual('abcc', content.innerText);\n  content. innerText = 'foo';\n\n  testing.expectEqual('foo', content.innerHTML);\n  testing.expectEqual('foo', content.innerText);\n</script>\n\n<script id=addEventListene>\n  let click_count = 0;\n  content.addEventListener('click', function() { click_count++ });\n  content.click()\n  testing.expectEqual(1, click_count);\n</script>\n\n<script id=style>\n  let style = content.style;\n  style.cssText = 'color: red; font-size: 12px; margin: 5px !important';\n  testing.expectEqual(3, style.length);\n  style.setProperty('background-color', 'blue')\n  testing.expectEqual('blue', style.getPropertyValue('background-color'));\n  testing.expectEqual(4, style.length);\n</script>\n\n<script id=a>\n  let a = document.createElement('a');\n  testing.expectEqual('', a.href);\n  testing.expectEqual('', a.host);\n  a.href = 'about';\n  testing.expectEqual('http://localhost:9589/html/about', a.href);\n</script>\n\n<script id=focus>\n  // detached node cannot be focused\n  const focused = document.activeElement;\n  document.createElement('a').focus();\n  testing.expectEqual(focused, document.activeElement);\n</script>\n\n<script id=link>\n  let l2 = document.createElement('link');\n  testing.expectEqual('', l2.href);\n  l2.href = 'https://lightpanda.io/opensource-browser/15';\n  testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);\n\n  l2.href = '/over/9000';\n  testing.expectEqual('http://localhost:9589/over/9000', l2.href);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/error_event.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=ErrorEvent>\n  let e1 = new ErrorEvent('err1')\n  testing.expectEqual('', e1.message);\n  testing.expectEqual('', e1.filename);\n  testing.expectEqual(0, e1.lineno);\n  testing.expectEqual(0, e1.colno);\n  testing.expectEqual(undefined, e1.error);\n\n  let e2 = new ErrorEvent('err1', {\n     message: 'm1',\n     filename: 'fx19',\n     lineno: 443,\n     colno: 8999,\n     error: 'under 9000!',\n   });\n\n  testing.expectEqual('m1', e2.message);\n  testing.expectEqual('fx19', e2.filename);\n  testing.expectEqual(443, e2.lineno);\n  testing.expectEqual(8999, e2.colno);\n  testing.expectEqual('under 9000!', e2.error);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/history/history.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=history>\n    testing.expectEqual('auto', history.scrollRestoration);\n\n    history.scrollRestoration = 'manual';\n    testing.expectEqual('manual', history.scrollRestoration);\n\n    history.scrollRestoration = 'auto';\n    testing.expectEqual('auto', history.scrollRestoration);\n    testing.expectEqual(null, history.state)\n\n    history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9589/html/history/history_after_nav.skip.html');\n    testing.expectEqual({ testInProgress: true }, history.state);\n\n    history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9589/xhr/json');\n    history.replaceState({ \"new\": \"field\", testComplete: true }, null);\n\n    let state = { \"new\": \"field\", testComplete: true };\n    testing.expectEqual(state, history.state);\n\n    let popstateEventFired = false;\n    let popstateEventState = null;\n\n    window.addEventListener('popstate', (event) => {\n        popstateEventFired = true;\n        popstateEventState = event.state;\n    });\n\n    testing.eventually(() => {\n      testing.expectEqual(true, popstateEventFired);\n      testing.expectEqual(state, popstateEventState);\n    })\n\n    history.back();\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/history/history2.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=history2>\n    let state = { \"new\": \"field\", testComplete: true, testInProgress: true };\n    history.replaceState(state, \"\");\n    history.pushState(null, null, 'http://127.0.0.1:9589/html/history/history_after_nav.skip.html');\n\n    let popstateEventFired = false;\n    let popstateEventState = null;\n\n    window.onpopstate = (event) => {\n        popstateEventFired = true;\n        popstateEventState = event.state;\n    };\n\n    testing.eventually(() => {\n      testing.expectEqual(true, popstateEventFired);\n      testing.expectEqual(state, popstateEventState);\n    })\n\n    history.back();\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/history/history_after_nav.skip.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=history-after-nav>\n    testing.expectEqual(true, history.state && history.state.testInProgress);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/image.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=image>\n  img = new Image();\n  testing.expectEqual(0, img.width);\n  testing.expectEqual(0, img.height);\n\n  img = new Image(4);\n  testing.expectEqual(4, img.width);\n  testing.expectEqual(0, img.height);\n\n  img = new Image(5, 6);\n  testing.expectEqual(5, img.width);\n  testing.expectEqual(6, img.height);\n\n  let fruit = new Image\n  testing.expectEqual(0, fruit.width);\n  fruit.width = 5;\n  testing.expectEqual(5, fruit.width);\n  fruit.width = '15';\n  testing.expectEqual(15, fruit.width);\n  fruit.width = 'apple';\n  testing.expectEqual(0, fruit.width);\n\n  let lyric = new Image\n  testing.expectEqual('', lyric.src);\n  lyric.src = 'okay';\n  testing.expectEqual('http://localhost:9589/html/okay', lyric.src);\n  lyric.src = 15;\n  testing.expectEqual('http://localhost:9589/html/15', lyric.src);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/input.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<form action=\"test.php\" target=\"_blank\" id=form>\n  <p>\n    <label>First name: <input type=\"text\" name=\"first-name\" id=input /></label>\n  </p>\n</form>\n\n<script id=input_properties>\n  let input = document.createElement('input');\n\n  testing.expectEqual(null, input.form);\n  input.form = 'foo';\n  testing.expectEqual(null, input.form);\n\n  testing.expectEqual('', input.name);\n  input.name = 'leto';\n  testing.expectEqual('leto', input.name);\n\n  testing.expectEqual('', input.accept);\n  input.accept = 'anything';\n  testing.expectEqual('anything', input.accept);\n\n  testing.expectEqual('', input.alt);\n  input.alt = 'x1';\n  testing.expectEqual('x1', input.alt);\n\n  testing.expectEqual(false, input.disabled);\n  input.disabled = true;\n  testing.expectEqual(true, input.disabled);\n  input.disabled = false;\n  testing.expectEqual(false, input.disabled);\n\n  testing.expectEqual(false, input.readOnly);\n  input.readOnly = true;\n  testing.expectEqual(true, input.readOnly);\n  input.readOnly = false;\n  testing.expectEqual(false, input.readOnly);\n\n  testing.expectEqual(-1, input.maxLength);\n  input.maxLength = 5;\n  testing.expectEqual(5, input.maxLength);\n  input.maxLength = 'banana';\n  testing.expectEqual(0, input.maxLength);\n  testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => { input.maxLength = -45;});\n\n  testing.expectEqual(20, input.size);\n  input.size = 5;\n  testing.expectEqual(5, input.size);\n  input.size = -449;\n  testing.expectEqual(20, input.size);\n  testing.expectError('Error: ZeroNotAllowed', () => { input.size = 0; });\n\n  testing.expectEqual('', input.src);\n  input.src = 'foo'\n  testing.expectEqual('http://localhost:9589/html/foo', input.src);\n  input.src = '-3'\n  testing.expectEqual('http://localhost:9589/html/-3', input.src);\n  input.src = ''\n  testing.expectEqual('http://localhost:9589/html/input.html', input.src);\n\n  testing.expectEqual('text', input.type);\n  input.type = 'checkbox';\n  testing.expectEqual('checkbox', input.type);\n  input.type = '5';\n  testing.expectEqual('text', input.type);\n</script>\n\n<script id=related>\n  let input_checked = document.createElement('input')\n  testing.expectEqual(false, input_checked.defaultChecked);\n  testing.expectEqual(false, input_checked.checked);\n\n  input_checked.defaultChecked = true;\n  testing.expectEqual(true, input_checked.defaultChecked);\n  testing.expectEqual(true, input_checked.checked);\n\n  input_checked.checked = false;\n  testing.expectEqual(true, input_checked.defaultChecked);\n  testing.expectEqual(false, input_checked.checked);\n\n  input_checked.defaultChecked = true;\n  testing.expectEqual(false, input_checked.checked);\n</script>\n\n<script id=defaultValue>\n  testing.expectEqual('', input.defaultValue);\n  testing.expectEqual('', input.value);\n\n  input.defaultValue = 3.1;\n  testing.expectEqual('3.1', input.defaultValue);\n  testing.expectEqual('3.1', input.value)\n\n  input.value = 'mango';\n  testing.expectEqual('3.1', input.defaultValue);\n  testing.expectEqual('mango', input.value);\n\n  input.defaultValue = true;\n  testing.expectEqual('mango', input.value);\n</script>\n\n<script id=form>\n  const form = $('#form');\n  input = $('#input');\n  testing.expectEqual(form, input.form);\n\n  // invalid\n  input.form = 'foo';\n  testing.expectEqual(form, input.form);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/link.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<a id=link href=foo>OK</a>\n\n<script id=link>\n  let link = $('#link');\n  testing.expectEqual('', link.target);\n  link.target = '_blank';\n  testing.expectEqual('_blank', link.target);\n  link.target = '';\n\n  testing.expectEqual('http://localhost:9589/html/foo', link.href);\n  link.href = 'https://lightpanda.io/';\n  testing.expectEqual('https://lightpanda.io/', link.href);\n\n  testing.expectEqual('https://lightpanda.io', link.origin);\n\n  link.host = 'lightpanda.io:443';\n  testing.expectEqual('lightpanda.io', link.host);\n  testing.expectEqual('', link.port);\n  testing.expectEqual('lightpanda.io', link.hostname);\n\n  link.host = 'lightpanda.io';\n  testing.expectEqual('lightpanda.io', link.host);\n  testing.expectEqual('', link.port);\n  testing.expectEqual('lightpanda.io', link.hostname);\n\n  testing.expectEqual('lightpanda.io', link.host);\n  testing.expectEqual('lightpanda.io', link.hostname);\n  link.hostname = 'foo.bar';\n  testing.expectEqual('https://foo.bar/', link.href);\n\n  testing.expectEqual('', link.search);\n  link.search = 'q=bar';\n  testing.expectEqual('?q=bar', link.search);\n  testing.expectEqual('https://foo.bar/?q=bar', link.href);\n\n  testing.expectEqual('', link.hash);\n  link.hash = 'frag';\n  testing.expectEqual('#frag', link.hash);\n  testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);\n\n  testing.expectEqual('', link.port);\n  link.port = '443';\n  testing.expectEqual('foo.bar', link.host);\n  testing.expectEqual('foo.bar', link.hostname);\n  testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);\n  link.port = null;\n  testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);\n\n  testing.expectEqual('foo', link.href = 'foo');\n\n  testing.expectEqual('', link.type);\n  link.type = 'text/html';\n  testing.expectEqual('text/html', link.type);\n\n  testing.expectEqual('OK', link.text);\n  link.text = 'foo';\n  testing.expectEqual('foo', link.text);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/location.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=location>\n  testing.expectEqual('http://localhost:9589/html/location.html', location.href);\n  testing.expectEqual('http://localhost:9589/html/location.html', document.location.href);\n\n  testing.expectEqual(\"localhost:9589\", location.host);\n  testing.expectEqual(\"localhost\", location.hostname);\n  testing.expectEqual(\"http://localhost:9589\", location.origin);\n  testing.expectEqual(\"/html/location.html\", location.pathname);\n  testing.expectEqual(\"\", location.hash);\n  testing.expectEqual(\"9589\", location.port);\n  testing.expectEqual(\"\", location.search);\n</script>\n\n<script id=location_hash>\n  location.hash = \"\";\n  testing.expectEqual(\"\", location.hash);\n  testing.expectEqual('http://localhost:9589/html/location.html', location.href);\n\n  location.hash = \"#abcdef\";\n  testing.expectEqual(\"#abcdef\", location.hash);\n  testing.expectEqual('http://localhost:9589/html/location.html#abcdef', location.href);\n\n  location.hash = \"xyzxyz\";\n  testing.expectEqual(\"#xyzxyz\", location.hash);\n  testing.expectEqual('http://localhost:9589/html/location.html#xyzxyz', location.href);\n\n  location.hash = \"\";\n  testing.expectEqual(\"\", location.hash);\n  testing.expectEqual('http://localhost:9589/html/location.html', location.href);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/navigation/navigation.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=navigation>\n    testing.expectEqual('object', typeof navigation);\n    testing.expectEqual('object', typeof navigation.currentEntry);\n\n    testing.expectEqual('string', typeof navigation.currentEntry.id);\n    testing.expectEqual('string', typeof navigation.currentEntry.key);\n    testing.expectEqual('string', typeof navigation.currentEntry.url);\n\n    const currentIndex = navigation.currentEntry.index;\n\n    navigation.navigate(\n        'http://localhost:9589/html/navigation/navigation_after_nav.skip.html',\n        { state: { currentIndex: currentIndex, navTestInProgress: true } }\n    );\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/navigation/navigation_after_nav.skip.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=navigation-after-nav>\n    const state = navigation.currentEntry.getState();\n    testing.expectEqual(true, state.navTestInProgress);\n    testing.expectEqual(state.currentIndex + 1, navigation.currentEntry.index);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/navigation/navigation_currententrychange.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=navigation_currententrychange>\n    let currentEntryChanged = false;\n\n    navigation.addEventListener(\"currententrychange\", () => {\n      currentEntryChanged = true;\n    });\n\n    // Doesn't fully navigate if same document.\n    location.href = location.href + \"#1\";\n\n    testing.expectEqual(true, currentEntryChanged);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/navigator.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=navigator>\n  testing.expectEqual('Lightpanda/1.0',  navigator.userAgent);\n  testing.expectEqual('1.0',  navigator.appVersion);\n  testing.expectEqual('en-US',  navigator.language);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/screen.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=screen>\n  let screen = window.screen;\n  testing.expectEqual(1920, screen.width);\n  testing.expectEqual(1080, screen.height);\n\n  let orientation = screen.orientation;\n  testing.expectEqual(0, orientation.angle);\n  testing.expectEqual('landscape-primary', orientation.type);\n\n  // this shouldn't crash (it used to)\n  screen.addEventListener('change', () => {});\n</script>\n\n<script id=orientation>\n  screen.orientation.addEventListener('change', () => {})\n  // above shouldn't crash (it used to)\n  testing.expectEqual(true, true);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/dynamic_import.html",
    "content": "<!DOCTYPE html>\n\n<script src=\"../../testing.js\"></script>\n\n<script id=dynamic_import type=module>\n  const promise1 = new Promise((resolve) => {\n    Promise.all([\n      import('./import.js'),\n      import('./import.js'),\n      import('./import.js'),\n      import('./import.js'),\n      import('./import.js'),\n      import('./import.js'),\n      import('./import2.js'),\n      import('./import.js'),\n      import('./import.js'),\n    ]).then(resolve);\n  });\n\n  testing.async(promise1, (res) => {\n    testing.expectEqual(9, res.length);\n    testing.expectEqual('hello', res[0].greeting);\n    testing.expectEqual('hello', res[1].greeting);\n    testing.expectEqual('hello', res[2].greeting);\n    testing.expectEqual('hello', res[3].greeting);\n    testing.expectEqual('hello', res[4].greeting);\n    testing.expectEqual('hello', res[5].greeting);\n    testing.expectEqual('world', res[6].greeting);\n    testing.expectEqual('hello', res[7].greeting);\n    testing.expectEqual('hello', res[8].greeting);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/import.html",
    "content": "<!DOCTYPE html>\n\n<script src=\"../../testing.js\"></script>\n\n<script id=import type=module>\n  import * as im from './import.js';\n  testing.expectEqual('hello', im.greeting);\n</script>\n\n<script id=cached type=module>\n  // hopefully cached, who knows, no real way to assert this\n  // but at least it works.\n  import * as im from './import.js';\n  testing.expectEqual('hello', im.greeting);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/import.js",
    "content": "let greeting = 'hello';\nexport {greeting as 'greeting'};\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/import2.js",
    "content": "let greeting = 'world';\nexport {greeting as 'greeting'};\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/importmap.html",
    "content": "<!DOCTYPE html>\n\n<script src=\"../../testing.js\"></script>\n\n<script type=importmap>\n    {\n      \"imports\": {\n        \"core\": \"./import.js\"\n      }\n    }\n</script>\n\n<script id=use_importmap type=module>\n  import * as im from 'core';\n  testing.expectEqual('hello', im.greeting);\n</script>\n\n<script id=cached_importmap type=module>\n  // hopefully cached, who knows, no real way to assert this\n  // but at least it works.\n  import * as im from 'core';\n  testing.expectEqual('hello', im.greeting);\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/inline_defer.html",
    "content": "<!DOCTYPE html>\n<head>\n  <script>\n    let dyn1_loaded = 0;\n    function loadScript(src) {\n      const script = document.createElement('script');\n      script.src = src;\n      document.getElementsByTagName(\"head\")[0].appendChild(script)\n    }\n  </script>\n</head>\n\n<script src=\"../../testing.js\"></script>\n\n<script defer>\n  loadScript('inline_defer.js');\n</script>\n\n<script async>\n  loadScript('inline_defer.js');\n</script>\n\n<script id=inline_defer>\n  // inline script should ignore defer and async attributes. If we don't do\n  // this correctly, we'd end up in an infinite loop\n  // https://github.com/lightpanda-io/browser/issues/1014\n  testing.eventually(() => testing.expectEqual(2, dyn1_loaded));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/inline_defer.js",
    "content": "dyn1_loaded += 1;\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/order.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script defer id=\"remote_defer\" src=\"order_defer.js\"></script>\n<script defer id=\"remote_async\" src=\"order_async.js\"></script>\n\n<script type=module id=\"inline_module\">\n    // inline module is always deferred.\n    list += 'g';\n    testing.expectEqual('abcdefg', list);\n</script>\n\n<script>\n    var list = '';\n</script>\n\n<script id=\"remote\" src=\"order.js\"></script>\n\n<script async id=\"inline_async\">\n    // inline script ignore async\n    list += 'b';\n    testing.expectEqual('ab', list);\n</script>\n\n<script defer id=\"inline_defer\">\n    // inline script ignore defer\n    list += 'c';\n    testing.expectEqual('abc', list);\n</script>\n\n<script id=\"default\">\n    // simple inline script\n    list += 'd';\n    testing.expectEqual('abcd', list);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/order.js",
    "content": "list += 'a';\ntesting.expectEqual('a', list);\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/order_async.js",
    "content": "list += 'f';\ntesting.expectEqual('abcdef', list);\n\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/order_defer.js",
    "content": "list += 'e';\ntesting.expectEqual('abcde', list);\n"
  },
  {
    "path": "src/browser/tests/legacy/html/script/script.html",
    "content": "<!DOCTYPE html>\n<script src=\"../../testing.js\"></script>\n\n<script id=script>\n  let script = document.createElement('script')\n  script.src = 'foo.bar';\n\n  script.async = true;\n  testing.expectEqual(true, script.async);\n  script.async = false;\n  testing.expectEqual(false, script.async);\n\n  script.defer = true;\n  testing.expectEqual(true, script.defer);\n\n  testing.expectEqual('', script.nonce);\n  script.nonce = 'hello';\n  testing.expectEqual('hello', script.nonce);\n</script>\n\n<script id=datauri src=\"data:text/plain;charset=utf-8;base64,dGVzdGluZy5leHBlY3RFcXVhbCh0cnVlLCB0cnVlKTs=\"></script>\n\n<script id=datauri_url_encoded_text src=\"data:text/javascript,testing.expectEqual(3, 3);\"></script>\n\n<script id=datauri_encoded_padding src=\"data:text/javascript;base64,dGVzdGluZy5leHBlY3RFcXVhbCgxLCAxKTs%3D\"></script>\n\n<script id=datauri_fully_encoded src=\"data:text/javascript;base64,%64%47%56%7a%64%47%6c%75%5a%79%35%6c%65%48%42%6c%59%33%52%46%63%58%56%68%62%43%67%79%4c%43%41%79%4b%54%73%3d\"></script>\n\n<script id=datauri_with_whitespace src=\"data:text/javascript;base64,%20ZD%20Qg%0D%0APS%20An%20Zm91cic%0D%0A%207%20\"></script>\n\n<script id=datauri_url_encoded_unicode src=\"data:text/javascript,testing.expectEqual(4%2C%204)%3B\"></script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/select.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<form id=f1>\n <select id=s1 name=s1><option>o1<option>o2</select>\n</form>\n<select id=s2></select>\n\n<script id=select>\n  const s = document.getElementById('s1');\n  testing.expectEqual('[object HTMLFormElement]', s.form.toString());\n  testing.expectEqual(null, document.getElementById('s2').form);\n\n  testing.expectEqual(false, s.disabled);\n  s.disabled = true;\n  testing.expectEqual(true, s.disabled);\n\n  s.disabled = false;\n  testing.expectEqual(false, s.disabled);\n\n  testing.expectEqual(false, s.multiple);\n  s.multiple = true;\n  testing.expectEqual(true, s.multiple);\n  s.multiple = false;\n  testing.expectEqual(false, s.multiple);\n\n  testing.expectEqual('s1', s.name);\n  s.name = 'sel1';\n  testing.expectEqual('sel1', s.name);\n\n  testing.expectEqual(2, s.length);\n\n  testing.expectEqual(0, s.selectedIndex);\n  s.selectedIndex = 2; // out of range\n  testing.expectEqual(-1, s.selectedIndex);\n\n  s.selectedIndex = -1;\n  testing.expectEqual(-1, s.selectedIndex);\n\n  s.selectedIndex = 0;\n  testing.expectEqual(0, s.selectedIndex);\n\n  s.selectedIndex = 1;\n  testing.expectEqual(1, s.selectedIndex);\n\n  s.selectedIndex = -323;\n  testing.expectEqual(-1, s.selectedIndex);\n\n  let options = s.options;\n  testing.expectEqual(2, options.length);\n  testing.expectEqual('o2', options.item(1).value);\n  testing.expectEqual(-1, options.selectedIndex);\n\n  let o3 = document.createElement('option');\n  o3.value = 'o3';\n  options.add(o3)\n  testing.expectEqual(3, options.length);\n  testing.expectEqual('o3', options[2].value);\n  testing.expectEqual('o3', options.item(2).value);\n\n  let o4 = document.createElement('option');\n  o4.value = 'o4';\n  options.add(o4, 1);\n  testing.expectEqual(4, options.length);\n  testing.expectEqual('o4', options.item(1).value);\n\n  let o5 = document.createElement('option');\n  o5.value = 'o5';\n  options.add(o5, o3)\n  testing.expectEqual(5, options.length);\n  testing.expectEqual('o5', options.item(3).value);\n\n  options.remove(3)\n  testing.expectEqual(4, options.length);\n  testing.expectEqual('o3', options[3].value);\n  testing.expectEqual('o3', options.item(3).value);\n\n  testing.expectEqual(undefined, options[10]);\n  testing.expectEqual(null, options.item(10));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/slot.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script>\n  class LightPanda extends HTMLElement {\n    constructor() {\n      super();\n    }\n\n    connectedCallback() {\n      const shadow = this.attachShadow({ mode: \"open\" });\n\n      const slot1 = document.createElement('slot');\n      slot1.name = 'slot-1';\n      shadow.appendChild(slot1);\n\n      switch (this.getAttribute('mode')) {\n        case '1':\n          slot1.innerHTML = 'hello';\n          break;\n        case '2':\n          const slot2 = document.createElement('slot');\n          shadow.appendChild(slot2);\n          break;\n      }\n    }\n  }\n  window.customElements.define(\"lp-test\", LightPanda);\n</script>\n\n<lp-test id=lp1 mode=1></lp-test>\n<lp-test id=lp2 mode=0></lp-test>\n<lp-test id=lp3 mode=0>default</lp-test>\n<lp-test id=lp4 mode=1><p slot=other>default</p></lp-test>\n<lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test>\n<lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test>\n\n<script>\n  function assertNodes(expected, actual) {\n    actual = actual.map((n) => n.id || n.textContent)\n    testing.expectEqual(expected, actual);\n  }\n</script>\n<script id=HTMLSlotElement>\n  for (let idx of [1, 2, 3, 4]) {\n    const lp = $(`#lp${idx}`);\n    const slot = lp.shadowRoot.querySelector('slot');\n\n    assertNodes([], slot.assignedNodes());\n    assertNodes([], slot.assignedNodes({}));\n    assertNodes([], slot.assignedNodes({flatten: false}));\n    if (lp.getAttribute('mode') === '1') {\n      assertNodes(['hello'], slot.assignedNodes({flatten: true}));\n    } else {\n      assertNodes([], slot.assignedNodes({flatten: true}));\n    }\n  }\n\n  {\n    const lp5 = $('#lp5');\n    const s5 = lp5.shadowRoot.querySelector('slot');\n    assertNodes(['default', 'other'], s5.assignedNodes());\n\n    const lp6 = $('#lp6');\n    const s6 = lp6.shadowRoot.querySelectorAll('slot');\n    assertNodes(['default2'], s6[0].assignedNodes({}));\n    assertNodes(['default2'], s6[0].assignedNodes({flatten: true}));\n    assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({}));\n    assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true}));\n\n    assertNodes(['default2'], s6[0].assignedElements({}));\n    assertNodes(['default2'], s6[0].assignedElements({flatten: true}));\n    assertNodes(['!!'], s6[1].assignedElements({}));\n    assertNodes(['!!'], s6[1].assignedElements({flatten: true}));\n  }\n</script>\n\n<lp-test id=sc1 mode=1></lp-test>\n<script id=slotChange1>\n  {\n    let calls = 0;\n    const lp = $('#sc1');\n    const slot = lp.shadowRoot.querySelector('slot');\n    slot.addEventListener('slotchange', (e) => {\n      assertNodes(['slotted'], slot.assignedNodes({}));\n      calls += 1\n    }, {});\n\n    const div = document.createElement('div');\n    div.textContent = 'Hello!';\n    div.id = 'slotted';\n    testing.expectEqual(null, div.assignedSlot);\n\n    div.setAttribute('slot', 'slot-1');\n    lp.appendChild(div);\n    testing.expectEqual(slot, div.assignedSlot);\n\n    testing.eventually(() => {\n      testing.expectEqual(1, calls)\n    });\n  }\n</script>\n\n<lp-test id=sc2 mode=1><div id=s2 slot=slot-1>hello</div></lp-test>\n<script id=slotChange2>\n  {\n    let calls = 0;\n    const lp = $('#sc2');\n    const slot = lp.shadowRoot.querySelector('slot');\n    slot.addEventListener('slotchange', (e) => {\n      assertNodes([], slot.assignedNodes({}));\n      calls += 1;\n    });\n\n    const div = $('#s2');\n    div.removeAttribute('slot');\n    testing.eventually(() => {\n      testing.expectEqual(1, calls)\n    });\n  }\n</script>\n\n<lp-test id=sc3 mode=1><div id=s3 slot=slot-1>hello</div></lp-test>\n<script id=slotChange3>\n  {\n    let calls = 0;\n    const lp = $('#sc3');\n    const slot = lp.shadowRoot.querySelector('slot');\n    slot.addEventListener('slotchange', (e) => {\n      assertNodes([], slot.assignedNodes({}));\n      calls += 1;\n    });\n\n    const div = $('#s3');\n    div.slot = 'other';\n    testing.eventually(() => {\n      testing.expectEqual(1, calls)\n    });\n  }\n</script>\n\n<lp-test id=sc4 mode=1></lp-test>\n<script id=slotChange4>\n  {\n    let calls = 0;\n    const lp = $('#sc4');\n    const slot = lp.shadowRoot.querySelector('slot');\n    slot.addEventListener('slotchange', (e) => {\n      assertNodes(['slotted'], slot.assignedNodes({}));\n      calls += 1;\n    });\n\n    const div = document.createElement('div');\n    div.id = 'slotted';\n    div.slot = 'other';\n    lp.appendChild(div);\n    div.slot = 'slot-1'\n    testing.eventually(() => {\n      testing.expectEqual(1, calls)\n    });\n  }\n</script>\n\n<lp-test id=sc5 mode=1><div id=s5 slot=slot-1>hello</div></lp-test>\n<script id=slotChange5>\n  {\n    let calls = 0;\n    const lp = $('#sc5');\n    const slot = lp.shadowRoot.querySelector('slot');\n    slot.addEventListener('slotchange', (e) => {\n      assertNodes([], slot.assignedNodes({}));\n      calls += 1;\n    });\n\n    $('#s5').remove();\n    testing.eventually(() => {\n      testing.expectEqual(1, calls)\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/style.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=style>\n  testing.expectEqual(null, document.createElement('style').sheet);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/html/svg.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<svg id=lower width=\"200\" height=\"100\" style=\"border:1px solid #ccc\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 200 100\">\n  <rect></rect>\n  <text x=\"100\" y=\"95\" font-size=\"14\" text-anchor=\"middle\">OVER 9000!!</text>\n</svg>\n\n<SVG ID=UPPER WIDTH=\"200\" HEIGHT=\"100\" STYLE=\"BORDER:1PX SOLID #CCC\" XMLNS=\"http://www.w3.org/2000/svg\" VIEWBOX=\"0 0 200 100\">\n  <RECT></RECT>\n  <TEXT X=\"100\" Y=\"95\" FONT-SIZE=\"14\" TEXT-ANCHOR=\"MIDDLE\">OVER 9000!!!</TEXT>\n</SVG>\n\n<script id=svg>\n  testing.expectEqual(false, 'AString' instanceof SVGElement);\n\n  const svg1 = $('#lower');\n  testing.expectEqual('http://www.w3.org/2000/svg', svg1.getAttribute('xmlns'));\n  testing.expectEqual('http://www.w3.org/2000/svg', svg1.getAttributeNode('xmlns').value);\n  testing.expectEqual('http://www.w3.org/2000/svg', svg1.attributes.getNamedItem('xmlns').value);\n  testing.expectEqual('0 0 200 100', svg1.getAttribute('viewBox'));\n  testing.expectEqual('viewBox', svg1.getAttributeNode('viewBox').name);\n  testing.expectEqual(true, svg1.outerHTML.includes('viewBox'));\n  testing.expectEqual('svg', svg1.tagName);\n  testing.expectEqual('rect', svg1.querySelector('rect').tagName);\n  testing.expectEqual('text', svg1.querySelector('text').tagName);\n\n  const svg2 = $('#UPPER');\n  testing.expectEqual('http://www.w3.org/2000/svg', svg2.getAttribute('xmlns'));\n  testing.expectEqual('http://www.w3.org/2000/svg', svg2.getAttributeNode('xmlns').value);\n  testing.expectEqual('http://www.w3.org/2000/svg', svg2.attributes.getNamedItem('xmlns').value);\n  testing.expectEqual('0 0 200 100', svg2.getAttribute('viewBox'));\n  testing.expectEqual('viewBox', svg2.getAttributeNode('viewBox').name);\n  testing.expectEqual(true, svg2.outerHTML.includes('viewBox'));\n  testing.expectEqual('svg', svg2.tagName);\n  testing.expectEqual('rect', svg2.querySelector('rect').tagName);\n  testing.expectEqual('text', svg2.querySelector('text').tagName);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/storage/local_storage.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=localstorage>\n  testing.expectEqual(0, localStorage.length);\n  testing.expectEqual(null, localStorage.getItem('foo'));\n  testing.expectEqual(null, localStorage.key(0));\n\n  localStorage.setItem('foo', 'bar');\n  testing.expectEqual(1, localStorage.length)\n  testing.expectEqual('bar', localStorage.getItem('foo'));\n  testing.expectEqual('foo', localStorage.key(0));\n  testing.expectEqual(null, localStorage.key(1));\n\n  localStorage.removeItem('foo');\n  testing.expectEqual(0, localStorage.length)\n  testing.expectEqual(null, localStorage.getItem('foo'));\n\n  localStorage['foo'] = 'bar';\n  testing.expectEqual(1, localStorage.length);\n  testing.expectEqual('bar', localStorage['foo']);\n\n  localStorage.setItem('a', '1');\n  localStorage.setItem('b', '2');\n  localStorage.setItem('c', '3');\n  testing.expectEqual(4, localStorage.length)\n  localStorage.clear();\n  testing.expectEqual(0, localStorage.length)\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/streams/readable_stream.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=readable_stream>\n  const stream = new ReadableStream({\n    start(controller) {\n      controller.enqueue(\"hello\");\n      controller.enqueue(\"world\");\n      controller.close();\n    }\n  });\n\n  const reader = stream.getReader();\n\n  testing.async(reader.read(), (data) => {\n    testing.expectEqual(\"hello\", data.value);\n    testing.expectEqual(false, data.done);\n  });\n</script>\n\n<script id=readable_stream_binary>\n  const input = new TextEncoder().encode('over 9000!');\n  const binStream = new ReadableStream({\n    start(controller) {\n      controller.enqueue(input);\n      controller.enqueue(\"world\");\n      controller.close();\n    }\n  });\n\n  testing.async(binStream.getReader().read(), (data) => {\n    testing.expectEqual(input, data.value);\n    testing.expectEqual(false, data.done);\n  });\n</script>\n\n<script id=readable_stream_close>\n  var closeResult;\n\n  const stream1 = new ReadableStream({\n    start(controller) {\n      controller.enqueue(\"first\");\n      controller.enqueue(\"second\");\n      controller.close();\n    }\n  });\n\n  const reader1 = stream1.getReader();\n\n  testing.async(reader1.read(), (data) => {\n    testing.expectEqual(\"first\", data.value);\n    testing.expectEqual(false, data.done);\n  });\n\n  testing.async(reader1.read(), (data) => {\n    testing.expectEqual(\"second\", data.value);\n    testing.expectEqual(false, data.done);\n  });\n\n  testing.async(reader1.read(), (data) => {\n    testing.expectEqual(undefined, data.value);\n    testing.expectEqual(true, data.done);\n  });\n</script>\n\n<script id=readable_stream_cancel>\n  var readResult;\n  var cancelResult;\n  var closeResult;\n\n  const stream2 = new ReadableStream({\n    start(controller) {\n      controller.enqueue(\"data1\");\n      controller.enqueue(\"data2\");\n      controller.enqueue(\"data3\");\n    },\n    cancel(reason) {\n      closeResult = `Stream cancelled: ${reason}`;\n    }\n  });\n\n  const reader2 = stream2.getReader();\n\n  testing.async(reader2.read(), (data) => {\n    testing.expectEqual(\"data1\", data.value);\n    testing.expectEqual(false, data.done);\n  });\n\n  testing.async(reader2.cancel(\"user requested\"), (result) => {\n    testing.expectEqual(undefined, result);\n    testing.expectEqual(\"Stream cancelled: user requested\", closeResult);\n  });\n</script>\n\n<script id=readable_stream_cancel_no_reason>\n  var closeResult2;\n\n  const stream3 = new ReadableStream({\n    start(controller) {\n      controller.enqueue(\"test\");\n    },\n    cancel(reason) {\n      closeResult2 = reason === undefined ? \"no reason\" : reason;\n    }\n  });\n\n  const reader3 = stream3.getReader();\n\n  testing.async(reader3.cancel(), (result) => {\n    testing.expectEqual(undefined, result);\n    testing.expectEqual(\"no reason\", closeResult2);\n  });\n</script>\n\n<script id=readable_stream_read_after_cancel>\n  var readAfterCancelResult;\n\n  const stream4 = new ReadableStream({\n    start(controller) {\n      controller.enqueue(\"before_cancel\");\n    },\n  });\n\n  const reader4 = stream4.getReader();\n\n  testing.async(reader4.cancel(\"test cancel\"), (cancelResult) => {\n    testing.expectEqual(undefined, cancelResult);\n  });\n\n  testing.async(reader4.read(), (data) => {\n    testing.expectEqual(undefined, data.value);\n    testing.expectEqual(true, data.done);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/testing.js",
    "content": "// Note: this code tries to make sure that we don't fail to execute a <script>\n// block without reporting an error. In other words, if the test passes, you\n// should be confident that the code actually ran.\n// We do a couple things to ensure this.\n// 1 - We make sure that ever script with an id has at least 1 assertion called\n// 2 - We add an onerror handler to every script and, on error, fail.\n//\n// This is pretty straightforward, with the only complexity coming from \"eventually\"\n// assertions, which are assertions we lazily check in `getStatus()`. We\n// do this because, by the time `getStatus()`, `page.wait()` will have been called\n// and any timer (setTimeout, requestAnimation, MutationObserver, etc...) will\n// have been evaluated. Test which use/test these behavior will use `eventually`.\n(() => {\n  function expectEqual(expected, actual) {\n    _recordExecution();\n    if (_equal(expected, actual)) {\n      return;\n    }\n    testing._status = 'fail';\n    let msg = `expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(actual)}`;\n\n    console.warn(\n      `id: ${testing._captured?.script_id || document.currentScript.id}`,\n      `msg: ${msg}`,\n      `stack: ${testing._captured?.stack || new Error().stack}`,\n    );\n  }\n\n  function expectError(expected, fn) {\n    withError((err) => {\n      expectEqual(expected, err.toString());\n    }, fn);\n  }\n\n  function withError(cb, fn) {\n    try{\n      fn();\n    } catch (err) {\n      cb(err);\n      return;\n    }\n    expectEqual('an error', null);\n  }\n\n  function skip() {\n    _recordExecution();\n  }\n\n  // Should only be called by the test runner\n  function assertOk() {\n    // if we're already in a fail state, return fail, nothing can recover this\n    if (testing._status === 'fail') {\n      throw new Error('Failed');\n    }\n\n    // run any eventually's that we've captured\n    for (const ev of testing._eventually) {\n      testing._captured = ev[1];\n      ev[0]();\n      testing._captured = null;\n    }\n\n    // Again, if we're in a fail state, we can immediately fail\n    if (testing._status === 'fail') {\n      throw new Error('Failed');\n    }\n\n    // make sure that any <script id=xyz></script> tags we have have had at least\n    // 1 assertion. This helps ensure that if a script tag fails to execute,\n    // we'll report an error, even if no assertions failed.\n    const scripts = document.getElementsByTagName('script');\n    for (script of scripts) {\n      const id = script.id;\n      if (!id) {\n        continue;\n      }\n\n      if (!testing._executed_scripts.has(id)) {\n        console.warn(`Failed to execute any expectations for <script id=\"${id}\">...</script>`);\n      \tthrow new Error('Failed');\n      }\n    }\n\n    if (testing._status != 'ok') {\n      throw new Error(testing._status);\n    }\n  }\n\n  // Set expectations to happen at some point in the future. Necessary for\n  // testing callbacks which will only be executed after page.wait is called.\n  function eventually(fn) {\n    // capture the current state (script id, stack) so that, when we do run this\n    // we can display more meaningful details on failure.\n    testing._eventually.push([fn, {\n      script_id: document.currentScript.id,\n      stack: new Error().stack,\n    }]);\n\n    _registerErrorCallback();\n  }\n\n  async function async(promise, cb) {\n    const script_id = document.currentScript ? document.currentScript.id : '<script id is unavailable in browsers>';\n    const stack = new Error().stack;\n    this._captured = {script_id: script_id, stack: stack};\n    const value = await promise;\n    // reset it, because await promise could change it.\n    this._captured = {script_id: script_id, stack: stack};\n    cb(value);\n    this._captured = null;\n  }\n\n  function _recordExecution() {\n    if (testing._status === 'fail') {\n      return;\n    }\n    testing._status = 'ok';\n\n    if (testing._captured || document.currentScript) {\n    \tconst script_id = testing._captured?.script_id || document.currentScript.id;\n    \ttesting._executed_scripts.add(script_id);\n\t    _registerErrorCallback();\n    }\n  }\n\n  // We want to attach an onError callback to each <script>, so that we can\n  // properly fail it.\n  function _registerErrorCallback() {\n    const script = document.currentScript;\n    if (!script) {\n      // can be null if we're executing an eventually assertion, but that's ok\n      // because the errorCallback would have been registered for this script\n      // already\n      return;\n    }\n\n    if (script.onerror) {\n      // already registered\n      return;\n    }\n\n    script.onerror = function(err, b) {\n      testing._status = 'fail';\n      console.warn(\n        `id: ${script.id}`,\n        `msg: There was an error executing the <script id=${script.id}>...</script>.\\n      There should be a eval error printed above this.`,\n      );\n    }\n  }\n\n  function _equal(a, b) {\n    if (a === b) {\n      return true;\n    }\n    if (a === null || b === null) {\n      return false;\n    }\n    if (typeof a !== 'object' || typeof b !== 'object') {\n      return false;\n    }\n\n    if (Object.keys(a).length != Object.keys(b).length) {\n      return false;\n    }\n\n    for (property in a) {\n      if (b.hasOwnProperty(property) === false) {\n        return false;\n      }\n      if (_equal(a[property], b[property]) === false) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  window.testing = {\n    _status: 'empty',\n    _eventually: [],\n    _executed_scripts: new Set(),\n    _captured: null,\n    skip: skip,\n    async: async,\n    assertOk: assertOk,\n    eventually: eventually,\n    expectEqual: expectEqual,\n    expectError: expectError,\n    withError: withError,\n  };\n\n  // Helper, so you can do $(sel) in a test\n  window.$ = function(sel) {\n    return document.querySelector(sel);\n  }\n\n  // Helper, so you can do $$(sel) in a test\n  window.$$ = function(sel) {\n    return document.querySelectorAll(sel);\n  }\n\n  if (!console.lp) {\n    // make this work in the browser\n    console.lp = console.log;\n  }\n})();\n"
  },
  {
    "path": "src/browser/tests/legacy/url/url.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=url>\n  var url = new URL('https://foo.bar/path?query#fragment');\n  testing.expectEqual(\"https://foo.bar\", url.origin);\n  testing.expectEqual(\"https://foo.bar/path?query#fragment\", url.href);\n  testing.expectEqual(\"https:\", url.protocol);\n  testing.expectEqual(\"\", url.username);\n  testing.expectEqual(\"\", url.password);\n  testing.expectEqual(\"foo.bar\", url.host);\n  testing.expectEqual(\"foo.bar\", url.hostname);\n  testing.expectEqual(\"\", url.port);\n  testing.expectEqual(\"/path\", url.pathname);\n  testing.expectEqual(\"?query\", url.search);\n  testing.expectEqual(\"#fragment\", url.hash);\n  testing.expectEqual(\"\", url.searchParams.get('query'));\n\n  url.search = 'hello=world';\n  testing.expectEqual(1, url.searchParams.size);\n  testing.expectEqual(\"world\", url.searchParams.get('hello'));\n\n  url.search = '?over=9000';\n  testing.expectEqual(1, url.searchParams.size);\n  testing.expectEqual(\"9000\", url.searchParams.get('over'));\n\n  url.search = '';\n  testing.expectEqual(0, url.searchParams.size);\n\n  const url2 = new URL(url);\n  testing.expectEqual(\"https://foo.bar/path#fragment\", url2.href);\n</script>\n\n<script id=\"constructor\">\n  testing.expectError(\"Error: TypeError\", () => {\n    new URL(document.createElement('a'));\n  });\n\n  let a = document.createElement('a');\n  a.href = 'https://www.lightpanda.io/over?9000=!!';\n  const url3 = new URL(a);\n  testing.expectEqual(\"https://www.lightpanda.io/over?9000=!!\", url3.href);\n </script>\n\n<script id=searchParams>\n  url = new URL('https://foo.bar/path?a=~&b=%7E#fragment');\n  testing.expectEqual(\"~\", url.searchParams.get('a'));\n  testing.expectEqual(\"~\", url.searchParams.get('b'));\n\n  url.searchParams.append('c', 'foo');\n  testing.expectEqual(\"foo\", url.searchParams.get('c'));\n  testing.expectEqual(1, url.searchParams.getAll('c').length);\n  testing.expectEqual(\"foo\", url.searchParams.getAll('c')[0]);\n  testing.expectEqual(3, url.searchParams.size);\n\n  // search is dynamic\n  testing.expectEqual(\"?a=%7E&b=%7E&c=foo\", url.search);\n\n  // href is dynamic\n  testing.expectEqual(\"https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment\", url.href);\n\n  url.searchParams.delete('c', 'foo');\n  testing.expectEqual(null, url.searchParams.get('c'));\n  url.searchParams.delete('a');\n  testing.expectEqual(null, url.searchParams.get('a'));\n</script>\n\n<script id=searchParamsSetHref>\n  url = new URL(\"https://foo.bar\");\n  const searchParams = url.searchParams;\n\n  // SearchParams should be empty.\n  testing.expectEqual(0, searchParams.size);\n\n  url.href = \"https://lightpanda.io?over=9000&light=panda\";\n  // It won't hurt to check href and host too.\n  testing.expectEqual(\"https://lightpanda.io/?over=9000&light=panda\", url.href);\n  testing.expectEqual(\"lightpanda.io\", url.host);\n  // SearchParams should be updated too when URL is set.\n  testing.expectEqual(2, searchParams.size);\n  testing.expectEqual(\"9000\", searchParams.get(\"over\"));\n  testing.expectEqual(\"panda\", searchParams.get(\"light\"));\n</script>\n\n<script id=base>\n  url = new URL('over?9000', 'https://lightpanda.io');\n  testing.expectEqual(\"https://lightpanda.io/over?9000\", url.href);\n</script>\n\n<script id=\"svelkit\">\n  let sk = new URL('sveltekit-internal://');\n  testing.expectEqual(\"sveltekit-internal:\", sk.protocol);\n  testing.expectEqual(\"\", sk.host);\n  testing.expectEqual(\"\", sk.hostname);\n  testing.expectEqual(\"sveltekit-internal://\", sk.href);\n</script>\n\n<script id=invalidUrl>\n  testing.expectError(\"Error: TypeError\", () => {\n    _ = new URL(\"://foo.bar/path?query#fragment\");\n  });\n</script>\n\n<script id=URL.canParse>\n  testing.expectEqual(true, URL.canParse(\"https://lightpanda.io\"));\n  testing.expectEqual(false, URL.canParse(\"://lightpanda.io\"));\n\n  testing.expectEqual(true, URL.canParse(\"/home\", \"https://lightpanda.io\"));\n  testing.expectEqual(false, URL.canParse(\"lightpanda.io\", \"https\"));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/url/url_search_params.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=url_search_params>\n  let usp = new URLSearchParams();\n  usp.get('a')\n  testing.expectEqual(false, usp.has('a'));\n  testing.expectEqual([], usp.getAll('a'));\n  usp.delete('a');\n\n  usp.set('a', 1);\n  testing.expectEqual(true, usp.has('a'));\n  testing.expectEqual(\"1\", usp.get('a'));\n  testing.expectEqual([\"1\"], usp.getAll('a'));\n\n  usp.append('a', 2);\n  testing.expectEqual(true, usp.has('a'));\n  testing.expectEqual(\"1\", usp.get('a'));\n  testing.expectEqual(['1','2'], usp.getAll('a'));\n\n  usp.append('b', '3');\n  testing.expectEqual(true, usp.has('a'));\n  testing.expectEqual(\"1\", usp.get('a'));\n  testing.expectEqual(['1','2'], usp.getAll('a'));\n  testing.expectEqual(true, usp.has('b'));\n  testing.expectEqual(\"3\", usp.get('b'));\n  testing.expectEqual(['3'], usp.getAll('b'));\n\n  let acc = [];\n  for (const key of usp.keys()) { acc.push(key) };\n  testing.expectEqual(['a', 'a', 'b'], acc);\n\n  acc = [];\n  for (const value of usp.values()) { acc.push(value) };\n  testing.expectEqual(['1', '2', '3'], acc);\n\n  acc = [];\n  for (const entry of usp.entries()) { acc.push(entry) };\n  testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);\n\n  acc = [];\n  for (const entry of usp) { acc.push(entry) };\n  testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);\n\n  usp.delete('a');\n  testing.expectEqual(false, usp.has('a'));\n  testing.expectEqual(true, usp.has('b'));\n\n  acc = [];\n  for (const key of usp.keys()) { acc.push(key) };\n  testing.expectEqual(['b'], acc);\n\n  acc = [];\n  for (const value of usp.values()) { acc.push(value) };\n  testing.expectEqual(['3'], acc);\n\n  acc = [];\n  for (const entry of usp.entries()) { acc.push(entry) };\n  testing.expectEqual([['b', '3']], acc);\n\n  acc = [];\n  for (const entry of usp) { acc.push(entry) };\n  testing.expectEqual([['b', '3']], acc);\n</script>\n\n <script id=parsed>\n  usp = new URLSearchParams('?hello');\n  testing.expectEqual('', usp.get('hello'));\n\n  usp = new URLSearchParams('?abc=');\n  testing.expectEqual('', usp.get('abc'));\n\n  usp = new URLSearchParams('?abc=123&');\n  testing.expectEqual('123', usp.get('abc'));\n  testing.expectEqual(1, usp.size);\n\n  var fd = new FormData();\n  fd.append('a', '1');\n  fd.append('a', '2');\n  fd.append('b', '3');\n  ups = new URLSearchParams(fd);\n\n  testing.expectEqual(3, ups.size);\n  testing.expectEqual(['1', '2'], ups.getAll('a'));\n  testing.expectEqual(['3'], ups.getAll('b'));\n\n  fd.delete('a'); // the two aren't linked, it created a copy\n  testing.expectEqual(3, ups.size);\n\n  ups = new URLSearchParams({over: 9000, spice: 'flow'});\n  testing.expectEqual(2, ups.size);\n  testing.expectEqual(['9000'], ups.getAll('over'));\n  testing.expectEqual(['flow'], ups.getAll('spice'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/window/frames.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<body>\n <iframe src=\"https://httpbin.io/html\" title=\"iframea\"></iframe>\n <iframe src=\"https://httpbin.io/html\" title=\"iframeb\"></iframe>\n</body>\n\n<script id=frames>\n  testing.expectEqual(2, frames.length);\n  testing.expectEqual(undefined, frames[3])\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/window/window.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body style=height:4000px;width:4000px></body>\n\n<script id=aliases>\n  testing.expectEqual(window, window.self);\n  testing.expectEqual(window, window.parent);\n  testing.expectEqual(window, window.top);\n  testing.expectEqual(window, window.frames);\n  testing.expectEqual(0, window.frames.length);\n</script>\n\n<script id=request_animation>\n  let start = 0;\n  function step(timestamp) {\n    start = timestamp;\n  }\n  requestAnimationFrame(step);\n  testing.eventually(() => {\n    testing.expectEqual(true, start > 0)\n  });\n\n  let request_id = requestAnimationFrame(() => {\n    start = 0;\n  });\n  cancelAnimationFrame(request_id);\n  testing.eventually(() => testing.expectEqual(true, start > 0));\n</script>\n\n<script id=setTimeout>\n  let longCall = false;\n  window.setTimeout(() => {longCall = true}, 5001);\n  testing.eventually(() => testing.expectEqual(false, longCall));\n\n  let wst1 = 0;\n  window.setTimeout(() => {wst1 += 1}, 1);\n  testing.eventually(() => testing.expectEqual(1, wst1));\n\n  let wst2 = 1;\n  window.setTimeout((a, b) => {\n    wst2 = a + b;\n  }, 1, 2, 3);\n  testing.eventually(() => testing.expectEqual(5, wst2));\n</script>\n\n<script id=eventTarget>\n  let called = false;\n\n  window.addEventListener(\"ready\", (e) => {\n    called = (e.currentTarget == window);\n  }, {capture: false, once: false});\n\n  const evt = new Event(\"ready\", { bubbles: true, cancelable: false });\n  window.dispatchEvent(evt);\n  testing.expectEqual(true, called);\n</script>\n\n<script id=btoa_atob>\n  const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')\n  testing.expectEqual('aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==', b64);\n\n  const str = atob(b64)\n  testing.expectEqual('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder', str);\n\n  testing.expectError('Error: InvalidCharacterError', () => {\n    atob('b');\n  });\n</script>\n\n<script id=queueMicroTask>\n  var qm = false;\n  window.queueMicrotask(() => {qm = true });\n  testing.eventually(() => testing.expectEqual(true, qm));\n</script>\n\n<script id=DOMContentLoaded>\n  let dcl = false;\n  window.queueMicrotask(() => {qm = true });\n  window.addEventListener('DOMContentLoaded', (e) => {\n    dcl = e.target == document;\n  });\n  testing.eventually(() => testing.expectEqual(true, dcl));\n</script>\n\n<script id=window.onload>\n  let isDocumentTarget = false;\n\n  const callback = (e) => {\n    isDocumentTarget = e.target === document;\n  };\n  // Callback is not set yet.\n  testing.expectEqual(null, window.onload);\n  // Setting an object.\n  window.onload = {};\n  testing.expectEqual(null, window.onload);\n  // Callback is set.\n  window.onload = callback;\n  testing.expectEqual(callback, window.onload);\n\n  testing.eventually(() => testing.expectEqual(true, isDocumentTarget));\n</script>\n\n<script id=reportError>\n  let errorEventFired = false;\n  let capturedError = null;\n\n  window.addEventListener('error', (e) => {\n    errorEventFired = true;\n    capturedError = e.error;\n  });\n\n  const testError = new Error('Test error message');\n  window.reportError(testError);\n\n  testing.expectEqual(true, errorEventFired);\n  testing.expectEqual(testError, capturedError);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/xhr/form_data.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<form id=\"form1\">\n  <input id=\"has_no_name\" value=\"nope1\">\n  <input id=\"is_disabled\" disabled value=\"nope2\">\n\n  <input name=\"txt-1\" value=\"txt-1-v\">\n  <input name=\"txt-2\" value=\"txt-~-v\" type=password>\n\n  <input name=\"chk-3\" value=\"chk-3-va\" type=checkbox>\n  <input name=\"chk-3\" value=\"chk-3-vb\" type=checkbox checked>\n  <input name=\"chk-3\" value=\"chk-3-vc\" type=checkbox checked>\n  <input name=\"chk-4\" value=\"chk-4-va\" type=checkbox>\n  <input name=\"chk-4\" value=\"chk-4-va\" type=checkbox>\n\n  <input name=\"rdi-1\" value=\"rdi-1-va\" type=radio>\n  <input name=\"rdi-1\" value=\"rdi-1-vb\" type=radio>\n  <input name=\"rdi-1\" value=\"rdi-1-vc\" type=radio checked>\n  <input name=\"rdi-2\" value=\"rdi-2-va\" type=radio>\n  <input name=\"rdi-2\" value=\"rdi-2-vb\" type=radio>\n\n  <textarea name=\"ta-1\"> ta-1-v</textarea>\n  <textarea name=\"ta\"></textarea>\n\n  <input type=hidden name=h1 value=\"h1-v\">\n  <input type=hidden name=h2 value=\"h2-v\" disabled=disabled>\n\n  <select name=\"sel-1\"><option>blue<option>red</select>\n  <select name=\"sel-2\"><option>blue<option value=sel-2-v selected>red</select>\n  <select name=\"sel-3\"><option disabled>nope1<option>nope2</select>\n  <select name=\"mlt-1\" multiple><option>water<option>tea</select>\n  <select name=\"mlt-2\" multiple><option selected>water<option selected>tea<option>coffee</select>\n  <input type=submit id=s1 name=s1 value=s1-v>\n  <input type=submit name=s2 value=s2-v>\n  <input type=image name=i1 value=i1-v>\n</form>\n<input type=text name=abc value=123 form=form1>\n\n<script id=formData>\n  let f = new FormData();\n  testing.expectEqual(null, f.get('a'));\n  testing.expectEqual(false, f.has('a'));\n  testing.expectEqual([], f.getAll('a'));\n  testing.expectEqual(undefined, f.delete('a'));\n\n  f.set('a', 1);\n  testing.expectEqual(true, f.has('a'));\n  testing.expectEqual('1', f.get('a'));\n  testing.expectEqual(['1'], f.getAll('a'));\n\n  f.append('a', 2);\n  testing.expectEqual(true, f.has('a'));\n  testing.expectEqual('1', f.get('a'));\n  testing.expectEqual(['1', '2'], f.getAll('a'));\n\n  f.append('b', '3');\n  testing.expectEqual(true, f.has('a'));\n  testing.expectEqual('1', f.get('a'));\n  testing.expectEqual(['1', '2'], f.getAll('a'));\n  testing.expectEqual(true, f.has('b'));\n  testing.expectEqual('3', f.get('b'));\n  testing.expectEqual(['3'], f.getAll('b'));\n\n  let acc = [];\n  for (const key of f.keys()) { acc.push(key) }\n  testing.expectEqual(['a', 'a', 'b'], acc);\n\n  acc = [];\n  for (const value of f.values()) { acc.push(value) }\n  testing.expectEqual(['1', '2', '3'], acc);\n\n  acc = [];\n  for (const entry of f.entries()) { acc.push(entry) }\n  testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);\n\n  acc = [];\n  for (const entry of f) { acc.push(entry) };\n  testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);\n\n  f.delete('a');\n  testing.expectEqual(false, f.has('a'));\n  testing.expectEqual(true, f.has('b'));\n\n  acc = [];\n  for (const key of f.keys()) { acc.push(key) }\n  testing.expectEqual(['b'], acc);\n\n  acc = [];\n  for (const value of f.values()) { acc.push(value) }\n  testing.expectEqual(['3'], acc);\n\n  acc = [];\n  for (const entry of f.entries()) { acc.push(entry) }\n  testing.expectEqual([['b', '3']], acc);\n\n  acc = [];\n  for (const entry of f) { acc.push(entry) }\n  testing.expectEqual([['b', '3']], acc);\n</script>\n\n<script id=serialize>\n  let form1 = $('#form1');\n  let submit1 = $('#s1');\n\n  let input = document.createElement('input');\n  input.name = 'dyn';\n  input.value = 'dyn-v';\n  form1.appendChild(input);\n  let f2 = new FormData(form1, submit1);\n\n  acc = [];\n  for (const entry of f2) {\n    acc.push(entry);\n  };\n\n  testing.expectEqual(['txt-1', 'txt-1-v'], acc[0]);\n  testing.expectEqual(['txt-2', 'txt-~-v'], acc[1]);\n  testing.expectEqual(['chk-3', 'chk-3-vb'], acc[2]);\n  testing.expectEqual(['chk-3', 'chk-3-vc'], acc[3]);\n  testing.expectEqual(['rdi-1', 'rdi-1-vc'], acc[4]);\n  testing.expectEqual(['ta-1', ' ta-1-v'], acc[5]);\n  testing.expectEqual(['ta', ''], acc[6]);\n  testing.expectEqual(['h1', 'h1-v'], acc[7]);\n  testing.expectEqual(['sel-1', 'blue'], acc[8]);\n  testing.expectEqual(['sel-2', 'sel-2-v'], acc[9]);\n  testing.expectEqual(['sel-3', 'nope2'], acc[10]);\n  testing.expectEqual(['mlt-2', 'water'], acc[11]);\n  testing.expectEqual(['mlt-2', 'tea'], acc[12]);\n  testing.expectEqual(['s1', 's1-v'], acc[13]);\n</script>\n\n\n"
  },
  {
    "path": "src/browser/tests/legacy/xhr/progress_event.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=progressEvent>\n  let pevt = new ProgressEvent('foo');\n  testing.expectEqual(0, pevt.loaded);\n\n  testing.expectEqual(true, pevt instanceof ProgressEvent);\n\n  var eevt = null;\n  function ccbk(event) {\n    eevt =  event;\n  }\n  document.addEventListener('foo', ccbk)\n  document.dispatchEvent(pevt);\n  testing.expectEqual('foo', eevt.type);\n  testing.expectEqual(true, eevt instanceof ProgressEvent);\n</script>\n"
  },
  {
    "path": "src/browser/tests/legacy/xhr/xhr.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=xhr type=module>\n  const req = new XMLHttpRequest();\n  const promise1 = new Promise((resolve) => {\n    function cbk(event) {\n      resolve(event)\n    }\n\n    req.onload = cbk;\n    testing.expectEqual(cbk, req.onload);\n    req.onload = cbk;\n\n    req.open('GET', 'http://127.0.0.1:9589/xhr');\n    testing.expectEqual(0, req.status);\n    testing.expectEqual('', req.statusText);\n    testing.expectEqual('', req.getAllResponseHeaders());\n    testing.expectEqual(null, req.getResponseHeader('Content-Type'));\n    testing.expectEqual('', req.responseText);\n    req.send();\n  });\n\n  testing.async(promise1, (event) => {\n    testing.expectEqual('load', event.type);\n    testing.expectEqual(true, event.loaded > 0);\n    testing.expectEqual(true, event instanceof ProgressEvent);\n    testing.expectEqual(200, req.status);\n    testing.expectEqual('OK', req.statusText);\n    testing.expectEqual('text/html; charset=utf-8', req.getResponseHeader('Content-Type'));\n    testing.expectEqual('content-length: 100\\r\\nContent-Type: text/html; charset=utf-8\\r\\n', req.getAllResponseHeaders());\n    testing.expectEqual(100, req.responseText.length);\n    testing.expectEqual(req.responseText.length, req.response.length);\n  });\n</script>\n\n<script id=xhr2 type=module>\n  const req2 = new XMLHttpRequest()\n  const promise2 = new Promise((resolve) => {\n    req2.onload = resolve;\n    req2.open('GET', 'http://127.0.0.1:9589/xhr')\n    req2.responseType = 'document';\n    req2.send()\n  });\n\n  testing.async(promise2, () => {\n    testing.expectEqual(200, req2.status);\n    testing.expectEqual('OK', req2.statusText);\n    testing.expectEqual(true, req2.response instanceof Document);\n    testing.expectEqual(true, req2.responseXML instanceof Document);\n  });\n</script>\n\n<script id=xhr3 type=module>\n  const req3 = new XMLHttpRequest()\n  const promise3 = new Promise((resolve) => {\n    req3.onload = resolve;\n    req3.open('GET', 'http://127.0.0.1:9589/xhr/json')\n    req3.responseType = 'json';\n    req3.send()\n  });\n\n  testing.async(promise3, () => {\n    testing.expectEqual(200, req3.status);\n    testing.expectEqual('OK', req3.statusText);\n    testing.expectEqual('9000!!!', req3.response.over);\n  });\n</script>\n\n<script id=xhr4 type=module>\n  const req4 = new XMLHttpRequest()\n  const promise4 = new Promise((resolve) => {\n    req4.onload = resolve;\n    req4.open('POST', 'http://127.0.0.1:9589/xhr')\n    req4.send('foo')\n  });\n\n  testing.async(promise4, () => {\n    testing.expectEqual(200, req4.status);\n    testing.expectEqual('OK', req4.statusText);\n    testing.expectEqual(true, req4.responseText.length > 64);\n  });\n</script>\n\n<script id=xhr5 type=module>\n  const promise5 = new Promise((resolve) => {\n    var state = [];\n    const req5 = new XMLHttpRequest();\n    req5.onreadystatechange = (e) => {\n      state.push(req5.readyState);\n      if (req5.readyState === XMLHttpRequest.DONE) {\n        resolve({states: state, target: e.currentTarget});\n      }\n    }\n\n    req5.open('GET', 'http://127.0.0.1:9589/xhr');\n    req5.send();\n  });\n\n  testing.async(promise5, (result) => {\n    const {states: states, target: target} = result;\n    testing.expectEqual(4, states.length)\n    testing.expectEqual(XMLHttpRequest.OPENED, readyStates[0]);\n    testing.expectEqual(XMLHttpRequest.HEADERS_RECEIVED, readyStates[1]);\n    testing.expectEqual(XMLHttpRequest.LOADING, readyStates[2]);\n    testing.expectEqual(XMLHttpRequest.DONE, readyStates[3]);\n    testing.expectEqual(req5, target);\n  })\n</script>\n\n<script id=xhr6 type=module>\n  const req5 = new XMLHttpRequest()\n  const promise5 = new Promise((resolve) => {\n    req5.onload = resolve;\n    req5.open('PROPFIND', 'http://127.0.0.1:9589/xhr')\n    req5.send('foo')\n  });\n\n  testing.async(promise5, () => {\n    testing.expectEqual(200, req5.status);\n    testing.expectEqual('OK', req5.statusText);\n    testing.expectEqual(true, req5.responseText.length > 65);\n  });\n"
  },
  {
    "path": "src/browser/tests/legacy/xmlserializer.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<p id=\"para\"> And</p>\n<script id=xmlserializer>\n  const s = new XMLSerializer();\n  testing.expectEqual('<p id=\"para\"> And</p>', s.serializeToString($('#para')));\n  testing.expectEqual('<!DOCTYPE html>', s.serializeToString(document.doctype));\n</script>\n"
  },
  {
    "path": "src/browser/tests/mcp_actions.html",
    "content": "<!DOCTYPE html>\n<html>\n<body>\n    <button id=\"btn\" onclick=\"window.clicked = true;\">Click Me</button>\n    <input id=\"inp\" oninput=\"window.inputVal = this.value\" onchange=\"window.changed = true;\">\n    <select id=\"sel\" onchange=\"window.selChanged = this.value\">\n        <option value=\"opt1\">Option 1</option>\n        <option value=\"opt2\">Option 2</option>\n    </select>\n    <div id=\"scrollbox\" style=\"width: 100px; height: 100px; overflow: scroll;\" onscroll=\"window.scrolled = true;\">\n        <div style=\"height: 500px;\">Long content</div>\n    </div>\n</body>\n</html>\n"
  },
  {
    "path": "src/browser/tests/media/mediaerror.html",
    "content": "<!DOCTYPE html>\n<script src=\"..//testing.js\"></script>\n\n<script id=constants>\n  {\n    // Test that MediaError constants exist\n    testing.expectEqual(1, MediaError.MEDIA_ERR_ABORTED);\n    testing.expectEqual(2, MediaError.MEDIA_ERR_NETWORK);\n    testing.expectEqual(3, MediaError.MEDIA_ERR_DECODE);\n    testing.expectEqual(4, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/media/vttcue.html",
    "content": "<!DOCTYPE html>\n<script src=\"..//testing.js\"></script>\n\n<script id=basic>\n  {\n    // Test that VTTCue constructor exists\n    testing.expectEqual(\"function\", typeof VTTCue);\n    testing.expectEqual(\"function\", typeof TextTrackCue);\n\n    // Create a basic VTTCue\n    const cue = new VTTCue(0, 5, \"Hello World\");\n    testing.expectEqual(0, cue.startTime);\n    testing.expectEqual(5, cue.endTime);\n    testing.expectEqual(\"Hello World\", cue.text);\n  }\n\n  {\n    // Test property setters\n    const cue = new VTTCue(1.5, 10.25, \"Test text\");\n    cue.id = \"test-cue\";\n    testing.expectEqual(\"test-cue\", cue.id);\n\n    cue.startTime = 2.0;\n    testing.expectEqual(2.0, cue.startTime);\n\n    cue.text = \"Modified text\";\n    testing.expectEqual(\"Modified text\", cue.text);\n  }\n</script>\n\n<script id=defaults>\n  {\n    // Test default values\n    const cue = new VTTCue(0, 1, \"test\");\n    testing.expectEqual(\"\", cue.vertical);\n    testing.expectEqual(true, cue.snapToLines);\n    testing.expectEqual(\"auto\", cue.line);\n    testing.expectEqual(\"auto\", cue.position);\n    testing.expectEqual(100, cue.size);\n    testing.expectEqual(\"center\", cue.align);\n    testing.expectEqual(false, cue.pauseOnExit);\n  }\n</script>\n\n<script id=properties>\n  {\n    // Test setting various properties\n    const cue = new VTTCue(0, 5, \"test\");\n\n    cue.vertical = \"rl\";\n    testing.expectEqual(\"rl\", cue.vertical);\n\n    cue.snapToLines = false;\n    testing.expectEqual(false, cue.snapToLines);\n\n    cue.line = 10;\n    testing.expectEqual(10, cue.line);\n\n    cue.position = 50;\n    testing.expectEqual(50, cue.position);\n\n    cue.size = 75;\n    testing.expectEqual(75, cue.size);\n\n    cue.align = \"left\";\n    testing.expectEqual(\"left\", cue.align);\n\n    cue.pauseOnExit = true;\n    testing.expectEqual(true, cue.pauseOnExit);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/message_channel.html",
    "content": "<!DOCTYPE html>\n<body>\n<script src=\"testing.js\"></script>\n<script id=\"basic\">\n{\n    const channel = new MessageChannel();\n\n    testing.expectEqual(true, channel.port1 !== undefined);\n    testing.expectEqual(true, channel.port2 !== undefined);\n    testing.expectEqual(true, channel.port1 !== channel.port2);\n}\n\n{\n    const channel = new MessageChannel();\n    let received = null;\n\n    channel.port2.onmessage = function(e) {\n        received = e.data;\n    };\n\n    channel.port1.postMessage('hello');\n\n    setTimeout(() => {\n        testing.expectEqual('hello', received);\n    }, 10);\n}\n\ntesting.async(async () => {\n    let messages = [];\n\n    let p = new Promise((resolve) => {\n        const channel = new MessageChannel();\n        channel.port2.addEventListener('message', (e) => {\n            messages.push(e.data);\n            if (e.data === 'third') {\n                resolve();\n            }\n        });\n        channel.port2.start();\n\n        channel.port1.postMessage('first');\n        channel.port1.postMessage('second');\n        channel.port1.postMessage('third');\n    });\n\n\n    await p;\n    testing.expectEqual(3, messages.length);\n    testing.expectEqual('first', messages[0]);\n    testing.expectEqual('second', messages[1]);\n    testing.expectEqual('third', messages[2]);\n});\n\n{\n    const channel = new MessageChannel();\n    let port1Count = 0;\n    let port2Count = 0;\n\n    channel.port1.onmessage = () => { port1Count++; };\n    channel.port2.onmessage = () => { port2Count++; };\n\n    channel.port1.postMessage('to port2');\n    channel.port2.postMessage('to port1');\n\n    setTimeout(() => {\n        testing.expectEqual(1, port1Count);\n        testing.expectEqual(1, port2Count);\n    }, 30);\n}\n\n{\n    const channel = new MessageChannel();\n    let received = null;\n\n    channel.port2.onmessage = (e) => {\n        received = e.data;\n    };\n\n    channel.port1.postMessage({ type: 'test', value: 42 });\n\n    setTimeout(() => {\n        testing.expectEqual('test', received.type);\n        testing.expectEqual(42, received.value);\n    }, 40);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/attribute_filter.html",
    "content": "<!DOCTYPE html>\n<div id=\"test-element-1\" data-status=\"active\" data-username=\"john\" data-role=\"admin\">Test</div>\n<div id=\"test-element-2\" class=\"initial\">Test</div>\n<div id=\"test-element-3\">\n  <div id=\"child-element\" data-foo=\"bar\" data-baz=\"qux\">Child</div>\n</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"attribute_filter_single\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-1');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(element, {\n      attributes: true,\n      attributeFilter: ['data-status']\n    });\n\n    element.setAttribute('data-status', 'inactive');\n    element.setAttribute('data-username', 'jane');\n    element.setAttribute('data-role', 'user');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('attributes', mutations[0].type);\n      testing.expectEqual('data-status', mutations[0].attributeName);\n    });\n  });\n</script>\n\n<script id=\"attribute_filter_multiple\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-1');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(element, {\n      attributes: true,\n      attributeFilter: ['data-status', 'data-username']\n    });\n\n    element.setAttribute('data-status', 'active');\n    element.setAttribute('data-username', 'alice');\n    element.setAttribute('data-role', 'moderator');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(2, mutations.length);\n      testing.expectEqual('data-status', mutations[0].attributeName);\n      testing.expectEqual('data-username', mutations[1].attributeName);\n    });\n  });\n</script>\n\n<script id=\"attribute_filter_with_old_value\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-2');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(element, {\n      attributes: true,\n      attributeOldValue: true,\n      attributeFilter: ['class']\n    });\n\n    element.setAttribute('class', 'changed');\n    element.setAttribute('data-ignored', 'value');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('class', mutations[0].attributeName);\n      testing.expectEqual('initial', mutations[0].oldValue);\n    });\n  });\n</script>\n\n<script id=\"attribute_filter_no_match\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-2');\n\n    let callbackCalled = false;\n    const observer = new MutationObserver(() => {\n      callbackCalled = true;\n    });\n    observer.observe(element, {\n      attributes: true,\n      attributeFilter: ['data-filtered']\n    });\n\n    element.setAttribute('class', 'another-change');\n    element.setAttribute('data-other', 'value');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(false, callbackCalled);\n      observer.disconnect();\n    });\n  });\n</script>\n\n<script id=\"attribute_filter_with_subtree\">\n  testing.async(async () => {\n    const parent = document.getElementById('test-element-3');\n    const child = document.getElementById('child-element');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(parent, {\n      attributes: true,\n      subtree: true,\n      attributeFilter: ['data-foo']\n    });\n\n    child.setAttribute('data-foo', 'changed');\n    child.setAttribute('data-baz', 'ignored');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('data-foo', mutations[0].attributeName);\n      testing.expectEqual(child, mutations[0].target);\n    });\n  });\n</script>\n\n<script id=\"attribute_filter_empty_array\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-2');\n\n    let callbackCalled = false;\n    const observer = new MutationObserver(() => {\n      callbackCalled = true;\n    });\n    observer.observe(element, {\n      attributes: true,\n      attributeFilter: []\n    });\n\n    element.setAttribute('class', 'yet-another-change');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(false, callbackCalled);\n      observer.disconnect();\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/character_data.html",
    "content": "<!DOCTYPE html>\n<div id=\"test-element-1\">Initial text</div>\n<div id=\"test-element-2\">Test</div>\n<div id=\"test-element-3\">Test</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"character_data\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-1');\n    const textNode = element.firstChild;\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(textNode, { characterData: true });\n\n    textNode.data = 'Changed text';\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('characterData', mutations[0].type);\n      testing.expectEqual(textNode, mutations[0].target);\n      testing.expectEqual(null, mutations[0].oldValue);\n    });\n  });\n</script>\n\n<script id=\"character_data_old_value\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-2');\n    const textNode = element.firstChild;\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(textNode, { characterData: true, characterDataOldValue: true });\n\n    textNode.data = 'First change';\n    textNode.data = 'Second change';\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(2, mutations.length);\n\n      testing.expectEqual('characterData', mutations[0].type);\n      testing.expectEqual(textNode, mutations[0].target);\n      testing.expectEqual('Test', mutations[0].oldValue);\n\n      testing.expectEqual('characterData', mutations[1].type);\n      testing.expectEqual(textNode, mutations[1].target);\n      testing.expectEqual('First change', mutations[1].oldValue);\n    });\n  });\n</script>\n\n<script id=\"character_data_no_observe\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-3');\n    const textNode = element.firstChild;\n\n    let callbackCalled = false;\n    const observer = new MutationObserver(() => {\n      callbackCalled = true;\n    });\n\n    // Observe for attributes, not characterData\n    observer.observe(element, { attributes: true });\n\n    // Change text node data (should not trigger)\n    textNode.data = 'Changed but not observed';\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(false, callbackCalled);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/childlist.html",
    "content": "<!DOCTYPE html>\n<div id=\"parent\"><div id=\"child1\">Child 1</div></div>\n<div id=\"empty-parent\"></div>\n<div id=\"remove-parent\"><div id=\"only-child\">Only</div></div>\n<div id=\"text-parent\"></div>\n<div id=\"middle-parent\"><div id=\"first\">First</div><div id=\"middle\">Middle</div><div id=\"last\">Last</div></div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"childlist\">\n  testing.async(async () => {\n    const parent = document.getElementById('parent');\n    const child1 = document.getElementById('child1');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(parent, { childList: true });\n\n    const child2 = document.createElement('div');\n    child2.textContent = 'Child 2';\n    parent.appendChild(child2);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(parent, mutations[0].target);\n      testing.expectEqual(1, mutations[0].addedNodes.length);\n      testing.expectEqual(child2, mutations[0].addedNodes[0]);\n      testing.expectEqual(0, mutations[0].removedNodes.length);\n      testing.expectEqual(child1, mutations[0].previousSibling);\n      testing.expectEqual(null, mutations[0].nextSibling);\n    });\n  });\n</script>\n\n<script id=\"childlist_empty_parent\">\n  testing.async(async () => {\n    const emptyParent = document.getElementById('empty-parent');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(emptyParent, { childList: true });\n\n    const firstChild = document.createElement('div');\n    firstChild.textContent = 'First child';\n    emptyParent.appendChild(firstChild);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(emptyParent, mutations[0].target);\n      testing.expectEqual(1, mutations[0].addedNodes.length);\n      testing.expectEqual(firstChild, mutations[0].addedNodes[0]);\n      testing.expectEqual(0, mutations[0].removedNodes.length);\n      testing.expectEqual(null, mutations[0].previousSibling);\n      testing.expectEqual(null, mutations[0].nextSibling);\n    });\n  });\n</script>\n\n<script id=\"childlist_remove_last\">\n  testing.async(async () => {\n    const removeParent = document.getElementById('remove-parent');\n    const onlyChild = document.getElementById('only-child');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(removeParent, { childList: true });\n\n    removeParent.removeChild(onlyChild);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(removeParent, mutations[0].target);\n      testing.expectEqual(0, mutations[0].addedNodes.length);\n      testing.expectEqual(1, mutations[0].removedNodes.length);\n      testing.expectEqual(onlyChild, mutations[0].removedNodes[0]);\n      testing.expectEqual(null, mutations[0].previousSibling);\n      testing.expectEqual(null, mutations[0].nextSibling);\n    });\n  });\n</script>\n\n<script id=\"childlist_text_node\">\n  testing.async(async () => {\n    const textParent = document.getElementById('text-parent');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(textParent, { childList: true });\n\n    const textNode = document.createTextNode('Hello world');\n    textParent.appendChild(textNode);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(textParent, mutations[0].target);\n      testing.expectEqual(1, mutations[0].addedNodes.length);\n      testing.expectEqual(textNode, mutations[0].addedNodes[0]);\n      testing.expectEqual(null, mutations[0].previousSibling);\n      testing.expectEqual(null, mutations[0].nextSibling);\n    });\n  });\n</script>\n\n<script id=\"childlist_remove_middle\">\n  testing.async(async () => {\n    const middleParent = document.getElementById('middle-parent');\n    const first = document.getElementById('first');\n    const middle = document.getElementById('middle');\n    const last = document.getElementById('last');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(middleParent, { childList: true });\n\n    middleParent.removeChild(middle);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(middleParent, mutations[0].target);\n      testing.expectEqual(0, mutations[0].addedNodes.length);\n      testing.expectEqual(1, mutations[0].removedNodes.length);\n      testing.expectEqual(middle, mutations[0].removedNodes[0]);\n      testing.expectEqual(first, mutations[0].previousSibling);\n      testing.expectEqual(last, mutations[0].nextSibling);\n    });\n  });\n</script>\n\n<div id=\"insert-parent\"><div id=\"insert-first\">First</div><div id=\"insert-last\">Last</div></div>\n\n<script id=\"childlist_insert_before\">\n  testing.async(async () => {\n    const insertParent = document.getElementById('insert-parent');\n    const insertFirst = document.getElementById('insert-first');\n    const insertLast = document.getElementById('insert-last');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(insertParent, { childList: true });\n\n    const insertMiddle = document.createElement('div');\n    insertMiddle.textContent = 'Middle';\n    insertParent.insertBefore(insertMiddle, insertLast);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(insertParent, mutations[0].target);\n      testing.expectEqual(1, mutations[0].addedNodes.length);\n      testing.expectEqual(insertMiddle, mutations[0].addedNodes[0]);\n      testing.expectEqual(0, mutations[0].removedNodes.length);\n      testing.expectEqual(insertFirst, mutations[0].previousSibling);\n      testing.expectEqual(insertLast, mutations[0].nextSibling);\n    });\n  });\n</script>\n\n<div id=\"replace-parent\"><div id=\"replace-old\">Old</div></div>\n\n<script id=\"childlist_replace_child\">\n  testing.async(async () => {\n    const replaceParent = document.getElementById('replace-parent');\n    const replaceOld = document.getElementById('replace-old');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(replaceParent, { childList: true });\n\n    const replaceNew = document.createElement('div');\n    replaceNew.textContent = 'New';\n    replaceParent.replaceChild(replaceNew, replaceOld);\n\n    Promise.resolve().then(() => {\n      // replaceChild generates two separate mutation records in modern spec:\n      // 1. First record for insertBefore (new node added)\n      // 2. Second record for removeChild (old node removed)\n      testing.expectEqual(2, mutations.length);\n\n      // First mutation: insertion of new node\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(replaceParent, mutations[0].target);\n      testing.expectEqual(1, mutations[0].addedNodes.length);\n      testing.expectEqual(replaceNew, mutations[0].addedNodes[0]);\n      testing.expectEqual(0, mutations[0].removedNodes.length);\n      testing.expectEqual(null, mutations[0].previousSibling);\n      testing.expectEqual(replaceOld, mutations[0].nextSibling);\n\n      // Second mutation: removal of old node\n      testing.expectEqual('childList', mutations[1].type);\n      testing.expectEqual(replaceParent, mutations[1].target);\n      testing.expectEqual(0, mutations[1].addedNodes.length);\n      testing.expectEqual(1, mutations[1].removedNodes.length);\n      testing.expectEqual(replaceOld, mutations[1].removedNodes[0]);\n      testing.expectEqual(replaceNew, mutations[1].previousSibling);\n      testing.expectEqual(null, mutations[1].nextSibling);\n    });\n  });\n</script>\n\n<div id=\"multiple-parent\"></div>\n\n<script id=\"childlist_multiple_mutations\">\n  testing.async(async () => {\n    const multipleParent = document.getElementById('multiple-parent');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(multipleParent, { childList: true });\n\n    const child1 = document.createElement('div');\n    child1.textContent = 'Child 1';\n    multipleParent.appendChild(child1);\n\n    const child2 = document.createElement('div');\n    child2.textContent = 'Child 2';\n    multipleParent.appendChild(child2);\n\n    const child3 = document.createElement('div');\n    child3.textContent = 'Child 3';\n    multipleParent.appendChild(child3);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(3, mutations.length);\n\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(child1, mutations[0].addedNodes[0]);\n      testing.expectEqual(null, mutations[0].previousSibling);\n      testing.expectEqual(null, mutations[0].nextSibling);\n\n      testing.expectEqual('childList', mutations[1].type);\n      testing.expectEqual(child2, mutations[1].addedNodes[0]);\n      testing.expectEqual(child1, mutations[1].previousSibling);\n      testing.expectEqual(null, mutations[1].nextSibling);\n\n      testing.expectEqual('childList', mutations[2].type);\n      testing.expectEqual(child3, mutations[2].addedNodes[0]);\n      testing.expectEqual(child2, mutations[2].previousSibling);\n      testing.expectEqual(null, mutations[2].nextSibling);\n    });\n  });\n</script>\n\n<div id=\"inner-html-parent\"><div>Old 1</div><div>Old 2</div><div>Old 3</div></div>\n\n<script id=\"childlist_inner_html\">\n  testing.async(async () => {\n    const innerHtmlParent = document.getElementById('inner-html-parent');\n    const oldChildren = Array.from(innerHtmlParent.children);\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(innerHtmlParent, { childList: true });\n\n    // No whitespace between tags to avoid text node mutations\n    innerHtmlParent.innerHTML = '<span>New 1</span><span>New 2</span><span>New 3</span>';\n\n    Promise.resolve().then(() => {\n      // innerHTML triggers mutations for both removals and additions\n      // With tri-state: from_parser=true + parse_mode=fragment -> mutations fire\n      // HTML wrapper element is filtered out, so: 3 removals + 3 additions = 6\n      testing.expectEqual(6, mutations.length);\n\n      // First 3: removals\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(1, mutations[0].removedNodes.length);\n      testing.expectEqual(oldChildren[0], mutations[0].removedNodes[0]);\n      testing.expectEqual(0, mutations[0].addedNodes.length);\n\n      testing.expectEqual('childList', mutations[1].type);\n      testing.expectEqual(1, mutations[1].removedNodes.length);\n      testing.expectEqual(oldChildren[1], mutations[1].removedNodes[0]);\n      testing.expectEqual(0, mutations[1].addedNodes.length);\n\n      testing.expectEqual('childList', mutations[2].type);\n      testing.expectEqual(1, mutations[2].removedNodes.length);\n      testing.expectEqual(oldChildren[2], mutations[2].removedNodes[0]);\n      testing.expectEqual(0, mutations[2].addedNodes.length);\n\n      // Last 3: additions (unwrapped span elements)\n      testing.expectEqual('childList', mutations[3].type);\n      testing.expectEqual(0, mutations[3].removedNodes.length);\n      testing.expectEqual(1, mutations[3].addedNodes.length);\n      testing.expectEqual('SPAN', mutations[3].addedNodes[0].nodeName);\n\n      testing.expectEqual('childList', mutations[4].type);\n      testing.expectEqual(0, mutations[4].removedNodes.length);\n      testing.expectEqual(1, mutations[4].addedNodes.length);\n      testing.expectEqual('SPAN', mutations[4].addedNodes[0].nodeName);\n\n      testing.expectEqual('childList', mutations[5].type);\n      testing.expectEqual(0, mutations[5].removedNodes.length);\n      testing.expectEqual(1, mutations[5].addedNodes.length);\n      testing.expectEqual('SPAN', mutations[5].addedNodes[0].nodeName);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/multiple_observers.html",
    "content": "<!DOCTYPE html>\n<div id=\"target\">Test</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"multiple_observers\">\n  testing.async(async () => {\n    const element = document.getElementById('target');\n\n    let records1 = null;\n    let records2 = null;\n    let callbackCount = 0;\n\n    const observer1 = new MutationObserver((records) => {\n      records1 = records;\n      observer1.disconnect();\n      callbackCount++;\n    });\n\n    const observer2 = new MutationObserver((records) => {\n      records2 = records;\n      observer2.disconnect();\n      callbackCount++;\n    });\n\n    observer1.observe(element, { attributes: true });\n    observer2.observe(element, { attributes: true, attributeOldValue: true });\n\n    element.setAttribute('data-foo', 'bar');\n    element.setAttribute('data-foo', 'baz');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(2, callbackCount);\n      testing.expectEqual(2, records1.length);\n      testing.expectEqual(2, records2.length);\n\n      testing.expectEqual('data-foo', records1[0].attributeName);\n      testing.expectEqual(null, records1[0].oldValue);\n      testing.expectEqual('data-foo', records1[1].attributeName);\n      testing.expectEqual(null, records1[1].oldValue);\n\n      testing.expectEqual('data-foo', records2[0].attributeName);\n      testing.expectEqual(null, records2[0].oldValue);\n      testing.expectEqual('data-foo', records2[1].attributeName);\n      testing.expectEqual('bar', records2[1].oldValue);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/mutation_observer.html",
    "content": "<!DOCTYPE html>\n<div id=\"test-element-1\" class=\"initial\">Test</div>\n<div id=\"test-element-2\">Test</div>\n<div id=\"test-element-3\">Test</div>\n<div id=\"test-element-4\">Test</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"mutation_observer\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-1');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(element, { attributes: true });\n\n    element.setAttribute('data-foo', 'bar');\n    element.setAttribute('class', 'changed');\n    element.removeAttribute('data-foo');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(3, mutations.length);\n\n      testing.expectEqual('attributes', mutations[0].type);\n      testing.expectEqual(element, mutations[0].target);\n      testing.expectEqual('data-foo', mutations[0].attributeName);\n      testing.expectEqual(null, mutations[0].oldValue);\n\n      testing.expectEqual('attributes', mutations[1].type);\n      testing.expectEqual(element, mutations[1].target);\n      testing.expectEqual('class', mutations[1].attributeName);\n      testing.expectEqual(null, mutations[1].oldValue);\n\n      testing.expectEqual('attributes', mutations[2].type);\n      testing.expectEqual(element, mutations[2].target);\n      testing.expectEqual('data-foo', mutations[2].attributeName);\n      testing.expectEqual(null, mutations[2].oldValue);\n    });\n  });\n</script>\n\n<script id=\"mutation_observer_old_value\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-2');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(element, { attributes: true, attributeOldValue: true });\n\n    element.setAttribute('data-test', 'value1');\n    element.setAttribute('data-test', 'value2');\n    element.removeAttribute('data-test');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(3, mutations.length);\n\n      testing.expectEqual('data-test', mutations[0].attributeName);\n      testing.expectEqual(null, mutations[0].oldValue);\n\n      testing.expectEqual('data-test', mutations[1].attributeName);\n      testing.expectEqual('value1', mutations[1].oldValue);\n\n      testing.expectEqual('data-test', mutations[2].attributeName);\n      testing.expectEqual('value2', mutations[2].oldValue);\n    });\n  });\n</script>\n\n<script id=\"mutation_observer_disconnect\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-3');\n\n    let callbackCalled = false;\n    const observer = new MutationObserver(() => {\n      callbackCalled = true;\n    });\n\n    observer.observe(element, { attributes: true });\n    observer.disconnect();\n\n    element.setAttribute('data-disconnected', 'test');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(false, callbackCalled);\n    });\n  });\n</script>\n\n<script id=\"mutation_observer_take_records\">\n  testing.async(async () => {\n    const element = document.getElementById('test-element-4');\n\n    let callbackCalled = false;\n    const observer = new MutationObserver(() => {\n      callbackCalled = true;\n    });\n\n    observer.observe(element, { attributes: true });\n    element.setAttribute('data-take', 'records');\n\n    const taken = observer.takeRecords();\n    testing.expectEqual(1, taken.length);\n    testing.expectEqual('data-take', taken[0].attributeName);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(false, callbackCalled);\n    });\n  });\n</script>\n\n<script id=\"microtask_access_to_records\">\n  testing.async(async () => {\n    let savedRecords;\n    const promise = new Promise((resolve) => {\n      const element = document.createElement('div');\n      const observer = new MutationObserver((records) => {\n        // Save the records array itself\n        savedRecords = records;\n        resolve();\n        observer.disconnect();\n      });\n      observer.observe(element, { attributes: true });\n      element.setAttribute('test', 'value');\n    });\n\n    await promise;\n    // Force arena reset by making a Zig call\n    document.getElementsByTagName('*');\n\n    testing.expectEqual(1, savedRecords.length);\n    testing.expectEqual('attributes', savedRecords[0].type);\n    testing.expectEqual('test', savedRecords[0].attributeName);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/mutations_during_callback.html",
    "content": "<!DOCTYPE html>\n<div id=\"target\">Test</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"mutations_during_callback\">\n  testing.async(async () => {\n    const element = document.getElementById('target');\n\n    let callCount = 0;\n    let firstRecords = null;\n    let secondRecords = null;\n\n    const observer = new MutationObserver((records) => {\n      callCount++;\n      if (callCount === 1) {\n        firstRecords = records;\n        element.setAttribute('data-second', 'from-callback');\n      } else if (callCount === 2) {\n        secondRecords = records;\n        observer.disconnect();\n      }\n    });\n\n    observer.observe(element, { attributes: true });\n\n    element.setAttribute('data-first', 'initial');\n\n    Promise.resolve().then(() => {\n      // After first microtask, first callback should have run and triggered second mutation\n    }).then(() => {\n      // After second microtask, second callback should have run\n      testing.expectEqual(2, callCount);\n      testing.expectEqual(1, firstRecords.length);\n      testing.expectEqual('data-first', firstRecords[0].attributeName);\n      testing.expectEqual(1, secondRecords.length);\n      testing.expectEqual('data-second', secondRecords[0].attributeName);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/observe_multiple_targets.html",
    "content": "<!DOCTYPE html>\n<div id=\"target1\">Test1</div>\n<div id=\"target2\">Test2</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"observe_multiple_targets\">\n  testing.async(async () => {\n    const element1 = document.getElementById('target1');\n    const element2 = document.getElementById('target2');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n\n    observer.observe(element1, { attributes: true });\n    observer.observe(element2, { attributes: true });\n\n    element1.setAttribute('data-one', 'value1');\n    element2.setAttribute('data-two', 'value2');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(2, mutations.length);\n      testing.expectEqual(element1, mutations[0].target);\n      testing.expectEqual('data-one', mutations[0].attributeName);\n      testing.expectEqual(element2, mutations[1].target);\n      testing.expectEqual('data-two', mutations[1].attributeName);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/reobserve_same_target.html",
    "content": "<!DOCTYPE html>\n<div id=\"target\">Test</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"reobserve_same_target\">\n  testing.async(async () => {\n    const element = document.getElementById('target');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n\n    observer.observe(element, { attributes: true, attributeOldValue: false });\n    observer.observe(element, { attributes: true, attributeOldValue: true });\n\n    element.setAttribute('data-foo', 'value1');\n    element.setAttribute('data-foo', 'value2');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(2, mutations.length);\n      testing.expectEqual(null, mutations[0].oldValue);\n      testing.expectEqual('value1', mutations[1].oldValue);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/mutation_observer/subtree.html",
    "content": "<!DOCTYPE html>\n<div id=\"grandparent\">\n  <div id=\"parent\">\n    <div id=\"child\" data-test=\"initial\">Child</div>\n  </div>\n</div>\n\n<div id=\"text-grandparent\">\n  <div id=\"text-parent\">\n    <div id=\"text-container\">Text here</div>\n  </div>\n</div>\n\n<div id=\"childlist-grandparent\">\n  <div id=\"childlist-parent\"></div>\n</div>\n\n<script src=\"../testing.js\"></script>\n<script id=\"subtree_attributes\">\n  testing.async(async () => {\n    const grandparent = document.getElementById('grandparent');\n    const child = document.getElementById('child');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(grandparent, { attributes: true, subtree: true });\n\n    child.setAttribute('data-test', 'changed');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('attributes', mutations[0].type);\n      testing.expectEqual(child, mutations[0].target);\n      testing.expectEqual('data-test', mutations[0].attributeName);\n    });\n  });\n</script>\n\n<script id=\"subtree_attributes_without_subtree\">\n  testing.async(async () => {\n    const grandparent = document.getElementById('grandparent');\n    const child = document.getElementById('child');\n\n    let callbackCalled = false;\n    const observer = new MutationObserver(() => {\n      callbackCalled = true;\n    });\n    observer.observe(grandparent, { attributes: true, subtree: false });\n\n    child.setAttribute('data-no-subtree', 'test');\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(false, callbackCalled);\n      observer.disconnect();\n    });\n  });\n</script>\n\n<script id=\"subtree_character_data\">\n  testing.async(async () => {\n    const grandparent = document.getElementById('text-grandparent');\n    const container = document.getElementById('text-container');\n    const textNode = container.firstChild;\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(grandparent, {\n      characterData: true,\n      characterDataOldValue: true,\n      subtree: true\n    });\n\n    textNode.data = 'Changed text';\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('characterData', mutations[0].type);\n      testing.expectEqual(textNode, mutations[0].target);\n      testing.expectEqual('Text here', mutations[0].oldValue);\n    });\n  });\n</script>\n\n<script id=\"subtree_childlist\">\n  testing.async(async () => {\n    const grandparent = document.getElementById('childlist-grandparent');\n    const parent = document.getElementById('childlist-parent');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n    observer.observe(grandparent, { childList: true, subtree: true });\n\n    const newChild = document.createElement('div');\n    newChild.textContent = 'New child';\n    parent.appendChild(newChild);\n\n    Promise.resolve().then(() => {\n      testing.expectEqual(1, mutations.length);\n      testing.expectEqual('childList', mutations[0].type);\n      testing.expectEqual(parent, mutations[0].target);\n      testing.expectEqual(1, mutations[0].addedNodes.length);\n      testing.expectEqual(newChild, mutations[0].addedNodes[0]);\n    });\n  });\n</script>\n\n<script id=\"subtree_deep_nesting\">\n  testing.async(async () => {\n    const root = document.createElement('div');\n    const child = document.createElement('div');\n\n    let mutations = null;\n    const observer = new MutationObserver((records) => {\n      observer.disconnect();\n      mutations = records;\n    });\n\n    root.appendChild(child);\n    observer.observe(root, { attributes: true, subtree: true });\n    child.setAttribute('data-deep', 'value');\n\n    Promise.resolve().then(() => {\n      const lastMutation = mutations[mutations.length - 1];\n      testing.expectEqual('attributes', lastMutation.type);\n      testing.expectEqual(child, lastMutation.target);\n      testing.expectEqual('data-deep', lastMutation.attributeName);\n    });\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/navigator/navigator.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=navigator>\n  // Navigator should be accessible from window\n  testing.expectEqual(navigator, window.navigator);\n  testing.expectEqual(true, navigator.userAgent.length > 0);\n  testing.expectEqual(true, navigator.userAgent.includes('Lightpanda'));\n  testing.expectEqual('Netscape', navigator.appName);\n  testing.expectEqual('1.0', navigator.appVersion);\n  testing.expectEqual(true, navigator.platform.length > 0);\n\n  // Platform should be one of the known values\n  const validPlatforms = ['MacIntel', 'Win32', 'Linux x86_64', 'FreeBSD', 'Unknown'];\n  testing.expectEqual(true, validPlatforms.includes(navigator.platform));\n  testing.expectEqual('en-US', navigator.language);\n  testing.expectEqual(true, Array.isArray(navigator.languages));\n  testing.expectEqual(1, navigator.languages.length);\n  testing.expectEqual('en-US', navigator.languages[0]);\n  testing.expectEqual(true, navigator.onLine);\n  testing.expectEqual(true, navigator.cookieEnabled);\n  testing.expectEqual(true, navigator.hardwareConcurrency > 0);\n  testing.expectEqual(4, navigator.hardwareConcurrency);\n  testing.expectEqual(0, navigator.maxTouchPoints);\n  testing.expectEqual('', navigator.vendor);\n  testing.expectEqual('Gecko', navigator.product);\n  testing.expectEqual(false, navigator.javaEnabled());\n  testing.expectEqual(false, navigator.webdriver);\n</script>\n\n<script id=permission_query>\n    testing.async(async (restore) => {\n      const p = navigator.permissions.query({ name: 'notifications' });\n      testing.expectTrue(p instanceof Promise);\n      const status = await p;\n      restore();\n      testing.expectEqual('prompt', status.state);\n      testing.expectEqual('notifications', status.name);\n  });\n</script>\n\n<script id=storage_estimate>\n  testing.async(async (restore) => {\n    const p = navigator.storage.estimate();\n    testing.expectTrue(p instanceof Promise);\n\n    const estimate = await p;\n    restore();\n    testing.expectEqual(0, estimate.usage);\n    testing.expectEqual(1024 * 1024 * 1024, estimate.quota);\n  });\n</script>\n\n<script id=deviceMemory>\n  testing.expectEqual(8, navigator.deviceMemory);\n</script>\n\n<script id=getBattery>\n  testing.async(async (restore) => {\n    const p = navigator.getBattery();\n    try {\n      await p;\n      testing.fail('getBattery should reject');\n    } catch (err) {\n      restore();\n      testing.expectEqual('NotSupportedError', err.name);\n    }\n  });\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/net/fetch.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=fetch_basic>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr');\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n    testing.expectEqual('basic', response.type);\n    testing.expectEqual('http://127.0.0.1:9582/xhr', response.url);\n    testing.expectEqual(false, response.redirected);\n\n    // Check headers\n    const headers = response.headers;\n    testing.expectEqual('text/html; charset=utf-8', headers.get('Content-Type'));\n    testing.expectEqual('100', headers.get('content-length'));\n\n    // Check text response\n    const text = await response.text();\n    testing.expectEqual(100, text.length);\n  });\n</script>\n\n<script id=fetch_json>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr/json');\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n    testing.expectEqual('basic', response.type);\n    testing.expectEqual(false, response.redirected);\n\n    const json = await response.json();\n    testing.expectEqual('9000!!!', json.over);\n    testing.expectEqual(\"number\", typeof json.updated_at);\n    testing.expectEqual(1765867200000, json.updated_at);\n    testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);\n  });\n</script>\n\n<script id=fetch_post>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr', {\n      method: 'POST',\n      body: 'foo'\n    });\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n\n    const text = await response.text();\n    testing.expectEqual(true, text.length > 64);\n  });\n</script>\n\n<script id=fetch_redirect>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr/redirect');\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n    testing.expectEqual('http://127.0.0.1:9582/xhr', response.url);\n    testing.expectEqual(true, response.redirected);\n\n    const text = await response.text();\n    testing.expectEqual(100, text.length);\n  });\n</script>\n\n<script id=fetch_404>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr/404');\n    restore();\n\n    testing.expectEqual(404, response.status);\n    testing.expectEqual(false, response.ok);\n\n    const text = await response.text();\n    testing.expectEqual('Not Found', text);\n  });\n</script>\n\n<script id=fetch_500>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr/500');\n    restore();\n\n    testing.expectEqual(500, response.status);\n    testing.expectEqual(false, response.ok);\n\n    const text = await response.text();\n    testing.expectEqual('Internal Server Error', text);\n  });\n</script>\n\n<script id=fetch_request_object>\n  testing.async(async (restore) => {\n    const request = new Request('http://127.0.0.1:9582/xhr', {\n      method: 'GET'\n    });\n\n    testing.expectEqual('http://127.0.0.1:9582/xhr', request.url);\n    testing.expectEqual('GET', request.method);\n\n    const response = await fetch(request);\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n\n    const text = await response.text();\n    testing.expectEqual(100, text.length);\n  });\n</script>\n\n<script id=fetch_request_with_headers>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr', {\n      method: 'GET',\n      headers: {\n        'X-Custom-Header': 'test-value'\n      }\n    });\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n  });\n</script>\n\n<script id=fetch_request_with_method_variations>\n  for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) {\n    const request = new Request('http://127.0.0.1:9582/xhr', {\n      method: method\n    });\n    testing.expectEqual(method, request.method);\n  }\n</script>\n\n<script id=fetch_cache_credentials>\n  {\n    const request = new Request('http://127.0.0.1:9582/xhr', {\n      cache: 'no-cache',\n      credentials: 'same-origin'\n    });\n\n    testing.expectEqual('no-cache', request.cache);\n    testing.expectEqual('same-origin', request.credentials);\n  }\n</script>\n\n<script id=response_constructor>\n  testing.async(async (restore) => {\n    const response1 = new Response(null);\n    testing.expectEqual(200, response1.status);\n    testing.expectEqual(true, response1.ok);\n\n    const response2 = new Response('Hello', {\n      status: 201,\n    });\n    testing.expectEqual(201, response2.status);\n    testing.expectEqual(true, response2.ok);\n\n    const text = await response2.text();\n    restore();\n\n    testing.expectEqual('Hello', text);\n  });\n</script>\n\n<script id=response_body_stream>\n  testing.async(async (restore) => {\n    const response = await fetch('http://127.0.0.1:9582/xhr');\n    restore();\n\n    testing.expectEqual(true, response.body !== null);\n    testing.expectEqual(true, response.body instanceof ReadableStream);\n\n    const buf = await response.arrayBuffer()\n    restore();\n\n    const uint8array = new Uint8Array(buf);\n    const decoder = new TextDecoder('utf-8');\n    testing.expectEqual('1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', decoder.decode(uint8array));\n  });\n</script>\n\n<script id=response_empty_body>\n  testing.async(async (restore) => {\n    const response = new Response('');\n    testing.expectEqual(200, response.status);\n\n    const text = await response.text();\n    restore();\n\n    testing.expectEqual('', text);\n    // Empty body should still create a valid stream\n    testing.expectEqual(true, response.body !== null);\n  });\n</script>\n\n<script id=fetch_blob_url>\n  testing.async(async (restore) => {\n    // Create a blob and get its URL\n    const blob = new Blob(['Hello from blob!'], { type: 'text/plain' });\n    const blobUrl = URL.createObjectURL(blob);\n\n    const response = await fetch(blobUrl);\n    restore();\n\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(true, response.ok);\n    testing.expectEqual(blobUrl, response.url);\n    testing.expectEqual('text/plain', response.headers.get('Content-Type'));\n\n    const text = await response.text();\n    testing.expectEqual('Hello from blob!', text);\n\n    // Clean up\n    URL.revokeObjectURL(blobUrl);\n  });\n</script>\n\n<script id=abort>\n  testing.async(async (restore) => {\n    const controller = new AbortController();\n    controller.abort();\n    try {\n      await fetch('http://127.0.0.1:9582/xhr', { signal: controller.signal });\n      testain.fail('fetch should have been aborted');\n    } catch (e) {\n      restore();\n      testing.expectEqual(\"AbortError\", e.name);\n    }\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/net/form_data.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script>\n  function assert(expected, fd) {\n    for (let e of expected) {\n      testing.expectEqual(true, fd.has(e.key));\n      testing.expectEqual(expected.find((ee) => ee.key == e.key).value, fd.get(e.key));\n      testing.expectEqual(expected.filter((ee) => ee.key == e.key).map((ee) => ee.value), fd.getAll(e.key));\n    }\n    testing.expectEqual(null, fd.get(\"nope\"));\n    testing.expectEqual([], fd.getAll(\"nope\"));\n\n    testing.expectEqual(expected.map((e) => e.key), Array.from(fd.keys()));\n    testing.expectEqual(expected.map((e) => e.value), Array.from(fd.values()));\n    testing.expectEqual(expected.map((e) => [e.key, e.value]), Array.from(fd));\n    testing.expectEqual(expected.map((e) => [e.key, e.value]), Array.from(fd.entries()));\n  }\n</script>\n\n<script id=basic>\n{\n  const fd = new FormData();\n  assert([], fd);\n\n  fd.append('name', 'John');\n  assert([{key: 'name', value: 'John'}], fd);\n\n  fd.append('email', 'john@example.com');\n  assert([{key: 'name', value: 'John'}, {key: 'email', value: 'john@example.com'}], fd);\n}\n</script>\n\n<script id=manipulation>\n{\n  const fd = new FormData();\n  assert([], fd);\n\n  fd.append('field', 'value1')\n  assert([{key: 'field', value: 'value1'}], fd);\n\n  fd.append('field', 'value2')\n  assert([{key: 'field', value: 'value1'}, {key: 'field', value: 'value2'}], fd);\n\n  fd.set('field', 'only')\n  assert([{key: 'field', value: 'only'}], fd);\n}\n</script>\n\n<script id=emptyAndNull>\n{\n  const fd = new FormData();\n  testing.expectEqual(null, fd.get('anything'));\n  testing.expectEqual([], fd.getAll('anything'));\n  testing.expectEqual(false, fd.has('anything'));\n}\n</script>\n\n<script id=duplicateKeys>\n{\n  const fd = new FormData();\n  fd.append('key', '1');\n  fd.append('key', '2');\n  fd.append('key', '3');\n\n  testing.expectEqual('1', fd.get('key'));\n  testing.expectEqual(['1', '2', '3'], fd.getAll('key'));\n\n  fd.set('key', 'only');\n  testing.expectEqual('only', fd.get('key'));\n  testing.expectEqual(['only'], fd.getAll('key'));\n}\n</script>\n\n<script id=deleteOperations>\n{\n  const fd = new FormData();\n  fd.append('a', '1');\n  fd.append('b', '2');\n  fd.append('a', '3');\n  fd.append('c', '4');\n  fd.append('a', '5');\n\n  fd.delete('a');\n  testing.expectEqual(false, fd.has('a'));\n  testing.expectEqual('2', fd.get('b'));\n  testing.expectEqual('4', fd.get('c'));\n\n  fd.delete('nonexistent');\n  testing.expectEqual('2', fd.get('b'));\n  testing.expectEqual('4', fd.get('c'));\n}\n</script>\n\n<script id=iteration>\n{\n  const fd = new FormData();\n  fd.append('a', '1');\n  fd.append('b', '2');\n  fd.append('a', '3');\n\n  const keys = Array.from(fd.keys());\n  testing.expectEqual(['a', 'b', 'a'], keys);\n\n  const values = Array.from(fd.values());\n  testing.expectEqual(['1', '2', '3'], values);\n\n  const entries = Array.from(fd.entries());\n  testing.expectEqual([['a', '1'], ['b', '2'], ['a', '3']], entries);\n\n  const defaultIter = Array.from(fd);\n  testing.expectEqual([['a', '1'], ['b', '2'], ['a', '3']], defaultIter);\n}\n</script>\n\n<script id=iteratorIsolation>\n{\n  const fd = new FormData();\n  fd.append('a', '1');\n  fd.append('b', '2');\n  fd.append('c', '3');\n\n  const keys1 = fd.keys();\n  const keys2 = fd.keys();\n  const values1 = fd.values();\n  const entries1 = fd.entries();\n\n  testing.expectEqual('a', keys1.next().value);\n  testing.expectEqual('a', keys2.next().value);\n  testing.expectEqual('1', values1.next().value);\n  testing.expectEqual(['a', '1'], entries1.next().value);\n\n  testing.expectEqual('b', keys1.next().value);\n  testing.expectEqual('b', keys2.next().value);\n  testing.expectEqual('2', values1.next().value);\n  testing.expectEqual(['b', '2'], entries1.next().value);\n\n  testing.expectEqual('c', keys1.next().value);\n  testing.expectEqual('c', keys2.next().value);\n  testing.expectEqual('3', values1.next().value);\n  testing.expectEqual(['c', '3'], entries1.next().value);\n\n  testing.expectEqual(true, keys1.next().done);\n  testing.expectEqual(true, keys2.next().done);\n  testing.expectEqual(true, values1.next().done);\n  testing.expectEqual(true, entries1.next().done);\n}\n</script>\n\n<script id=iteratorLifetime>\n{\n  let keysIter;\n  let valuesIter;\n  let entriesIter;\n\n  {\n    const fd = new FormData();\n    fd.append('x', '10');\n    fd.append('y', '20');\n    fd.append('z', '30');\n    keysIter = fd.keys();\n    valuesIter = fd.values();\n    entriesIter = fd.entries();\n  }\n\n  testing.expectEqual('x', keysIter.next().value);\n  testing.expectEqual('10', valuesIter.next().value);\n  testing.expectEqual(['x', '10'], entriesIter.next().value);\n\n  testing.expectEqual('y', keysIter.next().value);\n  testing.expectEqual('20', valuesIter.next().value);\n  testing.expectEqual(['y', '20'], entriesIter.next().value);\n\n  testing.expectEqual('z', keysIter.next().value);\n  testing.expectEqual('30', valuesIter.next().value);\n  testing.expectEqual(['z', '30'], entriesIter.next().value);\n\n  testing.expectEqual(true, keysIter.next().done);\n  testing.expectEqual(true, valuesIter.next().done);\n  testing.expectEqual(true, entriesIter.next().done);\n}\n</script>\n\n<script id=forEach>\n{\n  const fd = new FormData();\n  fd.append('a', '1');\n  fd.append('b', '2');\n  fd.append('c', '3');\n\n  const results = [];\n  fd.forEach((value, key, formData) => {\n    results.push({value, key});\n    // Verify third argument is the FormData instance itself\n    testing.expectEqual(fd, formData);\n  });\n\n  testing.expectEqual([\n    {key: 'a', value: '1'},\n    {key: 'b', value: '2'},\n    {key: 'c', value: '3'}\n  ], results);\n}\n</script>\n\n<script id=forEachWithDuplicates>\n{\n  const fd = new FormData();\n  fd.append('x', '10');\n  fd.append('x', '20');\n  fd.append('y', '30');\n\n  const results = [];\n  fd.forEach((value, key) => {\n    results.push({key, value});\n  });\n\n  testing.expectEqual([\n    {key: 'x', value: '10'},\n    {key: 'x', value: '20'},\n    {key: 'y', value: '30'}\n  ], results);\n}\n</script>\n\n<script id=forEachEmpty>\n{\n  const fd = new FormData();\n  let called = false;\n\n  fd.forEach(() => {\n    called = true;\n  });\n\n  testing.expectEqual(false, called);\n}\n</script>\n\n<script id=forEachThisArg>\n{\n  const fd = new FormData();\n  fd.append('a', '1');\n  fd.append('b', '2');\n  const context = {sum: 0};\n\n  fd.forEach(function(value) {\n    this.sum += parseInt(value);\n  }, context);\n\n  testing.expectEqual(3, context.sum);\n}\n</script>\n\n<form id=\"form1\">\n  <input id=\"has_no_name\" value=\"nope1\">\n  <input id=\"is_disabled\" disabled value=\"nope2\">\n\n  <input name=\"txt-1\" value=\"txt-1-v\">\n  <input name=\"txt-2\" value=\"txt-~-v\" type=password>\n\n  <input name=\"chk-3\" value=\"chk-3-va\" type=checkbox>\n  <input name=\"chk-3\" value=\"chk-3-vb\" type=checkbox checked>\n  <input name=\"chk-3\" value=\"chk-3-vc\" type=checkbox checked>\n  <input name=\"chk-4\" value=\"chk-4-va\" type=checkbox>\n  <input name=\"chk-4\" value=\"chk-4-va\" type=checkbox>\n\n  <input name=\"rdi-1\" value=\"rdi-1-va\" type=radio>\n  <input name=\"rdi-1\" value=\"rdi-1-vb\" type=radio>\n  <input name=\"rdi-1\" value=\"rdi-1-vc\" type=radio checked>\n  <input name=\"rdi-2\" value=\"rdi-2-va\" type=radio>\n  <input name=\"rdi-2\" value=\"rdi-2-vb\" type=radio>\n\n  <textarea name=\"ta-1\"> ta-1-v</textarea>\n  <textarea name=\"ta\"></textarea>\n\n  <input type=hidden name=h1 value=\"h1-v\">\n  <input type=hidden name=h2 value=\"h2-v\" disabled=disabled>\n\n  <select name=\"sel-1\"><option>blue<option>red</select>\n  <select name=\"sel-2\"><option>blue<option value=sel-2-v selected>red</select>\n  <select name=\"sel-3\"><option disabled>nope1<option>nope2</select>\n  <select name=\"mlt-1\" multiple><option>water<option>tea</select>\n  <select name=\"mlt-2\" multiple><option selected>water<option selected>tea<option>coffee</select>\n  <input type=submit id=s1 name=s1 value=s1-v>\n  <input type=submit name=s2 value=s2-v>\n  <input type=image name=i1 value=i1-v>\n</form>\n<input type=text name=abc value=123 form=form1>\n\n<script id=formData>\n  let f = new FormData();\n  testing.expectEqual(null, f.get('a'));\n  testing.expectEqual(false, f.has('a'));\n  testing.expectEqual([], f.getAll('a'));\n  testing.expectEqual(undefined, f.delete('a'));\n\n  f.set('a', 1);\n  testing.expectEqual(true, f.has('a'));\n  testing.expectEqual('1', f.get('a'));\n  testing.expectEqual(['1'], f.getAll('a'));\n\n  f.append('a', 2);\n  testing.expectEqual(true, f.has('a'));\n  testing.expectEqual('1', f.get('a'));\n  testing.expectEqual(['1', '2'], f.getAll('a'));\n\n  f.append('b', '3');\n  testing.expectEqual(true, f.has('a'));\n  testing.expectEqual('1', f.get('a'));\n  testing.expectEqual(['1', '2'], f.getAll('a'));\n  testing.expectEqual(true, f.has('b'));\n  testing.expectEqual('3', f.get('b'));\n  testing.expectEqual(['3'], f.getAll('b'));\n\n  let acc = [];\n  for (const key of f.keys()) { acc.push(key) }\n  testing.expectEqual(['a', 'a', 'b'], acc);\n\n  acc = [];\n  for (const value of f.values()) { acc.push(value) }\n  testing.expectEqual(['1', '2', '3'], acc);\n\n  acc = [];\n  for (const entry of f.entries()) { acc.push(entry) }\n  testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);\n\n  acc = [];\n  for (const entry of f) { acc.push(entry) };\n  testing.expectEqual([['a', '1'], ['a', '2'], ['b', '3']], acc);\n\n  f.delete('a');\n  testing.expectEqual(false, f.has('a'));\n  testing.expectEqual(true, f.has('b'));\n\n  acc = [];\n  for (const key of f.keys()) { acc.push(key) }\n  testing.expectEqual(['b'], acc);\n\n  acc = [];\n  for (const value of f.values()) { acc.push(value) }\n  testing.expectEqual(['3'], acc);\n\n  acc = [];\n  for (const entry of f.entries()) { acc.push(entry) }\n  testing.expectEqual([['b', '3']], acc);\n\n  acc = [];\n  for (const entry of f) { acc.push(entry) }\n  testing.expectEqual([['b', '3']], acc);\n</script>\n\n<script id=serialize>\n  {\n    let form1 = $('#form1');\n    let submit1 = $('#s1');\n\n    let input = document.createElement('input');\n    input.name = 'dyn';\n    input.value = 'dyn-v';\n    form1.appendChild(input);\n    let f2 = new FormData(form1, submit1);\n\n    acc = [];\n    for (const entry of f2) {\n      acc.push(entry);\n    };\n\n    testing.expectEqual(['txt-1', 'txt-1-v'], acc[0]);\n    testing.expectEqual(['txt-2', 'txt-~-v'], acc[1]);\n    testing.expectEqual(['chk-3', 'chk-3-vb'], acc[2]);\n    testing.expectEqual(['chk-3', 'chk-3-vc'], acc[3]);\n    testing.expectEqual(['rdi-1', 'rdi-1-vc'], acc[4]);\n    testing.expectEqual(['ta-1', ' ta-1-v'], acc[5]);\n    testing.expectEqual(['ta', ''], acc[6]);\n    testing.expectEqual(['h1', 'h1-v'], acc[7]);\n    testing.expectEqual(['sel-1', 'blue'], acc[8]);\n    testing.expectEqual(['sel-2', 'sel-2-v'], acc[9]);\n    testing.expectEqual(['sel-3', 'nope2'], acc[10]);\n    testing.expectEqual(['mlt-2', 'water'], acc[11]);\n    testing.expectEqual(['mlt-2', 'tea'], acc[12]);\n    testing.expectEqual(['s1', 's1-v'], acc[13]);\n  }\n</script>\n\n<script id=submitterBehavior>\n{\n  // Test that only the specified submitter is included in FormData\n  const form = document.createElement('form');\n\n  const input1 = document.createElement('input');\n  input1.name = 'field1';\n  input1.value = 'value1';\n  form.appendChild(input1);\n\n  const submit1 = document.createElement('input');\n  submit1.type = 'submit';\n  submit1.name = 'action';\n  submit1.value = 'save';\n  form.appendChild(submit1);\n\n  const submit2 = document.createElement('input');\n  submit2.type = 'submit';\n  submit2.name = 'action';\n  submit2.value = 'delete';\n  form.appendChild(submit2);\n\n  const submit3 = document.createElement('input');\n  submit3.type = 'submit';\n  submit3.name = 'action';\n  submit3.value = 'cancel';\n  form.appendChild(submit3);\n\n  // FormData with submit2 as submitter - should only include submit2\n  const fd = new FormData(form, submit2);\n  testing.expectEqual('value1', fd.get('field1'));\n  testing.expectEqual('delete', fd.get('action'));\n  testing.expectEqual(['delete'], fd.getAll('action'));\n\n  // FormData with no submitter - should not include any submit buttons\n  const fd2 = new FormData(form);\n  testing.expectEqual('value1', fd2.get('field1'));\n  testing.expectEqual(null, fd2.get('action'));\n  testing.expectEqual([], fd2.getAll('action'));\n\n  // FormData with submit1 as submitter\n  const fd3 = new FormData(form, submit1);\n  testing.expectEqual('value1', fd3.get('field1'));\n  testing.expectEqual('save', fd3.get('action'));\n  testing.expectEqual(['save'], fd3.getAll('action'));\n}\n</script>\n\n<script id=imageSubmitter>\n{\n  // Test that image inputs add name.x and name.y coordinates\n  const form = document.createElement('form');\n\n  const input1 = document.createElement('input');\n  input1.name = 'username';\n  input1.value = 'alice';\n  form.appendChild(input1);\n\n  const image1 = document.createElement('input');\n  image1.type = 'image';\n  image1.name = 'submit';\n  image1.value = 'ignored'; // value is ignored for image inputs\n  form.appendChild(image1);\n\n  const fd = new FormData(form, image1);\n  testing.expectEqual('alice', fd.get('username'));\n  testing.expectEqual('0', fd.get('submit.x'));\n  testing.expectEqual('0', fd.get('submit.y'));\n  testing.expectEqual(null, fd.get('submit')); // name without .x/.y should not exist\n\n  // Verify order and that .x comes before .y\n  const entries = Array.from(fd.entries());\n  testing.expectEqual([['username', 'alice'], ['submit.x', '0'], ['submit.y', '0']], entries);\n}\n</script>\n\n<script id=multipleImagesWithSameName>\n{\n  // Test that when multiple images share a name, only submitter's coordinates are included\n  const form = document.createElement('form');\n\n  const img1 = document.createElement('input');\n  img1.type = 'image';\n  img1.name = 'coords';\n  form.appendChild(img1);\n\n  const img2 = document.createElement('input');\n  img2.type = 'image';\n  img2.name = 'coords';\n  form.appendChild(img2);\n\n  const img3 = document.createElement('input');\n  img3.type = 'image';\n  img3.name = 'coords';\n  form.appendChild(img3);\n\n  // Only img2 should be included\n  const fd = new FormData(form, img2);\n  testing.expectEqual(['0'], fd.getAll('coords.x'));\n  testing.expectEqual(['0'], fd.getAll('coords.y'));\n\n  const entries = Array.from(fd.entries());\n  testing.expectEqual([['coords.x', '0'], ['coords.y', '0']], entries);\n}\n</script>\n\n<script id=buttonSubmitter>\n{\n  // Test that <button> elements work as submitters\n  const form = document.createElement('form');\n\n  const input1 = document.createElement('input');\n  input1.name = 'data';\n  input1.value = 'test';\n  form.appendChild(input1);\n\n  const button1 = document.createElement('button');\n  button1.name = 'btn';\n  button1.value = 'first';\n  button1.type = 'submit';\n  form.appendChild(button1);\n\n  const button2 = document.createElement('button');\n  button2.name = 'btn';\n  button2.value = 'second';\n  button2.type = 'submit';\n  form.appendChild(button2);\n\n  // With button1 as submitter\n  const fd1 = new FormData(form, button1);\n  testing.expectEqual('test', fd1.get('data'));\n  testing.expectEqual('first', fd1.get('btn'));\n  testing.expectEqual(['first'], fd1.getAll('btn'));\n\n  // With button2 as submitter\n  const fd2 = new FormData(form, button2);\n  testing.expectEqual('test', fd2.get('data'));\n  testing.expectEqual('second', fd2.get('btn'));\n  testing.expectEqual(['second'], fd2.getAll('btn'));\n\n  // No submitter - no button included\n  const fd3 = new FormData(form);\n  testing.expectEqual('test', fd3.get('data'));\n  testing.expectEqual(null, fd3.get('btn'));\n}\n</script>\n\n<script id=mixedSubmitTypes>\n{\n  // Test mix of input[type=submit], input[type=image], and button\n  const form = document.createElement('form');\n\n  const field = document.createElement('input');\n  field.name = 'name';\n  field.value = 'Bob';\n  form.appendChild(field);\n\n  const submit = document.createElement('input');\n  submit.type = 'submit';\n  submit.name = 'op';\n  submit.value = 'save';\n  form.appendChild(submit);\n\n  const image = document.createElement('input');\n  image.type = 'image';\n  image.name = 'map';\n  form.appendChild(image);\n\n  const button = document.createElement('button');\n  button.type = 'submit';\n  button.name = 'op';\n  button.value = 'delete';\n  form.appendChild(button);\n\n  // Using image as submitter - only image coordinates included\n  const fd1 = new FormData(form, image);\n  testing.expectEqual('Bob', fd1.get('name'));\n  testing.expectEqual('0', fd1.get('map.x'));\n  testing.expectEqual('0', fd1.get('map.y'));\n  testing.expectEqual(null, fd1.get('op'));\n\n  // Using submit as submitter\n  const fd2 = new FormData(form, submit);\n  testing.expectEqual('Bob', fd2.get('name'));\n  testing.expectEqual('save', fd2.get('op'));\n  testing.expectEqual(null, fd2.get('map.x'));\n  testing.expectEqual(null, fd2.get('map.y'));\n\n  // Using button as submitter\n  const fd3 = new FormData(form, button);\n  testing.expectEqual('Bob', fd3.get('name'));\n  testing.expectEqual('delete', fd3.get('op'));\n  testing.expectEqual(null, fd3.get('map.x'));\n}\n</script>\n\n<script id=submitterWithoutName>\n{\n  // Test that submitter without name is not included\n  const form = document.createElement('form');\n\n  const input = document.createElement('input');\n  input.name = 'field';\n  input.value = 'data';\n  form.appendChild(input);\n\n  const submit = document.createElement('input');\n  submit.type = 'submit';\n  // no name attribute\n  submit.value = 'Submit';\n  form.appendChild(submit);\n\n  const fd = new FormData(form, submit);\n  testing.expectEqual('data', fd.get('field'));\n  // Should not have any submit-related entries\n  const entries = Array.from(fd.entries());\n  testing.expectEqual([['field', 'data']], entries);\n}\n</script>\n\n<script id=disabledFieldset>\n{\n  // Elements inside a disabled fieldset should not be included\n  const form = document.createElement('form');\n\n  const fieldset = document.createElement('fieldset');\n  fieldset.disabled = true;\n\n  const inside = document.createElement('input');\n  inside.name = 'inside';\n  inside.value = 'nope';\n  fieldset.appendChild(inside);\n\n  const outside = document.createElement('input');\n  outside.name = 'outside';\n  outside.value = 'yes';\n\n  form.appendChild(fieldset);\n  form.appendChild(outside);\n\n  const fd = new FormData(form);\n  testing.expectEqual(null, fd.get('inside'));\n  testing.expectEqual('yes', fd.get('outside'));\n}\n</script>\n\n<script id=disabledFieldsetLegendExemption>\n{\n  // Elements inside the FIRST legend of a disabled fieldset are NOT disabled\n  const form = document.createElement('form');\n\n  const fieldset = document.createElement('fieldset');\n  fieldset.disabled = true;\n\n  const legend = document.createElement('legend');\n  const inLegend = document.createElement('input');\n  inLegend.name = 'in-legend';\n  inLegend.value = 'exempt';\n  legend.appendChild(inLegend);\n\n  const notInLegend = document.createElement('input');\n  notInLegend.name = 'not-in-legend';\n  notInLegend.value = 'nope';\n\n  fieldset.appendChild(legend);\n  fieldset.appendChild(notInLegend);\n  form.appendChild(fieldset);\n\n  const fd = new FormData(form);\n  testing.expectEqual('exempt', fd.get('in-legend'));\n  testing.expectEqual(null, fd.get('not-in-legend'));\n}\n</script>\n\n<script id=disabledFieldsetSecondLegend>\n{\n  // Only the FIRST legend gets the exemption; second legend inputs are still disabled\n  const form = document.createElement('form');\n\n  const fieldset = document.createElement('fieldset');\n  fieldset.disabled = true;\n\n  const legend1 = document.createElement('legend');\n  const inLegend1 = document.createElement('input');\n  inLegend1.name = 'first-legend';\n  inLegend1.value = 'exempt';\n  legend1.appendChild(inLegend1);\n\n  const legend2 = document.createElement('legend');\n  const inLegend2 = document.createElement('input');\n  inLegend2.name = 'second-legend';\n  inLegend2.value = 'nope';\n  legend2.appendChild(inLegend2);\n\n  fieldset.appendChild(legend1);\n  fieldset.appendChild(legend2);\n  form.appendChild(fieldset);\n\n  const fd = new FormData(form);\n  testing.expectEqual('exempt', fd.get('first-legend'));\n  testing.expectEqual(null, fd.get('second-legend'));\n}\n</script>\n\n<script id=disabledFieldsetNested>\n{\n  // Outer fieldset disabled: inner enabled fieldset's elements are still disabled\n  const form = document.createElement('form');\n\n  const outer = document.createElement('fieldset');\n  outer.disabled = true;\n\n  const inner = document.createElement('fieldset');\n  // inner is NOT disabled itself\n\n  const input = document.createElement('input');\n  input.name = 'deep';\n  input.value = 'nope';\n  inner.appendChild(input);\n\n  outer.appendChild(inner);\n  form.appendChild(outer);\n\n  const fd = new FormData(form);\n  testing.expectEqual(null, fd.get('deep'));\n}\n</script>\n\n<script id=imageWithoutName>\n{\n  // Test that image input without name still submits x and y coordinates\n  const form = document.createElement('form');\n\n  const input = document.createElement('input');\n  input.name = 'field';\n  input.value = 'data';\n  form.appendChild(input);\n\n  const image = document.createElement('input');\n  image.type = 'image';\n  // no name attribute\n  form.appendChild(image);\n\n  const fd = new FormData(form, image);\n  testing.expectEqual('data', fd.get('field'));\n  // Image without name submits plain 'x' and 'y' keys\n  testing.expectEqual('0', fd.get('x'));\n  testing.expectEqual('0', fd.get('y'));\n  const entries = Array.from(fd.entries());\n  testing.expectEqual([['field', 'data'], ['x', '0'], ['y', '0']], entries);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/net/headers.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=basic>\n{\n  const headers = new Headers();\n  testing.expectEqual(null, headers.get('Content-Type'));\n  testing.expectEqual(false, headers.has('Content-Type'));\n\n  headers.set('Content-Type', 'application/json');\n  testing.expectEqual('application/json', headers.get('Content-Type'));\n  testing.expectEqual(true, headers.has('Content-Type'));\n\n  headers.set('Content-Type', 'text/html');\n  testing.expectEqual('text/html', headers.get('Content-Type'));\n\n  headers.delete('Content-Type');\n  testing.expectEqual(null, headers.get('Content-Type'));\n  testing.expectEqual(false, headers.has('Content-Type'));\n}\n</script>\n\n<script id=case-insensitive>\n// Headers should be case-insensitive per HTTP spec\n{\n  const headers = new Headers();\n\n  // Set with one case, get with another\n  headers.set('Content-Type', 'application/json');\n  testing.expectEqual('application/json', headers.get('content-type'));\n  testing.expectEqual('application/json', headers.get('CONTENT-TYPE'));\n  testing.expectEqual('application/json', headers.get('Content-Type'));\n\n  // has should be case-insensitive\n  testing.expectEqual(true, headers.has('content-type'));\n  testing.expectEqual(true, headers.has('CONTENT-TYPE'));\n  testing.expectEqual(true, headers.has('Content-Type'));\n\n  // delete should be case-insensitive\n  headers.delete('CONTENT-TYPE');\n  testing.expectEqual(false, headers.has('content-type'));\n  testing.expectEqual(false, headers.has('Content-Type'));\n}\n\n{\n  const headers = new Headers();\n\n  // Append with different cases - should all be treated as same header\n  headers.append('Accept', 'application/json');\n  headers.append('ACCEPT', 'text/html');\n  headers.append('accept', 'text/plain');\n\n  // Verify all values are present using iteration\n  const values = Array.from(headers.values());\n  testing.expectEqual(3, values.length);\n  testing.expectEqual('application/json', values[0]);\n  testing.expectEqual('text/html', values[1]);\n  testing.expectEqual('text/plain', values[2]);\n}\n\n{\n  const headers = new Headers();\n\n  // Set should replace regardless of case\n  headers.set('Authorization', 'Bearer token1');\n  headers.set('AUTHORIZATION', 'Bearer token2');\n\n  testing.expectEqual('Bearer token2', headers.get('authorization'));\n\n  // Should only have one entry after set replaces\n  const entries = Array.from(headers.entries());\n  testing.expectEqual(1, entries.length);\n  testing.expectEqual('authorization', entries[0][0]);\n  testing.expectEqual('Bearer token2', entries[0][1]);\n}\n\n{\n    const headers = new Headers({\"Set-Cookie\": \"name=world\"});\n    testing.expectEqual(\"name=world\", headers.get(\"set-cookie\"));\n}\n\n// Test object initialization with case normalization\n{\n  const headers = new Headers({\n    \"Content-Type\": \"application/json\",\n    \"AUTHORIZATION\": \"Bearer token\",\n    \"x-CuStOm\": \"mixed-case\"\n  });\n\n  // All headers should be normalized to lowercase\n  testing.expectEqual(\"application/json\", headers.get(\"content-type\"));\n  testing.expectEqual(\"Bearer token\", headers.get(\"authorization\"));\n  testing.expectEqual(\"mixed-case\", headers.get(\"x-custom\"));\n\n  // Verify via keys iterator that names are normalized\n  const keys = Array.from(headers.keys());\n  testing.expectEqual(3, keys.length);\n  testing.expectEqual(true, keys.includes(\"content-type\"));\n  testing.expectEqual(true, keys.includes(\"authorization\"));\n  testing.expectEqual(true, keys.includes(\"x-custom\"));\n}\n</script>\n\n<script id=array-initialization>\n// Test array initialization\n{\n  const headers = new Headers([\n    [\"Content-Type\", \"application/json\"],\n    [\"Authorization\", \"Bearer token123\"]\n  ]);\n\n  testing.expectEqual(\"application/json\", headers.get(\"Content-Type\"));\n  testing.expectEqual(\"Bearer token123\", headers.get(\"Authorization\"));\n  testing.expectEqual(true, headers.has(\"Content-Type\"));\n  testing.expectEqual(true, headers.has(\"Authorization\"));\n}\n\n// Test array initialization with case normalization\n{\n  const headers = new Headers([\n    [\"Content-Type\", \"text/html\"],\n    [\"AUTHORIZATION\", \"Bearer abc\"],\n    [\"x-CuStOm-HeAdEr\", \"value123\"]\n  ]);\n\n  // All header names should be normalized to lowercase\n  testing.expectEqual(\"text/html\", headers.get(\"content-type\"));\n  testing.expectEqual(\"Bearer abc\", headers.get(\"authorization\"));\n  testing.expectEqual(\"value123\", headers.get(\"x-custom-header\"));\n\n  // Verify case-insensitive access works\n  testing.expectEqual(\"text/html\", headers.get(\"CONTENT-TYPE\"));\n  testing.expectEqual(\"Bearer abc\", headers.get(\"Authorization\"));\n  testing.expectEqual(\"value123\", headers.get(\"X-CUSTOM-HEADER\"));\n\n  // Verify keys are normalized\n  const keys = Array.from(headers.keys());\n  testing.expectEqual(3, keys.length);\n  testing.expectEqual(\"content-type\", keys[0]);\n  testing.expectEqual(\"authorization\", keys[1]);\n  testing.expectEqual(\"x-custom-header\", keys[2]);\n}\n\n// Test array initialization with multiple values\n{\n  const headers = new Headers([\n    [\"Accept\", \"application/json\"],\n    [\"Accept\", \"text/html\"],\n    [\"Content-Type\", \"text/plain\"]\n  ]);\n\n  const entries = Array.from(headers.entries());\n  testing.expectEqual(3, entries.length);\n\n  // All Accept headers should be present\n  const acceptValues = entries.filter(e => e[0] === 'accept').map(e => e[1]);\n  testing.expectEqual(2, acceptValues.length);\n  testing.expectEqual(\"application/json\", acceptValues[0]);\n  testing.expectEqual(\"text/html\", acceptValues[1]);\n}\n</script>\n\n<script id=copy-constructor>\n// Test creating Headers from another Headers object\n{\n  const original = new Headers();\n  original.set(\"Content-Type\", \"application/json\");\n  original.set(\"Authorization\", \"Bearer token123\");\n  original.set(\"X-Custom\", \"test-value\");\n\n  const copy = new Headers(original);\n\n  // Copy should have all the same headers\n  testing.expectEqual(\"application/json\", copy.get(\"Content-Type\"));\n  testing.expectEqual(\"Bearer token123\", copy.get(\"Authorization\"));\n  testing.expectEqual(\"test-value\", copy.get(\"X-Custom\"));\n\n  // Copy should be independent\n  copy.set(\"X-Modified\", \"new-value\");\n  testing.expectEqual(\"new-value\", copy.get(\"X-Modified\"));\n  testing.expectEqual(null, original.get(\"X-Modified\"));\n\n  // Modifying copy shouldn't affect original\n  copy.set(\"Content-Type\", \"text/html\");\n  testing.expectEqual(\"text/html\", copy.get(\"Content-Type\"));\n  testing.expectEqual(\"application/json\", original.get(\"Content-Type\"));\n}\n\n// Test copy constructor with mixed-case headers\n{\n  const original = new Headers([\n    [\"Content-TYPE\", \"application/json\"],\n    [\"AUTHORIZATION\", \"Bearer xyz\"]\n  ]);\n\n  const copy = new Headers(original);\n\n  // Headers should be normalized in copy\n  testing.expectEqual(\"application/json\", copy.get(\"content-type\"));\n  testing.expectEqual(\"Bearer xyz\", copy.get(\"authorization\"));\n\n  const keys = Array.from(copy.keys());\n  testing.expectEqual(2, keys.length);\n  testing.expectEqual(\"content-type\", keys[0]);\n  testing.expectEqual(\"authorization\", keys[1]);\n}\n</script>\n\n<script id=iterators>\n// Test keys(), values(), entries() iterators\n{\n  const headers = new Headers();\n  headers.set('Content-Type', 'application/json');\n  headers.set('Authorization', 'Bearer token123');\n  headers.set('X-Custom', 'test-value');\n\n  // Test keys()\n  const keys = Array.from(headers.keys());\n  testing.expectEqual(3, keys.length);\n  testing.expectEqual('content-type', keys[0]);\n  testing.expectEqual('authorization', keys[1]);\n  testing.expectEqual('x-custom', keys[2]);\n\n  // Test values()\n  const values = Array.from(headers.values());\n  testing.expectEqual(3, values.length);\n  testing.expectEqual('application/json', values[0]);\n  testing.expectEqual('Bearer token123', values[1]);\n  testing.expectEqual('test-value', values[2]);\n\n  // Test entries()\n  const entries = Array.from(headers.entries());\n  testing.expectEqual(3, entries.length);\n  testing.expectEqual('content-type', entries[0][0]);\n  testing.expectEqual('application/json', entries[0][1]);\n  testing.expectEqual('authorization', entries[1][0]);\n  testing.expectEqual('Bearer token123', entries[1][1]);\n  testing.expectEqual('x-custom', entries[2][0]);\n  testing.expectEqual('test-value', entries[2][1]);\n}\n\n// Test forEach()\n{\n  const headers = new Headers();\n  headers.set('Content-Type', 'application/json');\n  headers.set('Authorization', 'Bearer token123');\n\n  const collected = [];\n  headers.forEach(function(value, name, headersObj) {\n    collected.push([name, value]);\n    testing.expectEqual(headers, headersObj);\n  });\n\n  testing.expectEqual(2, collected.length);\n  testing.expectEqual('content-type', collected[0][0]);\n  testing.expectEqual('application/json', collected[0][1]);\n  testing.expectEqual('authorization', collected[1][0]);\n  testing.expectEqual('Bearer token123', collected[1][1]);\n}\n\n// Test forEach with thisArg\n{\n  const headers = new Headers();\n  headers.set('X-Test', 'value');\n\n  const context = { count: 0 };\n  headers.forEach(function() {\n    this.count++;\n  }, context);\n\n  testing.expectEqual(1, context.count);\n}\n\n// Test multiple values for same header using append\n{\n  const headers = new Headers();\n  headers.append('Accept', 'application/json');\n  headers.append('Accept', 'text/html');\n  headers.append('Accept', 'text/plain');\n\n  const values = [];\n  headers.forEach((value, name) => {\n    if (name === 'accept') {\n      values.push(value);\n    }\n  });\n\n  testing.expectEqual(3, values.length);\n  testing.expectEqual('application/json', values[0]);\n  testing.expectEqual('text/html', values[1]);\n  testing.expectEqual('text/plain', values[2]);\n}\n</script>\n\n<script id=get-concatenation>\n// Test that get() returns concatenated values for headers with multiple values\n{\n  const headers = new Headers();\n  headers.append('Accept', 'application/json');\n  headers.append('Accept', 'text/html');\n  headers.append('Accept', 'text/plain');\n\n  // get() should return comma-separated concatenated values\n  const acceptValue = headers.get('Accept');\n  testing.expectEqual('application/json, text/html, text/plain', acceptValue);\n}\n\n// Test concatenation with case-insensitive header names\n{\n  const headers = new Headers();\n  headers.append('Content-Type', 'image/jpeg');\n  headers.append('CONTENT-TYPE', 'image/png');\n  headers.append('content-type', 'image/svg+xml');\n\n  const contentType = headers.get('Content-Type');\n  testing.expectEqual('image/jpeg, image/png, image/svg+xml', contentType);\n}\n\n// Test that set() replaces all values, not concatenates\n{\n  const headers = new Headers();\n  headers.append('Authorization', 'Bearer token1');\n  headers.append('Authorization', 'Bearer token2');\n\n  // Before set, should have both values\n  testing.expectEqual('Bearer token1, Bearer token2', headers.get('Authorization'));\n\n  // set() should replace all values\n  headers.set('Authorization', 'Bearer new-token');\n  testing.expectEqual('Bearer new-token', headers.get('Authorization'));\n\n  // Should only have one entry now\n  const entries = Array.from(headers.entries());\n  const authEntries = entries.filter(e => e[0] === 'authorization');\n  testing.expectEqual(1, authEntries.length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/net/request.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=basic>\n{\n  const req = new Request('https://example.com/api');\n  testing.expectEqual('https://example.com/api', req.url);\n  testing.expectEqual('GET', req.method);\n}\n\n{\n  const req = new Request('https://example.com/api', { method: 'POST' });\n  testing.expectEqual('https://example.com/api', req.url);\n  testing.expectEqual('POST', req.method);\n}\n\n{\n  const req = new Request('https://example.com/api', { method: 'post' });\n  testing.expectEqual('POST', req.method);\n}\n\n{\n  const req = new Request('/path');\n  testing.expectEqual(true, req.url.includes('/path'));\n}\n</script>\n\n<script id=headers>\n{\n  const req = new Request('https://example.com/api');\n  const headers = req.headers;\n  testing.expectEqual('object', typeof headers);\n\n  headers.set('Content-Type', 'application/json');\n  testing.expectEqual('application/json', headers.get('Content-Type'));\n\n  const headers2 = req.headers;\n  testing.expectEqual(true, headers === headers2);\n  testing.expectEqual('application/json', headers2.get('Content-Type'));\n}\n\n{\n  const headers = new Headers();\n  headers.set('X-Custom', 'value');\n\n  const req = new Request('https://example.com/api', { headers });\n  testing.expectEqual('value', req.headers.get('X-Custom'));\n}\n\n{\n  const req = new Request('https://example.com/api', {headers: {over: '9000!'}});\n  testing.expectEqual('9000!', req.headers.get('over'));\n}\n</script>\n\n<script id=request_input>\n{\n  const req1 = new Request('https://example.com/api', { method: 'POST' });\n  const req2 = new Request(req1);\n\n  testing.expectEqual('https://example.com/api', req2.url);\n  testing.expectEqual('POST', req2.method);\n}\n\n{\n  const req1 = new Request('https://example.com/api', { method: 'POST' });\n  const req2 = new Request(req1, { method: 'GET' });\n\n  testing.expectEqual('https://example.com/api', req2.url);\n  testing.expectEqual('GET', req2.method);\n}\n\n{\n  const headers = new Headers();\n  headers.set('X-Original', 'value1');\n  const req1 = new Request('https://example.com/api', { headers });\n\n  const newHeaders = new Headers();\n  newHeaders.set('X-New', 'value2');\n  const req2 = new Request(req1, { headers: newHeaders });\n\n  testing.expectEqual(null, req2.headers.get('X-Original'));\n  testing.expectEqual('value2', req2.headers.get('X-New'));\n}\n</script>\n\n<script id=stringifier>\n{\n  const url = new URL('https://example.com/api');\n  const req = new Request(url);\n  testing.expectEqual('https://example.com/api', req.url);\n}\n\n{\n  const anchor = document.createElement('a');\n  anchor.href = 'https://example.com/test';\n  const req = new Request(anchor);\n  testing.expectEqual('https://example.com/test', req.url);\n}\n\n{\n  const obj = {\n    toString() {\n      return 'https://example.com/custom';\n    }\n  };\n  const req = new Request(obj);\n  testing.expectEqual('https://example.com/custom', req.url);\n}\n</script>\n\n<script id=legacy>\n  {\n    let request = new Request(\"flower.png\");\n    testing.expectEqual(testing.BASE_URL + 'net/flower.png', request.url);\n    testing.expectEqual(\"GET\", request.method);\n\n    let request2 = new Request(\"https://google.com\", {\n      method: \"POST\",\n      body: \"Hello, World\",\n      cache: \"reload\",\n      credentials: \"omit\",\n      headers: { \"Sender\": \"me\", \"Target\": \"you\" }\n      }\n    );\n    testing.expectEqual(\"https://google.com\", request2.url);\n    testing.expectEqual(\"POST\", request2.method);\n    testing.expectEqual(\"omit\", request2.credentials);\n    testing.expectEqual(\"reload\", request2.cache);\n    testing.expectEqual(\"me\", request2.headers.get(\"SeNdEr\"));\n    testing.expectEqual(\"you\", request2.headers.get(\"target\"));\n  }\n</script>\n\n<script id=propfind>\n  {\n    const req = new Request('https://example.com/api', { method: 'propfind' });\n    testing.expectEqual('PROPFIND', req.method);\n  }\n</script>\n\n<script id=body_methods>\n  testing.async(async () => {\n    const req = new Request('https://example.com/api', {\n      method: 'POST',\n      body: 'Hello, World!',\n      headers: { 'Content-Type': 'text/plain' }\n    });\n\n    const text = await req.text();\n    testing.expectEqual('Hello, World!', text);\n  });\n\n  testing.async(async () => {\n    const req = new Request('https://example.com/api', {\n      method: 'POST',\n      body: '{\"name\": \"test\"}',\n      headers: { 'Content-Type': 'application/json' }\n    });\n\n    const json = await req.json();\n    testing.expectEqual('test', json.name);\n  });\n\n  testing.async(async () => {\n    const req = new Request('https://example.com/api', {\n      method: 'POST',\n      body: 'binary data',\n      headers: { 'Content-Type': 'application/octet-stream' }\n    });\n\n    const buffer = await req.arrayBuffer();\n    testing.expectEqual(true, buffer instanceof ArrayBuffer);\n    testing.expectEqual(11, buffer.byteLength);\n  });\n\n  testing.async(async () => {\n    const req = new Request('https://example.com/api', {\n      method: 'POST',\n      body: 'blob content',\n      headers: { 'Content-Type': 'text/plain' }\n    });\n\n    const blob = await req.blob();\n    testing.expectEqual(true, blob instanceof Blob);\n    testing.expectEqual(12, blob.size);\n    testing.expectEqual('text/plain', blob.type);\n  });\n\n  testing.async(async () => {\n    const req = new Request('https://example.com/api', {\n      method: 'POST',\n      body: 'bytes'\n    });\n\n    const bytes = await req.bytes();\n    testing.expectEqual(true, bytes instanceof Uint8Array);\n    testing.expectEqual(5, bytes.length);\n  });\n</script>\n\n<script id=clone>\n  {\n    const req1 = new Request('https://example.com/api', {\n      method: 'POST',\n      body: 'test body',\n      headers: { 'X-Custom': 'value' }\n    });\n\n    const req2 = req1.clone();\n\n    testing.expectEqual(req1.url, req2.url);\n    testing.expectEqual(req1.method, req2.method);\n    testing.expectEqual('value', req2.headers.get('X-Custom'));\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/net/response.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=response>\n  {\n    let response = new Response(\"Hello, World!\");\n    testing.expectEqual(200, response.status);\n    testing.expectEqual(\"\", response.statusText);\n    testing.expectEqual(true, response.ok);\n    testing.expectEqual(\"\", response.url);\n    testing.expectEqual(false, response.redirected);\n  }\n\n  {\n    let response2 = new Response(\"Error occurred\", {\n      status: 404,\n      statusText: \"Not Found\",\n      headers: {\n          \"Content-Type\": \"text/plain\",\n          \"X-Custom\": \"test-value\",\n          \"Cache-Control\": \"no-cache\"\n      }\n    });\n    testing.expectEqual(404, response2.status);\n    testing.expectEqual(\"Not Found\", response2.statusText);\n    testing.expectEqual(false, response2.ok);\n    testing.expectEqual(\"test-value\", response2.headers.get(\"X-Custom\"));\n    testing.expectEqual(\"no-cache\", response2.headers.get(\"cache-control\"));\n  }\n\n  {\n    let response3 = new Response(\"Created\", { status: 201, statusText: \"Created\" });\n    testing.expectEqual(\"basic\", response3.type);\n    testing.expectEqual(201, response3.status);\n    testing.expectEqual(\"Created\", response3.statusText);\n    testing.expectEqual(true, response3.ok);\n  }\n\n  {\n    let nullResponse = new Response(null);\n    testing.expectEqual(200, nullResponse.status);\n    testing.expectEqual(\"\", nullResponse.statusText);\n  }\n\n  {\n    let emptyResponse = new Response(\"\");\n    testing.expectEqual(200, emptyResponse.status);\n  }\n</script>\n\n<script id=body_methods>\n  testing.async(async () => {\n    const response = new Response('Hello, World!');\n    const text = await response.text();\n    testing.expectEqual('Hello, World!', text);\n  });\n\n  testing.async(async () => {\n    const response = new Response('{\"name\": \"test\"}');\n    const json = await response.json();\n    testing.expectEqual('test', json.name);\n  });\n\n  testing.async(async () => {\n    const response = new Response('binary data');\n    const buffer = await response.arrayBuffer();\n    testing.expectEqual(true, buffer instanceof ArrayBuffer);\n    testing.expectEqual(11, buffer.byteLength);\n  });\n\n  testing.async(async () => {\n    const response = new Response('blob content', {\n      headers: { 'Content-Type': 'text/plain' }\n    });\n    const blob = await response.blob();\n    testing.expectEqual(true, blob instanceof Blob);\n    testing.expectEqual(12, blob.size);\n    testing.expectEqual('text/plain', blob.type);\n  });\n\n  testing.async(async () => {\n    const response = new Response('bytes');\n    const bytes = await response.bytes();\n    testing.expectEqual(true, bytes instanceof Uint8Array);\n    testing.expectEqual(5, bytes.length);\n  });\n</script>\n\n<script id=clone>\n  {\n    const response1 = new Response('test body', {\n      status: 201,\n      statusText: 'Created',\n      headers: { 'X-Custom': 'value' }\n    });\n\n    const response2 = response1.clone();\n\n    testing.expectEqual(response1.status, response2.status);\n    testing.expectEqual(response1.statusText, response2.statusText);\n    testing.expectEqual('value', response2.headers.get('X-Custom'));\n  }\n\n  testing.async(async () => {\n    const response1 = new Response('cloned body');\n    const response2 = response1.clone();\n\n    const text1 = await response1.text();\n    const text2 = await response2.text();\n\n    testing.expectEqual('cloned body', text1);\n    testing.expectEqual('cloned body', text2);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/net/url_search_params.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script>\n  function assert(expected, usp) {\n    for (let e of expected) {\n      testing.expectEqual(true, usp.has(e.key));\n      testing.expectEqual(expected.find((ee) => ee.key == e.key).value, usp.get(e.key));\n      testing.expectEqual(expected.filter((ee) => ee.key == e.key).map((ee) => ee.value), usp.getAll(e.key));\n    }\n    testing.expectEqual(null, usp.get(\"nope\"));\n    testing.expectEqual([], usp.getAll(\"nope\"));\n\n    testing.expectEqual(expected.map((e) => e.key), Array.from(usp.keys()));\n    testing.expectEqual(expected.map((e) => e.value), Array.from(usp.values()));\n    testing.expectEqual(expected.map((e) => [e.key, e.value]), Array.from(usp));\n    testing.expectEqual(expected.map((e) => [e.key, e.value]), Array.from(usp.entries()));\n  }\n</script>\n\n<script id=urlSearchParams>\n  const inputs = [\n   // @ZIGDOM [[\"over\", \"9000!!\"], [\"abc\", 123], [\"key1\", \"\"], [\"key2\", \"\"]],\n   {over: \"9000!!\", abc: 123, key1: \"\", key2: \"\"},\n    \"over=9000!!&abc=123&key1&key2=\",\n    \"?over=9000!!&abc=123&key1&key2=\",\n  ]\n\n  const expected = [\n    {key: \"over\", value: \"9000!!\"},\n    {key: \"abc\", value: \"123\"},\n    {key: \"key1\", value: \"\"},\n    {key: \"key2\", value: \"\"},\n  ]\n\n  for (let input of inputs) {\n    assert(expected, new URLSearchParams(input));\n  }\n</script>\n\n<script id=manipulation>\n  {\n    const usp = new URLSearchParams();\n    assert([], usp);\n\n    usp.append('hi', 'abc123')\n    assert([{key: 'hi', value: 'abc123'}], usp);\n\n    usp.append('hi', 'xzi3lj')\n    assert([{key: 'hi', value: 'abc123'}, {key: 'hi', value: 'xzi3lj'}], usp);\n\n    testing.expectEqual('hi=abc123&hi=xzi3lj', usp.toString());\n\n    usp.set('hi', 'over 9000!!!')\n    assert([{key: 'hi', value: 'over 9000!!!'}], usp);\n    testing.expectEqual('hi=over+9000%21%21%21', usp.toString()); // Real browser uses %20 for space\n  }\n</script>\n\n<script id=emptyAndNull>\n{\n  testing.expectEqual(0, new URLSearchParams().size);\n  testing.expectEqual(0, new URLSearchParams('').size);\n  testing.expectEqual(0, new URLSearchParams('?').size);\n  // @ZIGDOM\n  // testing.expectEqual(1, new URLSearchParams(null).size);\n  testing.expectEqual(0, new URLSearchParams(undefined).size);\n\n  const empty = new URLSearchParams();\n  testing.expectEqual(null, empty.get('anything'));\n  testing.expectEqual([], empty.getAll('anything'));\n  testing.expectEqual(false, empty.has('anything'));\n  testing.expectEqual('', empty.toString());\n}\n</script>\n\n<script id=encoding>\n{\n  const usp = new URLSearchParams('key=hello%20world&special=%21%40%23%24&plus=a+b');\n  testing.expectEqual('hello world', usp.get('key'));\n  testing.expectEqual('!@#$', usp.get('special'));\n  testing.expectEqual('a b', usp.get('plus'));\n\n  const usp2 = new URLSearchParams();\n  usp2.append('spaces', 'hello world');\n  usp2.append('special', '!@#$%^&*()');\n  usp2.append('utf8', 'café');\n  testing.expectEqual('spaces=hello+world&special=%21%40%23%24%25%5E%26*%28%29&utf8=caf%C3%83%C2%A9', usp2.toString());\n}\n</script>\n\n<script id=duplicateKeys>\n{\n  const usp = new URLSearchParams('key=1&key=2&key=3');\n  testing.expectEqual('1', usp.get('key'));\n  testing.expectEqual(['1', '2', '3'], usp.getAll('key'));\n  testing.expectEqual(3, usp.size);\n\n  usp.set('key', 'only');\n  testing.expectEqual('only', usp.get('key'));\n  testing.expectEqual(['only'], usp.getAll('key'));\n  testing.expectEqual(1, usp.size);\n}\n</script>\n\n<script id=deleteOperations>\n{\n  const usp = new URLSearchParams('a=1&b=2&a=3&c=4&a=5');\n  testing.expectEqual(5, usp.size);\n\n  usp.delete('a');\n  testing.expectEqual(2, usp.size);\n  testing.expectEqual(false, usp.has('a'));\n  testing.expectEqual('2', usp.get('b'));\n  testing.expectEqual('4', usp.get('c'));\n\n  usp.delete('nonexistent');\n  testing.expectEqual(2, usp.size);\n}\n</script>\n\n<script id=deleteWithValue>\n{\n  const usp = new URLSearchParams('a=1&a=2&a=3&b=1');\n  testing.expectEqual(4, usp.size);\n\n  usp.delete('a', '2');\n  testing.expectEqual(3, usp.size);\n  testing.expectEqual(['1', '3'], usp.getAll('a'));\n  testing.expectEqual('1', usp.get('b'));\n\n  usp.delete('a', '99');\n  testing.expectEqual(3, usp.size);\n}\n</script>\n\n<script id=parseEdgeCases>\n{\n  testing.expectEqual('', new URLSearchParams('key').get('key'));\n  testing.expectEqual('', new URLSearchParams('key=').get('key'));\n  testing.expectEqual('value', new URLSearchParams('=value').get(''));\n  testing.expectEqual('', new URLSearchParams('=').get(''));\n\n  const usp = new URLSearchParams('a=1&&b=2');\n  testing.expectEqual(2, usp.size);\n  testing.expectEqual(null, usp.get(''));\n\n  const trailing = new URLSearchParams('a=1&b=2&');\n  testing.expectEqual(2, trailing.size);\n}\n</script>\n\n<script id=iteration>\n{\n  const usp = new URLSearchParams('a=1&b=2&a=3');\n\n  const keys = Array.from(usp.keys());\n  testing.expectEqual(['a', 'b', 'a'], keys);\n\n  const values = Array.from(usp.values());\n  testing.expectEqual(['1', '2', '3'], values);\n\n  const entries = Array.from(usp.entries());\n  testing.expectEqual([['a', '1'], ['b', '2'], ['a', '3']], entries);\n\n  const defaultIter = Array.from(usp);\n  testing.expectEqual([['a', '1'], ['b', '2'], ['a', '3']], defaultIter);\n}\n</script>\n\n<script id=size>\n{\n  const usp = new URLSearchParams();\n  testing.expectEqual(0, usp.size);\n\n  usp.append('a', '1');\n  testing.expectEqual(1, usp.size);\n\n  usp.append('a', '2');\n  testing.expectEqual(2, usp.size);\n\n  usp.append('b', '3');\n  testing.expectEqual(3, usp.size);\n\n  usp.set('a', 'x');\n  testing.expectEqual(2, usp.size);\n\n  usp.delete('b');\n  testing.expectEqual(1, usp.size);\n\n  usp.delete('a');\n  testing.expectEqual(0, usp.size);\n}\n</script>\n\n<script id=iteratorIsolation>\n{\n  const usp = new URLSearchParams('a=1&b=2&c=3');\n\n  const keys1 = usp.keys();\n  const keys2 = usp.keys();\n  const values1 = usp.values();\n  const entries1 = usp.entries();\n\n  testing.expectEqual('a', keys1.next().value);\n  testing.expectEqual('a', keys2.next().value);\n  testing.expectEqual('1', values1.next().value);\n  testing.expectEqual(['a', '1'], entries1.next().value);\n\n  testing.expectEqual('b', keys1.next().value);\n  testing.expectEqual('b', keys2.next().value);\n  testing.expectEqual('2', values1.next().value);\n  testing.expectEqual(['b', '2'], entries1.next().value);\n\n  testing.expectEqual('c', keys1.next().value);\n  testing.expectEqual('c', keys2.next().value);\n  testing.expectEqual('3', values1.next().value);\n  testing.expectEqual(['c', '3'], entries1.next().value);\n\n  testing.expectEqual(true, keys1.next().done);\n  testing.expectEqual(true, keys2.next().done);\n  testing.expectEqual(true, values1.next().done);\n  testing.expectEqual(true, entries1.next().done);\n}\n</script>\n\n<script id=iteratorLifetime>\n{\n  let keysIter;\n  let valuesIter;\n  let entriesIter;\n\n  {\n    const usp = new URLSearchParams('x=10&y=20&z=30');\n    keysIter = usp.keys();\n    valuesIter = usp.values();\n    entriesIter = usp.entries();\n  }\n\n  testing.expectEqual('x', keysIter.next().value);\n  testing.expectEqual('10', valuesIter.next().value);\n  testing.expectEqual(['x', '10'], entriesIter.next().value);\n\n  testing.expectEqual('y', keysIter.next().value);\n  testing.expectEqual('20', valuesIter.next().value);\n  testing.expectEqual(['y', '20'], entriesIter.next().value);\n\n  testing.expectEqual('z', keysIter.next().value);\n  testing.expectEqual('30', valuesIter.next().value);\n  testing.expectEqual(['z', '30'], entriesIter.next().value);\n\n  testing.expectEqual(true, keysIter.next().done);\n  testing.expectEqual(true, valuesIter.next().done);\n  testing.expectEqual(true, entriesIter.next().done);\n}\n</script>\n\n<script id=forEach>\n{\n  const usp = new URLSearchParams('a=1&b=2&c=3');\n\n  const results = [];\n  usp.forEach((value, key, params) => {\n    results.push({value, key});\n    // Verify third argument is the URLSearchParams instance itself\n    testing.expectEqual(usp, params);\n  });\n\n  testing.expectEqual([\n    {key: 'a', value: '1'},\n    {key: 'b', value: '2'},\n    {key: 'c', value: '3'}\n  ], results);\n}\n</script>\n\n<script id=forEachWithDuplicates>\n{\n  const usp = new URLSearchParams('x=10&x=20&y=30');\n\n  const results = [];\n  usp.forEach((value, key) => {\n    results.push({key, value});\n  });\n\n  testing.expectEqual([\n    {key: 'x', value: '10'},\n    {key: 'x', value: '20'},\n    {key: 'y', value: '30'}\n  ], results);\n}\n</script>\n\n<script id=forEachEmpty>\n{\n  const usp = new URLSearchParams();\n  let called = false;\n\n  usp.forEach(() => {\n    called = true;\n  });\n\n  testing.expectEqual(false, called);\n}\n</script>\n\n<script id=forEachThisArg>\n{\n  const usp = new URLSearchParams('a=1&b=2');\n  const context = {sum: 0};\n\n  usp.forEach(function(value) {\n    this.sum += parseInt(value);\n  }, context);\n\n  testing.expectEqual(3, context.sum);\n}\n</script>\n\n<script id=sort>\n{\n  const usp = new URLSearchParams('z=3&a=1&m=2&a=4');\n  usp.sort();\n\n  testing.expectEqual('a=1&a=4&m=2&z=3', usp.toString());\n  testing.expectEqual(['a', 'a', 'm', 'z'], Array.from(usp.keys()));\n  testing.expectEqual(['1', '4', '2', '3'], Array.from(usp.values()));\n}\n</script>\n\n<script id=sortEmpty>\n{\n  const usp = new URLSearchParams();\n  usp.sort();\n  testing.expectEqual(0, usp.size);\n  testing.expectEqual('', usp.toString());\n}\n</script>\n\n<script id=sortStability>\n{\n  // Test that sort is stable - entries with same key maintain relative order\n  const usp = new URLSearchParams();\n  usp.append('b', '1');\n  usp.append('a', '2');\n  usp.append('b', '3');\n  usp.append('a', '4');\n  usp.append('c', '5');\n\n  usp.sort();\n\n  testing.expectEqual('a=2&a=4&b=1&b=3&c=5', usp.toString());\n  testing.expectEqual(['a', 'a', 'b', 'b', 'c'], Array.from(usp.keys()));\n}\n</script>\n\n<script id=formData>\n  {\n    let fd = new FormData();\n    fd.append('a', '1');\n    fd.append('a', '2');\n    fd.append('b', '3');\n    ups = new URLSearchParams(fd);\n\n    testing.expectEqual(3, ups.size);\n    testing.expectEqual(['1', '2'], ups.getAll('a'));\n    testing.expectEqual(['3'], ups.getAll('b'));\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/net/xhr.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=xhr>\n  testing.expectEqual(0, XMLHttpRequest.UNSENT);\n  testing.expectEqual(1, XMLHttpRequest.OPENED);\n  testing.expectEqual(2, XMLHttpRequest.HEADERS_RECEIVED);\n  testing.expectEqual(3, XMLHttpRequest.LOADING);\n  testing.expectEqual(4, XMLHttpRequest.DONE);\n\n  testing.async(async (restore) => {\n    const req = new XMLHttpRequest();\n    const event = await new Promise((resolve) => {\n      function cbk(event) {\n        resolve(event)\n      }\n\n      req.onload = cbk;\n      testing.expectEqual(cbk, req.onload);\n      req.onload = cbk;\n\n      req.open('GET', 'http://127.0.0.1:9582/xhr');\n      testing.expectEqual(0, req.status);\n      testing.expectEqual('', req.statusText);\n      testing.expectEqual('', req.getAllResponseHeaders());\n      testing.expectEqual(null, req.getResponseHeader('Content-Type'));\n      testing.expectEqual('', req.responseText);\n      testing.expectEqual('', req.responseURL);\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual('load', event.type);\n    testing.expectEqual(true, event.loaded > 0);\n    testing.expectEqual(true, event instanceof ProgressEvent);\n    testing.expectEqual(200, req.status);\n    testing.expectEqual('OK', req.statusText);\n    testing.expectEqual('text/html; charset=utf-8', req.getResponseHeader('Content-Type'));\n    testing.expectEqual('content-length: 100\\r\\nContent-Type: text/html; charset=utf-8\\r\\n', req.getAllResponseHeaders());\n    testing.expectEqual(100, req.responseText.length);\n    testing.expectEqual(req.responseText.length, req.response.length);\n    testing.expectEqual('http://127.0.0.1:9582/xhr', req.responseURL);\n  });\n</script>\n\n<script id=xhr2>\n  const req2 = new XMLHttpRequest()\n  testing.async(async (restore) => {\n    await new Promise((resolve) => {\n      req2.onload = resolve;\n      req2.open('GET', 'http://127.0.0.1:9582/xhr')\n      req2.responseType = 'document';\n      req2.send()\n    });\n\n    restore();\n    testing.expectEqual(200, req2.status);\n    testing.expectEqual('OK', req2.statusText);\n    testing.expectEqual(true, req2.response instanceof Document);\n    testing.expectEqual(true, req2.responseXML instanceof Document);\n  });\n</script>\n\n<script id=xhr3>\n  const req3 = new XMLHttpRequest()\n  testing.async(async (restore) => {\n    await new Promise((resolve) => {\n      req3.onload = resolve;\n      req3.open('GET', 'http://127.0.0.1:9582/xhr/json')\n      req3.responseType = 'json';\n      req3.send()\n    });\n\n    restore();\n    testing.expectEqual(200, req3.status);\n    testing.expectEqual('OK', req3.statusText);\n    testing.expectEqual('9000!!!', req3.response.over);\n    testing.expectEqual(\"number\", typeof json.updated_at);\n    testing.expectEqual(1765867200000, json.updated_at);\n    testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);\n  });\n</script>\n\n<script id=xhr4>\n  const req4 = new XMLHttpRequest()\n  testing.async(async (restore) => {\n    await new Promise((resolve) => {\n      req4.onload = resolve;\n      req4.open('POST', 'http://127.0.0.1:9582/xhr')\n      req4.send('foo')\n    });\n\n    restore();\n    testing.expectEqual(200, req4.status);\n    testing.expectEqual('OK', req4.statusText);\n    testing.expectEqual(true, req4.responseText.length > 64);\n  });\n</script>\n\n<script id=xhr5>\n  testing.async(async (restore) => {\n    let state = [];\n    const req5 = new XMLHttpRequest();\n\n    const result = await new Promise((resolve) => {\n      req5.onreadystatechange = (e) => {\n        state.push(req5.readyState);\n        if (req5.readyState === XMLHttpRequest.DONE) {\n          resolve({states: state, target: e.currentTarget});\n        }\n      }\n\n      req5.open('GET', 'http://127.0.0.1:9582/xhr');\n      req5.send();\n    });\n\n    restore();\n    const {states: states, target: target} = result;\n    testing.expectEqual(4, states.length)\n    testing.expectEqual(XMLHttpRequest.OPENED, states[0]);\n    testing.expectEqual(XMLHttpRequest.HEADERS_RECEIVED, states[1]);\n    testing.expectEqual(XMLHttpRequest.LOADING, states[2]);\n    testing.expectEqual(XMLHttpRequest.DONE, states[3]);\n    testing.expectEqual(req5, target);\n  })\n</script>\n\n<script id=xhr6>\n  const req6 = new XMLHttpRequest()\n  testing.async(async (restore) => {\n    await new Promise((resolve) => {\n      req6.onload = resolve;\n      req6.open('GET', 'http://127.0.0.1:9582/xhr/binary')\n      req6.responseType ='arraybuffer'\n      req6.send()\n    });\n\n    restore();\n    testing.expectEqual(200, req6.status);\n    testing.expectEqual('OK', req6.statusText);\n    testing.expectEqual(7, req6.response.byteLength);\n    testing.expectEqual([0, 0, 1, 2, 0, 0, 9], new Int32Array(req6.response));\n    testing.expectEqual('', typeof req6.response);\n    testing.expectEqual('arraybuffer', req6.responseType);\n  });\n</script>\n\n<script id=xhr_redirect>\n  testing.async(async (restore) => {\n    const req = new XMLHttpRequest();\n    await new Promise((resolve) => {\n      req.onload = resolve;\n      req.open('GET', 'http://127.0.0.1:9582/xhr/redirect');\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual(200, req.status);\n    testing.expectEqual('OK', req.statusText);\n    testing.expectEqual('http://127.0.0.1:9582/xhr', req.responseURL);\n    testing.expectEqual(100, req.responseText.length);\n  });\n</script>\n\n<script id=xhr_404>\n  testing.async(async (restore) => {\n\n    const req = new XMLHttpRequest();\n    await new Promise((resolve) => {\n      req.onload = resolve;\n      req.open('GET', 'http://127.0.0.1:9582/xhr/404');\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual(404, req.status);\n    testing.expectEqual('Not Found', req.statusText);\n    testing.expectEqual('Not Found', req.responseText);\n  });\n</script>\n\n<script id=xhr_500>\n  testing.async(async (restore) => {\n    const req = new XMLHttpRequest();\n    await new Promise((resolve) => {\n      req.onload = resolve;\n      req.open('GET', 'http://127.0.0.1:9582/xhr/500');\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual(500, req.status);\n    testing.expectEqual('Internal Server Error', req.statusText);\n    testing.expectEqual('Internal Server Error', req.responseText);\n  });\n</script>\n\n<script id=xhr_abort>\n  testing.async(async (restore) => {\n    const req = new XMLHttpRequest();\n    let abortFired = false;\n    let errorFired = false;\n    let loadEndFired = false;\n\n    await new Promise((resolve) => {\n      req.onabort = () => { abortFired = true; };\n      req.onerror = () => { errorFired = true; };\n      req.onloadend = () => {\n        loadEndFired = true;\n        resolve();\n      };\n\n      req.open('GET', 'http://127.0.0.1:9582/xhr');\n      req.send();\n      req.abort();\n    });\n\n    restore();\n    testing.expectEqual(true, abortFired);\n    testing.expectEqual(true, errorFired);\n    testing.expectEqual(true, loadEndFired);\n    testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);\n  });\n</script>\n\n<script id=xhr_abort_callback>\n  testing.async(async (restore) => {\n    const req = new XMLHttpRequest();\n    let abortFired = false;\n    let errorFired = false;\n    let loadEndFired = false;\n\n    await new Promise((resolve) => {\n      req.onabort = () => { abortFired = true; };\n      req.onerror = () => { errorFired = true; };\n      req.onloadend = () => {\n        loadEndFired = true;\n        resolve();\n      };\n\n      req.open('GET', 'http://127.0.0.1:9582/xhr');\n      req.onreadystatechange = (e) => {\n        req.abort();\n      }\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual(true, abortFired);\n    testing.expectEqual(true, errorFired);\n    testing.expectEqual(true, loadEndFired);\n    testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);\n  });\n</script>\n\n\n<script id=xhr_abort_callback_nobody>\n  testing.async(async (restore) => {\n    const req = new XMLHttpRequest();\n    let abortFired = false;\n    let errorFired = false;\n    let loadEndFired = false;\n\n    await new Promise((resolve) => {\n      req.onabort = () => { abortFired = true; };\n      req.onerror = () => { errorFired = true; };\n      req.onloadend = () => {\n        loadEndFired = true;\n        resolve();\n      };\n\n      req.open('GET', 'http://127.0.0.1:9582/xhr_empty');\n      req.onreadystatechange = (e) => {\n        req.abort();\n      }\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual(true, abortFired);\n    testing.expectEqual(true, errorFired);\n    testing.expectEqual(true, loadEndFired);\n    testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);\n  });\n</script>\n\n<script id=xhr_blob_url>\n  testing.async(async (restore) => {\n    // Create a blob and get its URL\n    const blob = new Blob(['Hello from blob!'], { type: 'text/plain' });\n    const blobUrl = URL.createObjectURL(blob);\n\n    const req = new XMLHttpRequest();\n    await new Promise((resolve) => {\n      req.onload = resolve;\n      req.open('GET', blobUrl);\n      req.send();\n    });\n\n    restore();\n    testing.expectEqual(200, req.status);\n    testing.expectEqual('Hello from blob!', req.responseText);\n    testing.expectEqual(blobUrl, req.responseURL);\n\n    // Clean up\n    URL.revokeObjectURL(blobUrl);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/adoption.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n<script id=\"adoptNode\">\nconst old = document.implementation.createHTMLDocument(\"\");\nconst div = old.createElement(\"div\");\ndiv.appendChild(old.createTextNode(\"text\"));\n\ntesting.expectEqual(old, div.ownerDocument);\ntesting.expectEqual(old, div.firstChild.ownerDocument);\n\ndocument.body.appendChild(div);\n\ntesting.expectEqual(document, div.ownerDocument);\ntesting.expectEqual(document, div.firstChild.ownerDocument);\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/append_child.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1></div>\n<div id=d2><p id=p1></p></div>\n\n<script id=appendChild>\n  function assertChildren(expected, parent) {\n    const actual = Array.from(parent.childNodes).map((n) => n.id);\n    testing.expectEqual(expected, actual)\n    for (let child of parent.childNodes) {\n      testing.expectEqual(parent, child.parentNode);\n    }\n  }\n  const d1 = $('#d1');\n  const d2 = $('#d2');\n  const p1 = $('#p1');\n\n  assertChildren([], d1);\n  assertChildren(['p1'], d2);\n\n  d1.appendChild(p1);\n\n  assertChildren(['p1'], d1);\n  assertChildren([], d2);\n\n  const p2 = document.createElement('p');\n  p2.id = 'p2';\n  d1.appendChild(p2);\n  assertChildren(['p1', 'p2'], d1);\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/base_uri.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<base href=\"https://example.com/\">\n\n<a href=\"foo\" id=\"foo\">foo</a>\n\n<script id=baseURI>\n  testing.expectEqual(testing.BASE_URL + 'node/base_uri.html', document.URL);\n  testing.expectEqual(\"https://example.com/\", document.baseURI);\n\n  const link = $('#foo');\n  testing.expectEqual(\"https://example.com/foo\", link.href);\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/node/child_nodes.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1><p id=p1></p><p id=p2></p></div>\n<div id=empty></div>\n<div id=one><p id=p10></p></div>\n\n<script id=childNodes>\n  const div = $('#d1');\n  const children = div.childNodes;\n  testing.expectEqual(true, children instanceof NodeList);\n  testing.expectEqual(2, children.length);\n  testing.expectEqual('NodeList', children.constructor.name);\n\n  // we do some weird stuff with caching, so accessing these in different order\n  // needs to be tested\n  testing.expectEqual($('#p1'), children[0]);\n  testing.expectEqual($('#p1'), children[0]);\n  testing.expectEqual($('#p2'), children[1]);\n  testing.expectEqual($('#p1'), children[0]);\n\n  testing.expectEqual(undefined, children[4]);\n  testing.expectEqual(undefined, children[-1]);\n\n  testing.expectEqual(['p1', 'p2'], Array.from(children).map((n) => n.id));\n\n  testing.expectEqual(false, 10 in children);\n</script>\n\n<script id=values>\n  let acc = [];\n  for (let x of children.values()) {\n    acc.push(x.id);\n  }\n  testing.expectEqual(['p1', 'p2'], acc);\n\n  // same as .values()\n  acc = [];\n  for (let x of children) {\n    acc.push(x.id);\n  }\n  testing.expectEqual(['p1', 'p2'], acc);\n</script>\n\n<script id=keys>\n  acc = [];\n  for (let x of children.keys()) {\n    acc.push(x);\n  }\n  testing.expectEqual([0, 1], acc);\n</script>\n\n<script id=entries>\n  acc = [];\n  for (let x of children.entries()) {\n    acc.push([x[0], x[1].id]);\n  }\n  testing.expectEqual([[0, 'p1'], [1, 'p2']], acc);\n</script>\n\n<script id=empty>\n  const empty = $('#empty').childNodes;\n  testing.expectEqual(0, empty.length);\n  testing.expectEqual(undefined, empty[0]);\n  testing.expectEqual([], Array.from(empty.keys()));\n  testing.expectEqual([], Array.from(empty.values()));\n  testing.expectEqual([], Array.from(empty.entries()));\n  testing.expectEqual([], Array.from(empty));\n</script>\n\n<script id=one>\n  const one = $('#one').childNodes;\n  const p10 = $('#p10');\n  testing.expectEqual(1, one.length);\n  testing.expectEqual(p10, one[0]);\n  testing.expectEqual([0], Array.from(one.keys()));\n  testing.expectEqual([p10], Array.from(one.values()));\n  testing.expectEqual([[0, p10]], Array.from(one.entries()));\n\n  testing.expectEqual([p10], Array.from(one));\n  let foreach = [];\n  one.forEach((p) => foreach.push(p));\n  testing.expectEqual([p10], foreach);\n</script>\n\n<script id=contains>\n  testing.expectEqual(true, document.contains(document));\n  testing.expectEqual(true, $('#d1').contains($('#d1')));\n  testing.expectEqual(true, document.contains($('#d1')));\n  testing.expectEqual(true, document.contains($('#p1')));\n  testing.expectEqual(true, document.contains($('#p2')));\n  testing.expectEqual(true, $('#d1').contains($('#p1')));\n  testing.expectEqual(true, $('#d1').contains($('#p2')));\n\n  testing.expectEqual(false, $('#d1').contains($('#empty')));\n  testing.expectEqual(false, $('#d1').contains($('#p10')));\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/clone_node.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=\"test-element\" class=\"test-class\" data-foo=\"bar\">\n  <p>Paragraph 1</p>\n  <p>Paragraph 2</p>\n  <span>Some text</span>\n</div>\n\n<svg id=\"test-svg\" width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">\n  <circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\"/>\n  <text x=\"10\" y=\"20\">SVG Text</text>\n</svg>\n\n<script id=\"cloneTextNode\">\n{\n  const text = document.createTextNode('Hello World');\n  const cloned = text.cloneNode(false);\n\n  testing.expectEqual(3, cloned.nodeType);\n  testing.expectEqual('Hello World', cloned.nodeValue);\n  testing.expectEqual(null, cloned.parentNode);\n  testing.expectEqual(false, text.isSameNode(cloned));\n}\n</script>\n\n<script id=\"cloneCommentNode\">\n{\n  const comment = document.createComment('test comment');\n  const cloned = comment.cloneNode(false);\n\n  testing.expectEqual(8, cloned.nodeType);\n  testing.expectEqual('test comment', cloned.nodeValue);\n  testing.expectEqual(null, cloned.parentNode);\n  testing.expectEqual(false, comment.isSameNode(cloned));\n}\n</script>\n\n<script id=\"cloneElementShallow\">\n{\n  const original = $('#test-element');\n  const cloned = original.cloneNode(false);\n\n  testing.expectEqual(1, cloned.nodeType);\n  testing.expectEqual('DIV', cloned.tagName);\n  testing.expectEqual('test-class', cloned.className);\n  testing.expectEqual('bar', cloned.getAttribute('data-foo'));\n  testing.expectEqual(null, cloned.parentNode);\n  testing.expectEqual(false, cloned.hasChildNodes());\n  testing.expectEqual(false, original.isSameNode(cloned));\n}\n</script>\n\n<script id=\"cloneElementDeep\">\n{\n  const original = $('#test-element');\n  const cloned = original.cloneNode(true);\n\n  testing.expectEqual(1, cloned.nodeType);\n  testing.expectEqual('DIV', cloned.tagName);\n  testing.expectEqual('test-class', cloned.className);\n  testing.expectEqual('bar', cloned.getAttribute('data-foo'));\n  testing.expectEqual(null, cloned.parentNode);\n  testing.expectEqual(true, cloned.hasChildNodes());\n\n  testing.expectEqual(false, original.isSameNode(cloned));\n  testing.expectEqual(false, original.firstChild.isSameNode(cloned.firstChild));\n\n  const p1 = cloned.querySelector('p');\n  testing.expectEqual('Paragraph 1', p1.textContent);\n\n  const span = cloned.querySelector('span');\n  testing.expectEqual('Some text', span.textContent);\n}\n</script>\n\n<script id=\"cloneEmptyElement\">\n{\n  const empty = document.createElement('div');\n  const cloned = empty.cloneNode(true);\n\n  testing.expectEqual('DIV', cloned.tagName);\n  testing.expectEqual(false, cloned.hasChildNodes());\n}\n</script>\n\n<script id=\"cloneWithAttributes\">\n{\n  const el = document.createElement('input');\n  el.setAttribute('type', 'text');\n  el.setAttribute('name', 'username');\n  el.setAttribute('value', 'test');\n\n  const cloned = el.cloneNode(false);\n  testing.expectEqual('text', cloned.getAttribute('type'));\n  testing.expectEqual('username', cloned.getAttribute('name'));\n  testing.expectEqual('test', cloned.getAttribute('value'));\n}\n</script>\n\n<script id=\"cloneNestedDeep\">\n{\n  const outer = document.createElement('div');\n  const middle = document.createElement('span');\n  const inner = document.createTextNode('nested');\n\n  middle.appendChild(inner);\n  outer.appendChild(middle);\n\n  const cloned = outer.cloneNode(true);\n  testing.expectEqual(true, cloned.hasChildNodes());\n  testing.expectEqual('SPAN', cloned.firstChild.tagName);\n  testing.expectEqual(true, cloned.firstChild.hasChildNodes());\n  testing.expectEqual('nested', cloned.firstChild.firstChild.nodeValue);\n}\n</script>\n\n<script id=\"cloneSVGElement\">\n{\n  const svg = $('#test-svg');\n  const cloned = svg.cloneNode(false);\n\n  testing.expectEqual('svg', cloned.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', cloned.namespaceURI);\n  testing.expectEqual('100', cloned.getAttribute('width'));\n  testing.expectEqual('100', cloned.getAttribute('height'));\n  testing.expectEqual(false, cloned.hasChildNodes());\n}\n</script>\n\n<script id=\"cloneSVGDeep\">\n{\n  const svg = $('#test-svg');\n  const cloned = svg.cloneNode(true);\n\n  testing.expectEqual('svg', cloned.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', cloned.namespaceURI);\n  testing.expectEqual(true, cloned.hasChildNodes());\n\n  const circle = cloned.querySelector('circle');\n  testing.expectEqual('circle', circle.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', circle.namespaceURI);\n  testing.expectEqual('50', circle.getAttribute('cx'));\n  testing.expectEqual('40', circle.getAttribute('r'));\n\n  testing.expectEqual(true, cloned.textContent.indexOf('SVG Text') >= 0);\n}\n</script>\n\n<script id=\"cloneSVGElementNS\">\n{\n  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n  circle.setAttribute('cx', '25');\n  circle.setAttribute('cy', '25');\n  circle.setAttribute('r', '20');\n\n  const cloned = circle.cloneNode(false);\n  testing.expectEqual('circle', cloned.tagName);\n  testing.expectEqual('http://www.w3.org/2000/svg', cloned.namespaceURI);\n  testing.expectEqual('25', cloned.getAttribute('cx'));\n  testing.expectEqual('25', cloned.getAttribute('cy'));\n  testing.expectEqual('20', cloned.getAttribute('r'));\n}\n</script>\n\n<script id=\"cloneMixedContent\">\n{\n  const div = document.createElement('div');\n  div.appendChild(document.createTextNode('Before'));\n  div.appendChild(document.createElement('span'));\n  div.appendChild(document.createComment('comment'));\n  div.appendChild(document.createTextNode('After'));\n\n  const cloned = div.cloneNode(true);\n  const children = cloned.childNodes;\n\n  testing.expectEqual(4, children.length);\n  testing.expectEqual(3, children[0].nodeType);\n  testing.expectEqual('Before', children[0].nodeValue);\n  testing.expectEqual(1, children[1].nodeType);\n  testing.expectEqual('SPAN', children[1].tagName);\n  testing.expectEqual(8, children[2].nodeType);\n  testing.expectEqual('comment', children[2].nodeValue);\n  testing.expectEqual(3, children[3].nodeType);\n  testing.expectEqual('After', children[3].nodeValue);\n}\n</script>\n\n<script id=\"cloneMultipleAttributes\">\n{\n  const input = document.createElement('input');\n  input.setAttribute('type', 'text');\n  input.setAttribute('name', 'username');\n  input.setAttribute('value', 'test');\n  input.setAttribute('placeholder', 'Enter username');\n  input.setAttribute('required', '');\n  input.setAttribute('data-custom', 'custom-value');\n\n  const cloned = input.cloneNode(false);\n  testing.expectEqual('text', cloned.getAttribute('type'));\n  testing.expectEqual('username', cloned.getAttribute('name'));\n  testing.expectEqual('test', cloned.getAttribute('value'));\n  testing.expectEqual('Enter username', cloned.getAttribute('placeholder'));\n  testing.expectEqual('', cloned.getAttribute('required'));\n  testing.expectEqual('custom-value', cloned.getAttribute('data-custom'));\n}\n</script>\n\n<script id=\"cloneDetached\">\n{\n  const original = $('#test-element');\n  const cloned = original.cloneNode(true);\n\n  testing.expectEqual(null, cloned.parentNode);\n  testing.expectEqual(false, cloned.isConnected);\n\n  document.body.appendChild(cloned);\n  testing.expectEqual(document.body, cloned.parentNode);\n  testing.expectEqual(true, cloned.isConnected);\n\n  cloned.remove();\n}\n</script>\n\n<script id=\"cloneEmptyTextNode\">\n{\n  const text = document.createTextNode('');\n  const cloned = text.cloneNode(false);\n\n  testing.expectEqual(3, cloned.nodeType);\n  testing.expectEqual('', cloned.nodeValue);\n}\n</script>\n\n<script id=\"cloneDeepPreservesStructure\">\n{\n  const original = document.createElement('ul');\n  const li1 = document.createElement('li');\n  li1.textContent = 'Item 1';\n  const li2 = document.createElement('li');\n  li2.textContent = 'Item 2';\n  const li3 = document.createElement('li');\n  li3.textContent = 'Item 3';\n\n  original.appendChild(li1);\n  original.appendChild(li2);\n  original.appendChild(li3);\n\n  const cloned = original.cloneNode(true);\n  const items = cloned.querySelectorAll('li');\n\n  testing.expectEqual(3, items.length);\n  testing.expectEqual('Item 1', items[0].textContent);\n  testing.expectEqual('Item 2', items[1].textContent);\n  testing.expectEqual('Item 3', items[2].textContent);\n}\n</script>\n\n<script id=\"cloneShallowDoesNotCloneChildren\">\n{\n  const div = document.createElement('div');\n  div.appendChild(document.createElement('p'));\n  div.appendChild(document.createElement('span'));\n  div.appendChild(document.createTextNode('text'));\n\n  const cloned = div.cloneNode(false);\n  testing.expectEqual(false, cloned.hasChildNodes());\n  testing.expectEqual(0, cloned.childNodes.length);\n}\n</script>\n\n<script id=\"cloneDefaultDeepParameter\">\n{\n  const text = document.createTextNode('test');\n  const cloned = text.cloneNode();\n\n  testing.expectEqual('test', cloned.nodeValue);\n  testing.expectEqual(false, text.isSameNode(cloned));\n}\n</script>\n\n<script id=\"svgNamespaceInheritance\">\n{\n  const svg = $('#test-svg');\n  testing.expectEqual('http://www.w3.org/2000/svg', svg.namespaceURI);\n\n  const circle = svg.querySelector('circle');\n  testing.expectEqual('http://www.w3.org/2000/svg', circle.namespaceURI);\n\n  const text = svg.querySelector('text');\n  testing.expectEqual('http://www.w3.org/2000/svg', text.namespaceURI);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/compare_document_position.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"parent\"><p id=\"child1\">Child 1</p><div id=\"child2\"><span id=\"grandchild\">Grandchild</span></div><p id=\"child3\">Child 3</p></div>\n\n<div id=\"unrelated\">Unrelated</div>\n\n<script id=\"sameNode\">\n{\n  const node = $('#parent');\n  testing.expectEqual(0, node.compareDocumentPosition(node));\n}\n</script>\n\n<script id=\"parentChild\">\n{\n  const parent = $('#parent');\n  const child = $('#child1');\n\n  // parent CONTAINS child (0x08) and child is FOLLOWING parent (0x04)\n  const result = parent.compareDocumentPosition(child);\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, result);\n}\n</script>\n\n<script id=\"childParent\">\n{\n  const parent = $('#parent');\n  const child = $('#child1');\n\n  // child is CONTAINED_BY parent (0x10) and parent is PRECEDING child (0x02)\n  const result = child.compareDocumentPosition(parent);\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING, result);\n}\n</script>\n\n<script id=\"siblings\">\n{\n  const child1 = $('#child1');\n  const child2 = $('#child2');\n  const child3 = $('#child3');\n\n  // child1 precedes child2\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, child1.compareDocumentPosition(child2));\n\n  // child2 follows child1\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, child2.compareDocumentPosition(child1));\n\n  // child1 precedes child3\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, child1.compareDocumentPosition(child3));\n\n  // child3 follows child1\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, child3.compareDocumentPosition(child1));\n}\n</script>\n\n<script id=\"ancestorDescendant\">\n{\n  const parent = $('#parent');\n  const grandchild = $('#grandchild');\n\n  // grandchild is contained by parent and parent precedes it\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING, grandchild.compareDocumentPosition(parent));\n\n  // parent contains grandchild and grandchild follows it\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, parent.compareDocumentPosition(grandchild));\n}\n</script>\n\n<script id=\"cousinNodes\">\n{\n  const child1 = $('#child1');\n  const grandchild = $('#grandchild');\n\n  // child1 precedes grandchild (they share parent as common ancestor)\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, child1.compareDocumentPosition(grandchild));\n\n  // grandchild follows child1\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, grandchild.compareDocumentPosition(child1));\n}\n</script>\n\n<script id=\"disconnectedNodes\">\n{\n  const detached = document.createElement('div');\n  const connected = $('#parent');\n\n  // Nodes in different trees are disconnected\n  const result = connected.compareDocumentPosition(detached);\n  testing.expectEqual(true, (result & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0);\n  testing.expectEqual(true, (result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0);\n}\n</script>\n\n<script id=\"twoDetachedNodes\">\n{\n  const detached1 = document.createElement('div');\n  const detached2 = document.createElement('span');\n\n  // Both disconnected\n  const result = detached1.compareDocumentPosition(detached2);\n  testing.expectEqual(true, (result & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0);\n  testing.expectEqual(true, (result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0);\n}\n</script>\n\n<script id=\"textNodes\">\n{\n  const parent = $('#child1');\n  const textNode = parent.firstChild;\n\n  // parent contains text node\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, parent.compareDocumentPosition(textNode));\n\n  // text node is contained by parent\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING, textNode.compareDocumentPosition(parent));\n}\n</script>\n\n<script id=\"documentNode\">\n{\n  const element = $('#parent');\n\n  // document contains element\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, document.compareDocumentPosition(element));\n\n  // element is contained by document\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING, element.compareDocumentPosition(document));\n\n  // document vs document\n  testing.expectEqual(0, document.compareDocumentPosition(document));\n}\n</script>\n\n<script id=\"commentNodes\">\n{\n  const parent = document.createElement('div');\n  const comment = document.createComment('test comment');\n  const text = document.createTextNode('test text');\n  parent.appendChild(comment);\n  parent.appendChild(text);\n\n  // parent contains comment\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, parent.compareDocumentPosition(comment));\n\n  // comment precedes text (siblings)\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, comment.compareDocumentPosition(text));\n\n  // text follows comment\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, text.compareDocumentPosition(comment));\n}\n</script>\n\n<script id=\"manySiblings\">\n{\n  const container = document.createElement('div');\n  const children = [];\n\n  // Create 10 children\n  for (let i = 0; i < 10; i++) {\n    const child = document.createElement('span');\n    child.textContent = 'Child ' + i;\n    container.appendChild(child);\n    children.push(child);\n  }\n\n  // First child precedes last child\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, children[0].compareDocumentPosition(children[9]));\n\n  // Last child follows first child\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, children[9].compareDocumentPosition(children[0]));\n\n  // Middle children\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, children[3].compareDocumentPosition(children[7]));\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, children[7].compareDocumentPosition(children[3]));\n}\n</script>\n\n<script id=\"mixedNodeTypes\">\n{\n  const div = document.createElement('div');\n  const text1 = document.createTextNode('before');\n  const span = document.createElement('span');\n  const comment = document.createComment('middle');\n  const text2 = document.createTextNode('after');\n\n  div.appendChild(text1);\n  div.appendChild(span);\n  div.appendChild(comment);\n  div.appendChild(text2);\n\n  // text1 precedes span\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, text1.compareDocumentPosition(span));\n\n  // span precedes comment\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, span.compareDocumentPosition(comment));\n\n  // comment precedes text2\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, comment.compareDocumentPosition(text2));\n\n  // text1 precedes text2 (non-adjacent siblings)\n  testing.expectEqual(Node.DOCUMENT_POSITION_FOLLOWING, text1.compareDocumentPosition(text2));\n\n  // text2 follows text1\n  testing.expectEqual(Node.DOCUMENT_POSITION_PRECEDING, text2.compareDocumentPosition(text1));\n}\n</script>\n\n<script id=\"deeplyNested\">\n{\n  let current = document.createElement('div');\n  const root = current;\n  const deepElements = [current];\n\n  // Create a 20-level deep structure\n  for (let i = 0; i < 20; i++) {\n    const child = document.createElement('div');\n    current.appendChild(child);\n    deepElements.push(child);\n    current = child;\n  }\n\n  const deepest = deepElements[deepElements.length - 1];\n\n  // Root contains deepest\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, root.compareDocumentPosition(deepest));\n\n  // Deepest is contained by root\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING, deepest.compareDocumentPosition(root));\n\n  // Middle level comparisons\n  const level5 = deepElements[5];\n  const level15 = deepElements[15];\n\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING, level5.compareDocumentPosition(level15));\n  testing.expectEqual(Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING, level15.compareDocumentPosition(level5));\n}\n</script>\n\n<script id=\"detachedTree\">\n{\n  const root1 = document.createElement('div');\n  const child1 = document.createElement('span');\n  root1.appendChild(child1);\n\n  const root2 = document.createElement('div');\n  const child2 = document.createElement('span');\n  root2.appendChild(child2);\n\n  // Children in different detached trees\n  const result = child1.compareDocumentPosition(child2);\n  testing.expectEqual(true, (result & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0);\n  testing.expectEqual(true, (result & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0);\n\n  // Consistency: comparing same pair should give consistent results\n  const result2 = child1.compareDocumentPosition(child2);\n  testing.expectEqual(result, result2);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/insert_before.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1></div>\n<div id=d2></div>\n\n<script id=insertBefore>\n  function assertChildren(expected, parent) {\n    const actual = Array.from(parent.childNodes);\n    testing.expectEqual(expected, actual);\n    for (let child of parent.childNodes) {\n      testing.expectEqual(parent, child.parentNode);\n      testing.expectEqual(child, document.getElementById(child.id));\n    }\n  }\n\n  const d1 = $('#d1');\n  const d2 = $('#d2');\n\n  testing.withError((err) => {\n    testing.expectEqual(8, err.code);\n    testing.expectEqual(\"NotFoundError\", err.name);\n  }, () => d1.insertBefore(document.createElement('div'), d2));\n\n  let c1 = document.createElement('div');\n  c1.id = 'c1';\n  d1.insertBefore(c1, null);\n  assertChildren([c1], d1);\n\n  let c2 = document.createElement('div');\n  c2.id = 'c2';\n  d1.insertBefore(c2, c1);\n  assertChildren([c2, c1], d1);\n\n  d2.insertBefore(c1, null);\n  assertChildren([c2], d1);\n  assertChildren([c1], d2);\n\n  d2.insertBefore(c2, null);\n  assertChildren([], d1);\n  assertChildren([c1, c2], d2);\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/is_connected.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body>\n<div id=\"test-content\">\n  <p id=\"p1\">Connected paragraph</p>\n</div>\n\n<script id=isConnected_elementsInMainDocument>\n{\n  const p1 = document.getElementById('p1');\n  testing.expectEqual(true, p1.isConnected);\n\n  const body = document.body;\n  testing.expectEqual(true, body.isConnected);\n\n  const html = document.documentElement;\n  testing.expectEqual(true, html.isConnected);\n}\n</script>\n\n<script id=isConnected_detachedElement>\n{\n  const div = document.createElement('div');\n  testing.expectEqual(false, div.isConnected);\n\n  // Even if it has content, still not connected\n  div.textContent = 'Hello';\n  testing.expectEqual(false, div.isConnected);\n}\n</script>\n\n<script id=isConnected_afterAppending>\n{\n  const div = document.createElement('div');\n  testing.expectEqual(false, div.isConnected);\n\n  document.body.appendChild(div);\n  testing.expectEqual(true, div.isConnected);\n\n  // Remove it\n  div.remove();\n  testing.expectEqual(false, div.isConnected);\n}\n</script>\n\n<script id=isConnected_nestedDetachedElements>\n{\n  const parent = document.createElement('div');\n  const child = document.createElement('span');\n\n  parent.appendChild(child);\n\n  // Neither should be connected\n  testing.expectEqual(false, parent.isConnected);\n  testing.expectEqual(false, child.isConnected);\n\n  // Add parent to document\n  document.body.appendChild(parent);\n\n  // Now both should be connected\n  testing.expectEqual(true, parent.isConnected);\n  testing.expectEqual(true, child.isConnected);\n}\n</script>\n\n<script id=isConnected_parsedDocument>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"parsed\">Parsed content</div>', 'text/html');\n\n  // Use querySelector instead of getElementById for now\n  const div = doc.querySelector('div');\n  // CRITICAL: Elements in a parsed document should be connected to their own document\n  testing.expectEqual(true, div.isConnected);\n\n  const body = doc.body;\n  testing.expectEqual(true, body.isConnected);\n}\n</script>\n\n<script id=isConnected_documentNode>\n{\n  // The document itself should be connected\n  testing.expectEqual(true, document.isConnected);\n\n  // A parsed document should also be connected (to itself)\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div>test</div>', 'text/html');\n  testing.expectEqual(true, doc.isConnected);\n}\n</script>\n\n<script id=isConnected_removedFromParsedDoc>\n{\n  const parser = new DOMParser();\n  const doc = parser.parseFromString('<div id=\"test\">Content</div>', 'text/html');\n\n  // Use querySelector instead of getElementById for now\n  const div = doc.querySelector('div');\n  testing.expectEqual(true, div.isConnected);\n\n  // Remove it from the parsed document\n  div.remove();\n  testing.expectEqual(false, div.isConnected);\n}\n</script>\n\n</body>\n"
  },
  {
    "path": "src/browser/tests/node/is_equal_node.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div class=\"song\" rick=\"roll\">\n    <span>we're no strangers to love</span>\n    you know the rules\n\n    <b>and so do I</b>\n</div>\n\n<div class=\"song\" rick=\"roll\">\n    <span>we're no strangers to love</span>\n    you know the rules\n\n    <b>and so do I</b>\n</div>\n\n<script id=isEqualNode>\n  // Compare nodes of parsed elements.\n  {\n    const elements = document.getElementsByClassName(\"song\");\n    testing.expectEqual(true, elements.item(0).isEqualNode(elements.item(1)));\n  }\n\n  {\n    const e1 = document.createElement(\"div\");\n    e1.innerHTML = \"<h1>We come from the land of the ice and snow</h1>\";\n    const e2 = document.createElement(\"div\");\n    e2.innerHTML = \"<h1>We come from the land of the ice and snow</h1>\";\n    testing.expectEqual(true, e1.isEqualNode(e2));\n  }\n\n  {\n    const e1 = document.createElement(\"div\");\n    e1.innerHTML = \"<h1>From the midnight sun where the hot springs flow</h1>\";\n    const e2 = document.createElement(\"div\");\n    e2.innerHTML = \"<h1>from the midnight sun where the hot springs flow</h1>\";\n    testing.expectEqual(false, e1.isEqualNode(e2));\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/node.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<p id=\"p1\" class=cl>9000!!</p>\n\n<script>\n  const p1 = $('#p1');\n  const text = p1.firstChild;\n</script>\n\n<script id=isConnected>\n  testing.expectEqual(true, document.isConnected);\n  testing.expectEqual(true, p1.isConnected);\n  testing.expectEqual(true, p1.firstChild.isConnected);\n  testing.expectEqual(false, document.createElement('div').isConnected);\n</script>\n\n<script id=tagName>\n  testing.expectEqual(undefined, document.tagName);\n  testing.expectEqual(undefined, p1.firstChild.tagName);\n  const tags = [\n    [\"a\",\"A\"],\n    [\"address\",\"ADDRESS\"],\n    [\"area\",\"AREA\"],\n    [\"article\",\"ARTICLE\"],\n    [\"aside\",\"ASIDE\"],\n    [\"audio\",\"AUDIO\"],\n    [\"base\",\"BASE\"],\n    [\"bdi\",\"BDI\"],\n    [\"bdo\",\"BDO\"],\n    [\"blockquote\",\"BLOCKQUOTE\"],\n    [\"body\",\"BODY\"],\n    [\"button\",\"BUTTON\"],\n    [\"canvas\",\"CANVAS\"],\n    [\"caption\",\"CAPTION\"],\n    [\"cite\",\"CITE\"],\n    [\"col\",\"COL\"],\n    [\"colgroup\",\"COLGROUP\"],\n    [\"data\",\"DATA\"],\n    [\"datalist\",\"DATALIST\"],\n    [\"dd\",\"DD\"],\n    [\"del\",\"DEL\"],\n    [\"details\",\"DETAILS\"],\n    [\"dfn\",\"DFN\"],\n    [\"dialog\",\"DIALOG\"],\n    [\"div\",\"DIV\"],\n    [\"dl\",\"DL\"],\n    [\"dt\",\"DT\"],\n    [\"em\",\"EM\"],\n    [\"embed\",\"EMBED\"],\n    [\"fieldset\",\"FIELDSET\"],\n    [\"figure\",\"FIGURE\"],\n    [\"footer\",\"FOOTER\"],\n    [\"form\",\"FORM\"],\n    [\"h1\",\"H1\"],\n    [\"h2\",\"H2\"],\n    [\"h3\",\"H3\"],\n    [\"h4\",\"H4\"],\n    [\"h5\",\"H5\"],\n    [\"h6\",\"H6\"],\n    [\"head\",\"HEAD\"],\n    [\"header\",\"HEADER\"],\n    [\"hgroup\",\"HGROUP\"],\n    [\"hr\",\"HR\"],\n    [\"i\",\"I\"],\n    [\"iframe\",\"IFRAME\"],\n    [\"input\",\"INPUT\"],\n    [\"ins\",\"INS\"],\n    [\"keygen\",\"KEYGEN\"],\n    [\"label\",\"LABEL\"],\n    [\"legend\",\"LEGEND\"],\n    [\"li\",\"LI\"],\n    [\"link\",\"LINK\"],\n    [\"main\",\"MAIN\"],\n    [\"map\",\"MAP\"],\n    [\"mark\",\"MARK\"],\n    [\"menu\",\"MENU\"],\n    [\"menuitem\",\"MENUITEM\"],\n    [\"meta\",\"META\"],\n    [\"meter\",\"METER\"],\n    [\"nav\",\"NAV\"],\n    [\"noscript\",\"NOSCRIPT\"],\n    [\"object\",\"OBJECT\"],\n    [\"ol\",\"OL\"],\n    [\"optgroup\",\"OPTGROUP\"],\n    [\"option\",\"OPTION\"],\n    [\"output\",\"OUTPUT\"],\n    [\"param\",\"PARAM\"],\n    [\"pre\",\"PRE\"],\n    [\"progress\",\"PROGRESS\"],\n    [\"q\",\"Q\"],\n    [\"rb\",\"RB\"],\n    [\"rp\",\"RP\"],\n    [\"rt\",\"RT\"],\n    [\"rtc\",\"RTC\"],\n    [\"ruby\",\"RUBY\"],\n    [\"s\",\"S\"],\n    [\"script\",\"SCRIPT\"],\n    [\"section\",\"SECTION\"],\n    [\"select\",\"SELECT\"],\n    [\"small\",\"SMALL\"],\n    [\"source\",\"SOURCE\"],\n    [\"span\",\"SPAN\"],\n    [\"strong\",\"STRONG\"],\n    [\"style\",\"STYLE\"],\n    [\"sub\",\"SUB\"],\n    [\"summary\",\"SUMMARY\"],\n    [\"sup\",\"SUP\"],\n    [\"table\",\"TABLE\"],\n    [\"tbody\",\"TBODY\"],\n    [\"td\",\"TD\"],\n    [\"template\",\"TEMPLATE\"],\n    [\"textarea\",\"TEXTAREA\"],\n    [\"tfoot\",\"TFOOT\"],\n    [\"th\",\"TH\"],\n    [\"thead\",\"THEAD\"],\n    [\"time\",\"TIME\"],\n    [\"tr\",\"TR\"],\n    [\"track\",\"TRACK\"],\n    [\"u\",\"U\"],\n    [\"ul\",\"UL\"],\n    [\"var\",\"VAR\"],\n    [\"video\",\"VIDEO\"]\n  ]\n  for (tag of tags) {\n    testing.expectEqual(tag[1], document.createElement(tag[0]).tagName);\n  }\n</script>\n\n<script id=nodeValue>\n  testing.expectEqual(null, document.nodeValue);\n  document.nodeValue = 'over';\n  testing.expectEqual(null, document.nodeValue);\n\n  testing.expectEqual(null, p1.nodeValue);\n  p1.nodeValue = '9000';\n  testing.expectEqual(null, p1.nodeValue);\n\n  testing.expectEqual('9000!!', text.nodeValue);\n  text.nodeValue = 'what?!';\n  testing.expectEqual('what?!', text.nodeValue);\n\n  testing.expectEqual('cl', p1.getAttributeNode('class').nodeValue);\n  p1.getAttributeNode('class').nodeValue = 'over 9000!!'\n  testing.expectEqual('over 9000!!', p1.getAttributeNode('class').nodeValue);\n</script>\n\n<script id=\"ownerDocument\">\n  testing.expectEqual(null, document.ownerDocument);\n  testing.expectEqual(document, p1.ownerDocument);\n  testing.expectEqual(document, text.ownerDocument);\n  testing.expectEqual(document, document.createElement('div').ownerDocument);\n</script>\n\n<script id=\"hasChildNodes\">\n  testing.expectEqual(true, p1.hasChildNodes());\n  testing.expectEqual(false, text.hasChildNodes());\n  const el = document.createElement('div');\n  testing.expectEqual(false, el.hasChildNodes());\n  el.appendChild(document.createElement('p'));\n  testing.expectEqual(true, el.hasChildNodes());\n</script>\n\n<script id=\"isSameNode\">\n  testing.expectEqual(true, p1.isSameNode(p1));\n  testing.expectEqual(false, p1.isSameNode(text));\n  testing.expectEqual(true, p1.isSameNode($('#p1')));\n  testing.expectEqual(false, p1.isSameNode(document.createElement('p')));\n  testing.expectEqual(false, p1.isSameNode());\n  testing.expectEqual(false, p1.isSameNode(null));\n</script>\n\n<script id=\"nodeType\">\n  testing.expectEqual(9, document.nodeType);\n  testing.expectEqual(1, p1.nodeType);\n  testing.expectEqual(3, text.nodeType);\n  testing.expectEqual(1, document.createElement('div').nodeType);\n  testing.expectEqual(2, p1.getAttributeNode('class').nodeType);\n\n  const comment = document.createComment('test comment');\n  testing.expectEqual(8, comment.nodeType);\n\n  const fragment = document.createDocumentFragment();\n  testing.expectEqual(11, fragment.nodeType);\n\n  testing.expectEqual(9, Node.DOCUMENT_NODE);\n  testing.expectEqual(1, Node.ELEMENT_NODE);\n  testing.expectEqual(3, Node.TEXT_NODE);\n  testing.expectEqual(2, Node.ATTRIBUTE_NODE);\n  testing.expectEqual(8, Node.COMMENT_NODE);\n  testing.expectEqual(11, Node.DOCUMENT_FRAGMENT_NODE);\n</script>\n\n<div id=\"rootNodeComposed\"></div>\n<script id=getRootNodeComposed>\n  const testContainer = $('#rootNodeComposed');\n  const shadowHost = document.createElement('div');\n  testContainer.appendChild(shadowHost);\n  const shadowRoot = shadowHost.attachShadow({ mode: 'open' });\n  const shadowChild = document.createElement('span');\n  shadowRoot.appendChild(shadowChild);\n\n  testing.expectEqual('ShadowRoot', shadowChild.getRootNode().__proto__.constructor.name);\n  testing.expectEqual('ShadowRoot', shadowChild.getRootNode({ composed: false }).__proto__.constructor.name);\n  testing.expectEqual('HTMLDocument', shadowChild.getRootNode({ composed: true }).__proto__.constructor.name);\n  testing.expectEqual('HTMLDocument', shadowHost.getRootNode().__proto__.constructor.name);\n\n  const disconnected = document.createElement('div');\n  const disconnectedChild = document.createElement('span');\n  disconnected.appendChild(disconnectedChild);\n  testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);\n  testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);\n</script>\n\n<div id=contains><div id=other></div></div>\n<script id=contains>\n  {\n    const d1 = $('#contains');\n    const d2 = $('#other');\n    testing.expectEqual(false, d1.contains(null));\n    testing.expectEqual(true, d1.contains(d1));\n    testing.expectEqual(false, d2.contains(d1));\n    testing.expectEqual(true, d1.contains(d2));\n    testing.expectEqual(false, d1.contains(p1));\n  }\n</script>\n\n<script id=childNodes>\n  {\n    const d1 = $('#contains');\n    testing.expectEqual(true, d1.childNodes === d1.childNodes)\n\n    let c1 = d1.childNodes;\n    d1.removeChild(c1[0])\n    testing.expectEqual(0, c1.length);\n    testing.expectEqual(0, d1.childNodes.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/node_iterator.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<body>\n  <div id=\"root\">\n    <div id=\"child1\">\n      <span id=\"grandchild1\">Text 1</span>\n      <span id=\"grandchild2\">Text 2</span>\n    </div>\n    <div id=\"child2\">\n      <span id=\"grandchild3\">Text 3</span>\n    </div>\n    <p id=\"child3\">Paragraph</p>\n  </div>\n</body>\n\n<script id=node_iterator_basic>\n  {\n    const root = document.getElementById(\"root\");\n    const iterator = document.createNodeIterator(root);\n\n    testing.expectEqual(\"NodeIterator\", iterator.constructor.name);\n    testing.expectEqual(root, iterator.root);\n    testing.expectEqual(root, iterator.referenceNode);\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n    testing.expectEqual(0xFFFFFFFF, iterator.whatToShow);\n    testing.expectEqual(null, iterator.filter);\n  }\n</script>\n\n<script id=node_iterator_pointer_state_forward>\n  {\n    const container = document.createElement(\"div\");\n    const a = document.createElement(\"div\");\n    a.id = \"a\";\n    const b = document.createElement(\"div\");\n    b.id = \"b\";\n    container.appendChild(a);\n    container.appendChild(b);\n\n    const iterator = document.createNodeIterator(container, NodeFilter.SHOW_ELEMENT);\n\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n    testing.expectEqual(container, iterator.referenceNode);\n\n    const first = iterator.nextNode();\n    testing.expectEqual(container, first);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n    testing.expectEqual(container, iterator.referenceNode);\n\n    const second = iterator.nextNode();\n    testing.expectEqual(\"a\", second.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n    testing.expectEqual(second, iterator.referenceNode);\n\n    const third = iterator.nextNode();\n    testing.expectEqual(\"b\", third.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n    testing.expectEqual(third, iterator.referenceNode);\n  }\n</script>\n\n<script id=node_iterator_pointer_state_backward>\n  {\n    const container = document.createElement(\"div\");\n    const a = document.createElement(\"div\");\n    a.id = \"a\";\n    const b = document.createElement(\"div\");\n    b.id = \"b\";\n    container.appendChild(a);\n    container.appendChild(b);\n\n    const iterator = document.createNodeIterator(container, NodeFilter.SHOW_ELEMENT);\n\n    iterator.nextNode();\n    iterator.nextNode();\n    iterator.nextNode();\n    testing.expectEqual(\"b\", iterator.referenceNode.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    const prev = iterator.previousNode();\n    testing.expectEqual(\"b\", prev.id);\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n    testing.expectEqual(\"b\", iterator.referenceNode.id);\n  }\n</script>\n\n<script id=node_iterator_pointer_state_zigzag>\n  {\n    const container = document.createElement(\"div\");\n    const a = document.createElement(\"div\");\n    a.id = \"a\";\n    const b = document.createElement(\"div\");\n    b.id = \"b\";\n    const c = document.createElement(\"div\");\n    c.id = \"c\";\n    container.appendChild(a);\n    container.appendChild(b);\n    container.appendChild(c);\n\n    const iterator = document.createNodeIterator(container, NodeFilter.SHOW_ELEMENT);\n\n    iterator.nextNode();\n    iterator.nextNode();\n    testing.expectEqual(\"a\", iterator.referenceNode.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    const back = iterator.previousNode();\n    testing.expectEqual(\"a\", back.id);\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n\n    const forward = iterator.nextNode();\n    testing.expectEqual(\"a\", forward.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    iterator.nextNode();\n    testing.expectEqual(\"b\", iterator.referenceNode.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n  }\n</script>\n\n<script id=node_iterator_skip_vs_reject>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div id=\"skip-me\">\n        <span id=\"nested-span\">Found me!</span>\n      </div>\n      <div id=\"reject-me\">\n        <span id=\"hidden-span\">Can't find me!</span>\n      </div>\n    `;\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id === \"skip-me\") {\n          return NodeFilter.FILTER_SKIP;\n        }\n        if (node.id === \"reject-me\") {\n          return NodeFilter.FILTER_REJECT;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const root = iterator.nextNode();\n    testing.expectEqual(container, root);\n\n    const first = iterator.nextNode();\n    testing.expectEqual(\"nested-span\", first.id);\n\n    const second = iterator.nextNode();\n    testing.expectEqual(\"hidden-span\", second.id);\n  }\n</script>\n\n<script id=node_iterator_nested_comments_in_skipped>\n  {\n    const container = document.createElement(\"div\");\n    const skipped = document.createElement(\"div\");\n    skipped.id = \"skipped\";\n    const comment = document.createComment(\"Important comment\");\n    skipped.appendChild(comment);\n    container.appendChild(skipped);\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_COMMENT,\n      function(node) {\n        if (node.nodeType === 1 && node.id === \"skipped\") {\n          return NodeFilter.FILTER_SKIP;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const found = iterator.nextNode();\n    testing.expectEqual(8, found.nodeType);\n    testing.expectEqual(\"Important comment\", found.textContent);\n  }\n</script>\n\n<script id=node_iterator_nested_in_rejected>\n  {\n    const container = document.createElement(\"div\");\n    const rejected = document.createElement(\"div\");\n    rejected.id = \"rejected\";\n    rejected.appendChild(document.createTextNode(\"Hidden text\"));\n    container.appendChild(rejected);\n    container.appendChild(document.createTextNode(\"Visible text\"));\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_TEXT,\n      function(node) {\n        if (node.nodeType === 1 && node.id === \"rejected\") {\n          return NodeFilter.FILTER_REJECT;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const text1 = iterator.nextNode();\n    testing.expectEqual(\"Hidden text\", text1.textContent);\n\n    const text2 = iterator.nextNode();\n    testing.expectEqual(\"Visible text\", text2.textContent);\n  }\n</script>\n\n<script id=node_iterator_deep_nesting>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div id=\"level1-skip\">\n        <div id=\"level2-skip\">\n          <div id=\"level3-accept\">\n            <span id=\"level4-accept\">Deep span</span>\n          </div>\n        </div>\n      </div>\n    `;\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id && node.id.includes(\"accept\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    const first = iterator.nextNode();\n    testing.expectEqual(\"level3-accept\", first.id);\n\n    const second = iterator.nextNode();\n    testing.expectEqual(\"level4-accept\", second.id);\n\n    const none = iterator.nextNode();\n    testing.expectEqual(null, none);\n  }\n</script>\n\n<script id=node_iterator_previousNode_with_filter>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div class=\"accept\">A</div>\n      <div class=\"skip\">\n        <div class=\"accept\">B</div>\n      </div>\n      <div class=\"accept\">C</div>\n    `;\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.classList.contains(\"accept\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    iterator.nextNode();\n    iterator.nextNode();\n    iterator.nextNode();\n    iterator.nextNode();\n\n    testing.expectEqual(\"C\", iterator.previousNode().textContent);\n    testing.expectEqual(\"B\", iterator.previousNode().textContent);\n    testing.expectEqual(\"A\", iterator.previousNode().textContent);\n  }\n</script>\n\n<script id=node_iterator_pointer_consistency_with_skipped>\n  {\n    const container = document.createElement(\"div\");\n    const a = document.createElement(\"div\");\n    a.id = \"accept\";\n    const b = document.createElement(\"div\");\n    b.id = \"skip\";\n    const c = document.createElement(\"div\");\n    c.id = \"accept2\";\n    container.appendChild(a);\n    container.appendChild(b);\n    container.appendChild(c);\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id && node.id.includes(\"accept\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    const first = iterator.nextNode();\n    testing.expectEqual(\"accept\", first.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    const second = iterator.nextNode();\n    testing.expectEqual(\"accept2\", second.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    const back = iterator.previousNode();\n    testing.expectEqual(\"accept2\", back.id);\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n  }\n</script>\n\n<script id=node_iterator_function_filter>\n  {\n    const root = document.getElementById(\"root\");\n\n    const iterator = document.createNodeIterator(\n      root,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id && node.id.startsWith(\"child\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    testing.expectEqual(\"child1\", iterator.nextNode().id);\n    testing.expectEqual(\"child2\", iterator.nextNode().id);\n    testing.expectEqual(\"child3\", iterator.nextNode().id);\n    testing.expectEqual(null, iterator.nextNode());\n  }\n</script>\n\n<script id=node_iterator_object_filter>\n  {\n    const root = document.getElementById(\"root\");\n\n    const filter = {\n      acceptNode: function(node) {\n        if (node.tagName === \"SPAN\") {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    };\n\n    const iterator = document.createNodeIterator(\n      root,\n      NodeFilter.SHOW_ELEMENT,\n      filter\n    );\n\n    testing.expectEqual(\"grandchild1\", iterator.nextNode().id);\n    testing.expectEqual(\"grandchild2\", iterator.nextNode().id);\n    testing.expectEqual(\"grandchild3\", iterator.nextNode().id);\n    testing.expectEqual(null, iterator.nextNode());\n  }\n</script>\n\n<script id=node_iterator_text_nodes>\n  {\n    const container = document.createElement(\"div\");\n    container.appendChild(document.createTextNode(\"text1\"));\n    container.appendChild(document.createTextNode(\"text2\"));\n    container.appendChild(document.createTextNode(\"text3\"));\n\n    const iterator = document.createNodeIterator(container, NodeFilter.SHOW_TEXT);\n\n    const text1 = iterator.nextNode();\n    testing.expectEqual(\"#text\", text1.nodeName);\n    testing.expectEqual(\"text1\", text1.textContent);\n\n    const text2 = iterator.nextNode();\n    testing.expectEqual(\"text2\", text2.textContent);\n\n    const text3 = iterator.nextNode();\n    testing.expectEqual(\"text3\", text3.textContent);\n\n    testing.expectEqual(null, iterator.nextNode());\n  }\n</script>\n\n<script id=node_iterator_all_node_types>\n  {\n    const container = document.createElement(\"div\");\n    container.appendChild(document.createTextNode(\"text\"));\n    container.appendChild(document.createComment(\"comment\"));\n    const span = document.createElement(\"span\");\n    span.textContent = \"element\";\n    container.appendChild(span);\n\n    const allIterator = document.createNodeIterator(container, NodeFilter.SHOW_ALL);\n    testing.expectEqual(1, allIterator.nextNode().nodeType);\n    testing.expectEqual(3, allIterator.nextNode().nodeType);\n    testing.expectEqual(8, allIterator.nextNode().nodeType);\n    testing.expectEqual(1, allIterator.nextNode().nodeType);\n    testing.expectEqual(3, allIterator.nextNode().nodeType);\n  }\n</script>\n\n<script id=node_iterator_boundary_at_root>\n  {\n    const root = document.getElementById(\"root\");\n    const iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);\n\n    testing.expectEqual(null, iterator.previousNode());\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n  }\n</script>\n\n<script id=node_iterator_pointer_after_reaching_end>\n  {\n    const container = document.createElement(\"div\");\n    const a = document.createElement(\"div\");\n    a.id = \"a\";\n    container.appendChild(a);\n\n    const iterator = document.createNodeIterator(container, NodeFilter.SHOW_ELEMENT);\n\n    iterator.nextNode();\n    iterator.nextNode();\n    testing.expectEqual(null, iterator.nextNode());\n\n    testing.expectEqual(\"a\", iterator.referenceNode.id);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n  }\n</script>\n\n<script id=node_iterator_complex_filter_pointer_tracking>\n  {\n    const container = document.createElement(\"div\");\n    for (let i = 0; i < 5; i++) {\n      const div = document.createElement(\"div\");\n      div.className = i % 2 === 0 ? \"even\" : \"odd\";\n      div.textContent = String(i);\n      container.appendChild(div);\n    }\n\n    const iterator = document.createNodeIterator(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.classList.contains(\"even\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    testing.expectEqual(\"0\", iterator.nextNode().textContent);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    testing.expectEqual(\"2\", iterator.nextNode().textContent);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    testing.expectEqual(\"2\", iterator.previousNode().textContent);\n    testing.expectEqual(true, iterator.pointerBeforeReferenceNode);\n\n    testing.expectEqual(\"2\", iterator.nextNode().textContent);\n    testing.expectEqual(false, iterator.pointerBeforeReferenceNode);\n\n    testing.expectEqual(\"4\", iterator.nextNode().textContent);\n    testing.expectEqual(null, iterator.nextNode());\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/normalize.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=container></div>\n\n<script id=normalize>\n  const container = $('#container');\n\n  // Test merging adjacent text nodes\n  container.innerHTML = 'a' + 'b';\n  container.normalize();\n  testing.expectEqual('ab', container.innerHTML);\n  testing.expectEqual(1, container.childNodes.length);\n\n  // Test removing empty text nodes\n  container.innerHTML = 'a' + '' + 'b';\n  container.normalize();\n  testing.expectEqual('ab', container.innerHTML);\n  testing.expectEqual(1, container.childNodes.length);\n\n  // Test recursive normalization\n  container.innerHTML = '<div>a' + '' + 'b</div>c';\n  container.normalize();\n  testing.expectEqual('<div>ab</div>c', container.innerHTML);\n  testing.expectEqual(2, container.childNodes.length);\n  testing.expectEqual(1, container.firstChild.childNodes.length);\n\n  container.innerHTML = 'a<p></p>b';\n  container.normalize();\n  testing.expectEqual('a<p></p>b', container.innerHTML);\n  testing.expectEqual(3, container.childNodes.length);\n</script>\n\n<span id=token class=\"token\" style=\"color:#ce9178\">&quot;puppeteer &quot;</span>\n                                        <h3 id=name>Leto\n                                        <!-- -->\n                                        <!-- -->\n                                        Atreides</h3>\n<script id=adjascent_test_nodes>\n  const token = $('#token');\n  testing.expectEqual('\"puppeteer \"', token.firstChild.nodeValue);\n\n  const name = $('#name');\n  testing.expectEqual([\n    \"Leto\\n                                        \",\n    \" \",\n    \"\\n                                        \",\n    \" \",\n    \"\\n                                        Atreides\"\n  ], Array.from(name.childNodes).map((n) => n.nodeValue));\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/noscript_serialization.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<!-- When scripting is enabled the HTML parser treats <noscript> as a raw text\n     element: its entire content is stored as a single text node containing the\n     raw markup.  Serializing it back (outerHTML / innerHTML) must output that\n     markup as-is without HTML-escaping the angle brackets. -->\n<noscript id=\"ns1\"><h1>Hello</h1><p>World</p></noscript>\n<noscript id=\"ns2\"><div id=\"bsky_post_summary\"><h3>Post</h3><p id=\"bsky_display_name\">Henri Helvetica</p></div></noscript>\n\n<script id=\"noscript-outerHTML\">\n  const ns1 = document.getElementById('ns1');\n  testing.expectEqual('<noscript id=\"ns1\"><h1>Hello</h1><p>World</p></noscript>', ns1.outerHTML);\n</script>\n\n<script id=\"noscript-innerHTML\">\n  const ns2 = document.getElementById('ns2');\n  testing.expectEqual('<div id=\"bsky_post_summary\"><h3>Post</h3><p id=\"bsky_display_name\">Henri Helvetica</p></div>', ns2.innerHTML);\n</script>\n\n<script id=\"noscript-textContent\">\n  // The raw text node content must be the literal HTML markup (not parsed DOM)\n  testing.expectEqual('<h1>Hello</h1><p>World</p>', ns1.firstChild.nodeValue);\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/owner.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"target-container\">\n  <p id=\"reference-node\">\n  I am the original reference node.\n  </p>\n</div>\n\n\n<script id=owner>\n  const parser = new DOMParser();\n  const newDoc = parser.parseFromString('<div id=\"new-node\"><p>Hey</p><span>Marked</span></div>', 'text/html');\n  const newNode = newDoc.getElementById('new-node');\n  const parent = $('#target-container');\n  const referenceNode = $('#reference-node');\n\n  parent.insertBefore(newNode, referenceNode);\n  const k = $('#new-node');\n  const ptag = k.querySelector('p');\n  const spanTag = k.querySelector('span');\n  const anotherDoc = parser.parseFromString('<div id=\"another-new-node\"></div>', 'text/html');\n  const anotherNewNode = anotherDoc.getElementById('another-new-node');\n  testing.expectEqual('[object HTMLDivElement]', parent.appendChild(anotherNewNode).toString());\n\n\n  testing.expectEqual(newNode.ownerDocument, parent.ownerDocument);\n  testing.expectEqual(anotherNewNode.ownerDocument, parent.ownerDocument);\n  testing.expectEqual('P', newNode.firstChild.nodeName);\n  testing.expectEqual(parent.ownerDocument, ptag.ownerDocument);\n  testing.expectEqual(parent.ownerDocument, spanTag.ownerDocument);\n  testing.expectEqual(true, parent.contains(newNode));\n  testing.expectEqual(true, parent.contains(anotherNewNode));\n  testing.expectEqual(false, anotherDoc.contains(anotherNewNode));\n  testing.expectEqual(false, newDoc.contains(newNode));\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/remove_child.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1></div>\n<div id=d2><p id=p1></p></div>\n\n<script id=removeChild>\n  testing.withError((err) => {\n    testing.expectEqual(8, err.code);\n    testing.expectEqual(\"NotFoundError\", err.name);\n  }, () => $('#d1').removeChild($('#p1')));\n\n  const p1 = $('#p1');\n  const node = $('#d2').removeChild(p1);\n  testing.expectEqual(node, p1);\n  testing.expectEqual(null, $('#p1'));\n  testing.expectEqual(null, p1.parentNode);\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/replace_child.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=d1><div id=c1></div><div id=c2></div></div>\n<div id=d2><div id=c3></div></div>\n\n<script id=replaceChild>\n  function assertChildren(expected, parent) {\n    const actual = Array.from(parent.childNodes);\n    testing.expectEqual(expected, actual);\n    for (let child of parent.childNodes) {\n      testing.expectEqual(parent, child.parentNode);\n      testing.expectEqual(child, document.getElementById(child.id));\n    }\n  }\n\n  const d1 = $('#d1');\n  const d2 = $('#d2');\n  const c1 = $('#c1');\n  const c2 = $('#c2');\n  const c3 = $('#c3');\n\n  let c4 = document.createElement('div');\n  c4.id = 'c4';\n\n  testing.withError((err) => {\n    testing.expectEqual(3, err.code);\n    testing.expectEqual(\"HierarchyRequestError\", err.name);\n  }, () => d1.replaceChild(c4, c3));\n\n  testing.expectEqual(c2, d1.replaceChild(c4, c2));\n  testing.expectEqual(null, c2.parentNode);\n  assertChildren([c1, c4], d1)\n  assertChildren([c3], d2)\n\n  testing.expectEqual(c1, d1.replaceChild(c3, c1));\n  testing.expectEqual(null, c2.parentNode);\n  assertChildren([c3, c4], d1)\n  assertChildren([], d2)\n\n  testing.expectEqual(c3, d1.replaceChild(c3, c3));\n  assertChildren([c3, c4], d1)\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/text_content.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=id1>d1 <p>hello</p></div>\n<div id=id2>\n  <style>h1 { font-size: 1em; }</style>\n  <!-- this is a comment -->\n  This is a <br>\n  text\n</div>\n\n<script id=element>\n  const div = $('#id1');\n  testing.expectEqual('d1 hello', div.textContent);\n\n  div.textContent = 'world <p>!</p>';\n  testing.expectEqual('world <p>!</p>', div.textContent);\n\n  const div2 = $('#id2');\n  testing.expectEqual(\"\\n  h1 { font-size: 1em; }\\n  \\n  This is a \\n  text\\n\", div2.textContent);\n</script>\n\n<script id=document>\n  testing.expectEqual(null, document.textContent);\n  document.textContent = 'over 9000';\n  testing.expectEqual(null, document.textContent);\n</script>\n\n<script id=cdata>\n  div.innerHTML = 'd1 <p>hello</p>';\n  testing.expectEqual('d1 ', div.firstChild.textContent);\n  div.firstChild.textContent = 'dd2d';\n  testing.expectEqual('dd2d', div.firstChild.textContent);\n  testing.expectEqual('dd2dhello', div.textContent);\n</script>\n\n<script id=attribute>\n  const attr = div.getAttributeNode('id');\n  testing.expectEqual('id1', attr.value);\n  testing.expectEqual('id1', attr.textContent);\n  attr.textContent = 'attr2';\n  testing.expectEqual('attr2', attr.value);\n  testing.expectEqual('attr2', attr.textContent);\n  testing.expectEqual(div, $('#attr2'));\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/tree.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<div id=parent>\n  <p id=c1>1</p>\n  <p id=c2>2</p>\n  <p id=c3>3</p>\n</div>\n<script id=tree>\n  const parent = $('#parent');\n  const c1 = $('#c1');\n  const c2 = $('#c2');\n  const c3 = $('#c3');\n\n  // remember text nodes get inserted between elements if there's whitespace\n  testing.expectEqual('#text', parent.firstChild.nodeName)\n  testing.expectEqual(c1, parent.firstChild.nextSibling)\n\n  testing.expectEqual('1', c1.firstChild.textContent);\n  testing.expectEqual(c2, c1.nextSibling.nextSibling);\n  testing.expectEqual('#text', c1.previousSibling.nodeName)\n  testing.expectEqual(null, c1.previousSibling.previousSibling);\n\n  testing.expectEqual(parent, c1.parentNode);\n  testing.expectEqual(parent, c1.parentElement);\n</script>\n"
  },
  {
    "path": "src/browser/tests/node/tree_walker.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<body>\n  <div id=\"root\">\n    <div id=\"child1\">\n      <span id=\"grandchild1\">Text 1</span>\n      <span id=\"grandchild2\">Text 2</span>\n    </div>\n    <div id=\"child2\">\n      <span id=\"grandchild3\">Text 3</span>\n    </div>\n    <p id=\"child3\">Paragraph</p>\n  </div>\n</body>\n\n<script id=tree_walker_basic>\n  {\n    const root = document.getElementById(\"root\");\n    const walker = document.createTreeWalker(root);\n\n    testing.expectEqual(\"TreeWalker\", walker.constructor.name);\n    testing.expectEqual(root, walker.root);\n    testing.expectEqual(root, walker.currentNode);\n    testing.expectEqual(0xFFFFFFFF, walker.whatToShow);\n    testing.expectEqual(null, walker.filter);\n  }\n</script>\n\n<script id=tree_walker_element_filter>\n  {\n    const root = document.getElementById(\"root\");\n    const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n\n    testing.expectEqual(NodeFilter.SHOW_ELEMENT, walker.whatToShow);\n    testing.expectEqual(root, walker.currentNode);\n\n    const child1 = walker.firstChild();\n    testing.expectEqual(document.getElementById(\"child1\"), child1);\n    testing.expectEqual(child1, walker.currentNode);\n\n    const child2 = walker.nextSibling();\n    testing.expectEqual(document.getElementById(\"child2\"), child2);\n\n    const parent = walker.parentNode();\n    testing.expectEqual(root, parent);\n  }\n</script>\n\n<script id=tree_walker_text_filter>\n  {\n    const container = document.createElement(\"div\");\n    container.appendChild(document.createTextNode(\"text1\"));\n    container.appendChild(document.createTextNode(\"text2\"));\n    container.appendChild(document.createTextNode(\"text3\"));\n\n    const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);\n\n    const text1 = walker.nextNode();\n    testing.expectEqual(\"#text\", text1.nodeName);\n    testing.expectEqual(\"text1\", text1.textContent);\n\n    const text2 = walker.nextNode();\n    testing.expectEqual(\"text2\", text2.textContent);\n\n    const text3 = walker.nextNode();\n    testing.expectEqual(\"text3\", text3.textContent);\n\n    testing.expectEqual(null, walker.nextNode());\n  }\n</script>\n\n<script id=tree_walker_skip_vs_reject>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div id=\"skip-me\">\n        <span id=\"nested-span\">Found me!</span>\n      </div>\n      <div id=\"reject-me\">\n        <span id=\"hidden-span\">Can't find me!</span>\n      </div>\n    `;\n\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id === \"skip-me\") {\n          return NodeFilter.FILTER_SKIP;\n        }\n        if (node.id === \"reject-me\") {\n          return NodeFilter.FILTER_REJECT;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const first = walker.nextNode();\n    testing.expectEqual(\"nested-span\", first.id);\n\n    const second = walker.nextNode();\n    testing.expectEqual(null, second);\n  }\n</script>\n\n<script id=tree_walker_nested_comments_in_skipped>\n  {\n    const container = document.createElement(\"div\");\n    const skipped = document.createElement(\"div\");\n    skipped.id = \"skipped\";\n    const comment = document.createComment(\"Important comment\");\n    skipped.appendChild(comment);\n    container.appendChild(skipped);\n\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_COMMENT,\n      function(node) {\n        if (node.nodeType === 1 && node.id === \"skipped\") {\n          return NodeFilter.FILTER_SKIP;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const found = walker.nextNode();\n    testing.expectEqual(8, found.nodeType);\n    testing.expectEqual(\"Important comment\", found.data);\n  }\n</script>\n\n<script id=tree_walker_nested_text_in_rejected>\n  {\n    const container = document.createElement(\"div\");\n    const rejected = document.createElement(\"div\");\n    rejected.id = \"rejected\";\n    rejected.appendChild(document.createTextNode(\"Hidden text\"));\n    container.appendChild(rejected);\n    container.appendChild(document.createTextNode(\"Visible text\"));\n\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_TEXT,\n      function(node) {\n        if (node.nodeType === 1 && node.id === \"rejected\") {\n          return NodeFilter.FILTER_REJECT;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const hidden = walker.nextNode();\n    testing.expectEqual(\"Hidden text\", hidden.textContent);\n\n    const found = walker.nextNode();\n    testing.expectEqual(\"Visible text\", found.textContent);\n\n    const none = walker.nextNode();\n    testing.expectEqual(null, none);\n  }\n</script>\n\n<script id=tree_walker_deep_nesting_with_mixed_filters>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div id=\"level1-skip\">\n        <div id=\"level2-accept\">\n          <div id=\"level3-skip\">\n            <span id=\"level4-accept\">Deep span</span>\n          </div>\n        </div>\n      </div>\n    `;\n\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id && node.id.includes(\"skip\")) {\n          return NodeFilter.FILTER_SKIP;\n        }\n        return NodeFilter.FILTER_ACCEPT;\n      }\n    );\n\n    const first = walker.nextNode();\n    testing.expectEqual(\"level2-accept\", first.id);\n\n    const second = walker.nextNode();\n    testing.expectEqual(\"level4-accept\", second.id);\n\n    const none = walker.nextNode();\n    testing.expectEqual(null, none);\n  }\n</script>\n\n<script id=tree_walker_function_filter>\n  {\n    const root = document.getElementById(\"root\");\n\n    const walker = document.createTreeWalker(\n      root,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.id && node.id.startsWith(\"child\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    const child1 = walker.firstChild();\n    testing.expectEqual(\"child1\", child1.id);\n\n    const child2 = walker.nextSibling();\n    testing.expectEqual(\"child2\", child2.id);\n\n    const child3 = walker.nextSibling();\n    testing.expectEqual(\"child3\", child3.id);\n\n    const none = walker.nextSibling();\n    testing.expectEqual(null, none);\n  }\n</script>\n\n<script id=tree_walker_object_filter>\n  {\n    const root = document.getElementById(\"root\");\n\n    const filter = {\n      acceptNode: function(node) {\n        if (node.tagName === \"SPAN\") {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    };\n\n    const walker = document.createTreeWalker(\n      root,\n      NodeFilter.SHOW_ELEMENT,\n      filter\n    );\n\n    const span1 = walker.nextNode();\n    testing.expectEqual(\"grandchild1\", span1.id);\n\n    const span2 = walker.nextNode();\n    testing.expectEqual(\"grandchild2\", span2.id);\n\n    const span3 = walker.nextNode();\n    testing.expectEqual(\"grandchild3\", span3.id);\n\n    const none = walker.nextNode();\n    testing.expectEqual(null, none);\n  }\n</script>\n\n<script id=tree_walker_navigation>\n  {\n    const root = document.getElementById(\"root\");\n    const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n\n    testing.expectEqual(\"child1\", walker.firstChild().id);\n    testing.expectEqual(\"grandchild2\", walker.lastChild().id);\n    testing.expectEqual(\"grandchild1\", walker.previousSibling().id);\n    testing.expectEqual(\"grandchild2\", walker.nextSibling().id);\n    testing.expectEqual(\"child1\", walker.parentNode().id);\n    testing.expectEqual(\"grandchild1\", walker.nextNode().id);\n    testing.expectEqual(\"child1\", walker.previousNode().id);\n  }\n</script>\n\n<script id=tree_walker_current_node_setter>\n  {\n    const root = document.getElementById(\"root\");\n    const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n\n    const child2 = document.getElementById(\"child2\");\n    walker.currentNode = child2;\n    testing.expectEqual(child2, walker.currentNode);\n\n    const grandchild3 = walker.firstChild();\n    testing.expectEqual(\"grandchild3\", grandchild3.id);\n  }\n</script>\n\n<script id=tree_walker_previousNode_complex>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div id=\"a\">\n        <div id=\"b\">\n          <div id=\"c\"></div>\n        </div>\n        <div id=\"d\"></div>\n      </div>\n      <div id=\"e\"></div>\n    `;\n\n    const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);\n\n    walker.currentNode = container.querySelector(\"#e\");\n    testing.expectEqual(\"d\", walker.previousNode().id);\n    testing.expectEqual(\"c\", walker.previousNode().id);\n    testing.expectEqual(\"b\", walker.previousNode().id);\n    testing.expectEqual(\"a\", walker.previousNode().id);\n    testing.expectEqual(container, walker.previousNode());\n    testing.expectEqual(null, walker.previousNode());\n  }\n</script>\n\n<script id=tree_walker_nextNode_with_filter>\n  {\n    const container = document.createElement(\"div\");\n    container.innerHTML = `\n      <div class=\"accept\">A</div>\n      <div class=\"skip\">\n        <div class=\"accept\">B</div>\n      </div>\n      <div class=\"reject\">\n        <div class=\"accept\">C</div>\n      </div>\n      <div class=\"accept\">D</div>\n    `;\n\n    const walker = document.createTreeWalker(\n      container,\n      NodeFilter.SHOW_ELEMENT,\n      function(node) {\n        if (node.classList.contains(\"accept\")) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        if (node.classList.contains(\"reject\")) {\n          return NodeFilter.FILTER_REJECT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      }\n    );\n\n    testing.expectEqual(\"A\", walker.nextNode().textContent);\n    testing.expectEqual(\"B\", walker.nextNode().textContent);\n    testing.expectEqual(\"D\", walker.nextNode().textContent);\n    testing.expectEqual(null, walker.nextNode());\n  }\n</script>\n\n<script id=tree_walker_all_node_types>\n  {\n    const container = document.createElement(\"div\");\n    container.appendChild(document.createTextNode(\"text\"));\n    container.appendChild(document.createComment(\"comment\"));\n    const span = document.createElement(\"span\");\n    span.textContent = \"element\";\n    container.appendChild(span);\n\n    const textWalker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);\n    testing.expectEqual(\"text\", textWalker.nextNode().textContent);\n    testing.expectEqual(\"element\", textWalker.nextNode().textContent);\n\n    const commentWalker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);\n    const comment = commentWalker.nextNode();\n    testing.expectEqual(8, comment.nodeType);\n    testing.expectEqual(\"comment\", comment.data);\n\n    const elementWalker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);\n    testing.expectEqual(\"SPAN\", elementWalker.nextNode().tagName);\n  }\n</script>\n\n<script id=tree_walker_boundary_conditions>\n  {\n    const root = document.getElementById(\"root\");\n    const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n\n    testing.expectEqual(null, walker.parentNode());\n\n    walker.currentNode = document.getElementById(\"child3\");\n    testing.expectEqual(null, walker.nextSibling());\n    testing.expectEqual(null, walker.firstChild());\n    testing.expectEqual(null, walker.lastChild());\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/page/blob.html",
    "content": "<!DOCTYPE html>\n<body></body>\n<script src=\"../testing.js\"></script>\n\n<script id=\"basic_blob_navigation\">\n  {\n    const html = '<html><head></head><body><div id=\"test\">Hello Blob</div></body></html>';\n    const blob = new Blob([html], { type: 'text/html' });\n    const blob_url = URL.createObjectURL(blob);\n\n    const iframe = document.createElement('iframe');\n    document.body.appendChild(iframe);\n    iframe.src = blob_url;\n\n    testing.eventually(() => {\n      testing.expectEqual('Hello Blob', iframe.contentDocument.getElementById('test').textContent);\n    });\n  }\n</script>\n\n<script id=\"multiple_blobs\">\n  {\n    const blob1 = new Blob(['<html><body>First</body></html>'], { type: 'text/html' });\n    const blob2 = new Blob(['<html><body>Second</body></html>'], { type: 'text/html' });\n    const url1 = URL.createObjectURL(blob1);\n    const url2 = URL.createObjectURL(blob2);\n\n    const iframe1 = document.createElement('iframe');\n    document.body.appendChild(iframe1);\n    iframe1.src = url1;\n\n    const iframe2 = document.createElement('iframe');\n    document.body.appendChild(iframe2);\n    iframe2.src = url2;\n\n    testing.eventually(() => {\n      testing.expectEqual('First', iframe1.contentDocument.body.textContent);\n      testing.expectEqual('Second', iframe2.contentDocument.body.textContent);\n    });\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/page/load_event.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=DOMContentLoaded>\n  let call1 = false;\n  let ex1 = null;\n  document.addEventListener('DOMContentLoaded', function(e) {\n    ex1 = e; // let's just capture this to make sure our lifetimes are working\n    testing.expectEqual(document, e.target);\n    call1 = true;\n  });\n\n  testing.eventually(() => {\n    testing.expectEqual(document, ex1.target);\n    testing.expectEqual('DOMContentLoaded', ex1.type);\n    testing.expectEqual(true, call1);\n  });\n  testing.expectEqual(true, true);\n</script>\n"
  },
  {
    "path": "src/browser/tests/page/meta.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=meta>\n  testing.expectEqual('HTMLDocument', document.constructor.name);\n  testing.expectEqual('Document', new Document().constructor.name);\n  testing.expectEqual('[object Document]', new Document().toString());\n  testing.expectEqual('Window', window.constructor.name);\n\n  // Important test. new Document() returns a Node, but getElementById\n  // exists on the Document. So this is a simple way to make sure that\n  // the returned Zig type is associated with the correct JS class.\n  testing.expectEqual(null, new Document().getElementById('x'));\n\n  // HTMLDocument (global document) should have HTML-specific properties\n  testing.expectEqual('object', typeof document.head);\n  testing.expectEqual('object', typeof document.body);\n  testing.expectEqual('string', typeof document.title);\n  testing.expectEqual('object', typeof document.images);\n  testing.expectEqual('object', typeof document.scripts);\n  testing.expectEqual('object', typeof document.links);\n  testing.expectEqual('object', typeof document.forms);\n  testing.expectEqual('object', typeof document.location);\n\n  // Plain Document should NOT have HTML-specific properties\n  const plainDoc = new Document();\n  testing.expectEqual('undefined', typeof plainDoc.head);\n  testing.expectEqual('undefined', typeof plainDoc.body);\n  testing.expectEqual('undefined', typeof plainDoc.title);\n  testing.expectEqual('undefined', typeof plainDoc.images);\n  testing.expectEqual('undefined', typeof plainDoc.scripts);\n  testing.expectEqual('undefined', typeof plainDoc.links);\n  testing.expectEqual('undefined', typeof plainDoc.forms);\n  testing.expectEqual('undefined', typeof plainDoc.location);\n\n  // Both should have common Document properties\n  testing.expectEqual('string', typeof document.URL);\n  testing.expectEqual('string', typeof plainDoc.URL);\n  testing.expectEqual('string', typeof document.readyState);\n  testing.expectEqual('string', typeof plainDoc.readyState);\n\n  testing.expectEqual(\"[object Intl.DateTimeFormat]\", new Intl.DateTimeFormat().toString());\n</script>\n"
  },
  {
    "path": "src/browser/tests/page/mod1.js",
    "content": "const val1 = 'value-1';\nexport { val1 as \"val1\" }\n"
  },
  {
    "path": "src/browser/tests/page/module.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=meta type=module>\n  testing.expectEqual('/src/browser/tests/page/module.html', new URL(import.meta.url).pathname)\n</script>\n\n<script id=basic-import type=module>\n  import { \"val1\" as val1 } from \"./mod1.js\";\n  testing.expectEqual('value-1', val1);\n</script>\n\n<script id=relative-imports type=module>\n  import { importedValue, localValue } from \"./modules/importer.js\";\n  testing.expectEqual('from-base', importedValue);\n  testing.expectEqual('local', localValue);\n</script>\n\n<script id=re-exports type=module>\n  import { baseValue, importedValue, localValue } from \"./modules/re-exporter.js\";\n  testing.expectEqual('from-base', baseValue);\n  testing.expectEqual('from-base', importedValue);\n  testing.expectEqual('local', localValue);\n</script>\n\n<script id=shared-module-1 type=module>\n  import { increment, getCount } from \"./modules/shared.js\";\n  testing.expectEqual(1, increment());\n  testing.expectEqual(1, getCount());\n</script>\n\n<script id=shared-module-2 type=module>\n  import { increment, getCount } from \"./modules/shared.js\";\n  testing.expectEqual(2, increment());\n  testing.expectEqual(2, getCount());\n</script>\n\n<script id=circular-imports type=module>\n  import { aValue, getFromB } from \"./modules/circular-a.js\";\n  import { bValue, getFromA } from \"./modules/circular-b.js\";\n  testing.expectEqual('a', aValue);\n  testing.expectEqual('b', bValue);\n  testing.expectEqual('b', getFromB());\n  testing.expectEqual('a', getFromA());\n</script>\n\n<script id=basic-async type=module>\n  import { \"val1\" as val1 } from \"./mod1.js\";\n  const m = await import(\"./mod1.js\");\n  testing.expectEqual('value-1', m.val1, {script_id: 'basic-async'});\n</script>\n\n<script id=import-404 type=module>\n  try {\n    await import(\"./modules/nonexistent.js\");\n    testing.expectFail(\"error expected\");\n  } catch(e) {\n    testing.expectEqual(true, e.toString().includes(\"FailedToLoad\"), {script_id: 'import-404'});\n  }\n</script>\n\n<!-- this used to crash -->\n<script type=module src=modules/self_async.js></script>\n"
  },
  {
    "path": "src/browser/tests/page/modules/base.js",
    "content": "export const baseValue = 'from-base';\n"
  },
  {
    "path": "src/browser/tests/page/modules/circular-a.js",
    "content": "import { getBValue } from './circular-b.js';\n\nexport const aValue = 'a';\n\nexport function getFromB() {\n  return getBValue();\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/circular-b.js",
    "content": "import { aValue } from './circular-a.js';\n\nexport const bValue = 'b';\n\nexport function getBValue() {\n  return bValue;\n}\n\nexport function getFromA() {\n  return aValue;\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/dynamic-chain-a.js",
    "content": "export async function loadChain() {\n  const b = await import('./dynamic-chain-b.js');\n  return b.loadNext();\n}\n\nexport const chainValue = 'chain-a';\n"
  },
  {
    "path": "src/browser/tests/page/modules/dynamic-chain-b.js",
    "content": "export async function loadNext() {\n  const c = await import('./dynamic-chain-c.js');\n  return c.finalValue;\n}\n\nexport const chainValue = 'chain-b';\n"
  },
  {
    "path": "src/browser/tests/page/modules/dynamic-chain-c.js",
    "content": "export const finalValue = 'chain-end';\n"
  },
  {
    "path": "src/browser/tests/page/modules/dynamic-circular-x.js",
    "content": "export const xValue = 'dynamic-x';\n\nexport async function loadY() {\n  const y = await import('./dynamic-circular-y.js');\n  return y.yValue;\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/dynamic-circular-y.js",
    "content": "export const yValue = 'dynamic-y';\n\nexport async function loadX() {\n  const x = await import('./dynamic-circular-x.js');\n  return x.xValue;\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/importer.js",
    "content": "import { baseValue } from './base.js';\n\nexport const importedValue = baseValue;\nexport const localValue = 'local';\n"
  },
  {
    "path": "src/browser/tests/page/modules/mixed-circular-dynamic.js",
    "content": "import { staticValue } from './mixed-circular-static.js';\n\nexport const dynamicValue = 'dynamic-side';\n\nexport function getStaticValue() {\n  return staticValue;\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/mixed-circular-static.js",
    "content": "export const staticValue = 'static-side';\n\nexport async function loadDynamicSide() {\n  const dynamic = await import('./mixed-circular-dynamic.js');\n  return dynamic.dynamicValue;\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/re-exporter.js",
    "content": "export { baseValue } from './base.js';\nexport { importedValue, localValue } from './importer.js';\n"
  },
  {
    "path": "src/browser/tests/page/modules/self_async.js",
    "content": "const c = await import('./self_async.js');\n"
  },
  {
    "path": "src/browser/tests/page/modules/shared.js",
    "content": "let counter = 0;\n\nexport function increment() {\n  return ++counter;\n}\n\nexport function getCount() {\n  return counter;\n}\n"
  },
  {
    "path": "src/browser/tests/page/modules/syntax-error.js",
    "content": "export const value = 'test'\nthis is a syntax error!\n"
  },
  {
    "path": "src/browser/tests/page/modules/test-404.js",
    "content": "import { something } from './nonexistent.js';\nexport { something };\n"
  },
  {
    "path": "src/browser/tests/page/modules/test-syntax-error.js",
    "content": "import { value } from './syntax-error.js';\nexport { value };\n"
  },
  {
    "path": "src/browser/tests/performance.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=performance>\n  testing.expectEqual(performance, window.performance);\n</script>\n\n<script id=now_returns_number>\n  const t = performance.now();\n  testing.expectEqual('number', typeof t);\n  testing.expectEqual(true, t >= 0);\n</script>\n\n<script id=now_increases>\n  const t1 = performance.now();\n  const t2 = performance.now();\n  testing.expectEqual(true, t2 >= t1);\n</script>\n\n<script id=timeOrigin>\n  const origin = performance.timeOrigin;\n  testing.expectEqual('number', typeof origin);\n  testing.expectEqual(true, origin > 0);\n</script>\n\n<script id=now_relative_to_origin>\n  {\n    const t = performance.now();\n    const now = Date.now();\n    testing.expectEqual(true, t < now);\n  }\n</script>\n\n<script id=multiple_calls>\n  {\n    const times = [];\n    for (let i = 0; i < 5; i++) {\n      times.push(performance.now());\n    }\n\n    for (let i = 1; i < times.length; i++) {\n      testing.expectEqual(true, times[i] >= times[i-1]);\n    }\n  }\n</script>\n\n<script id=mark>\n  {\n    let mark1 = performance.mark(\"start\");\n    testing.expectEqual(true, mark1 instanceof PerformanceMark);\n    testing.expectEqual('start', mark1.name);\n    testing.expectEqual('mark', mark1.entryType);\n    testing.expectEqual(0, mark1.duration);\n    testing.expectEqual(null, mark1.detail);\n\n    let mark2 = performance.mark(\"start\", {startTime: 32939393.9});\n    testing.expectEqual(32939393.9, mark2.startTime);\n  }\n</script>\n\n<script id=getEntries>\n  {\n    // Clear any existing marks first\n    performance.clearMarks();\n\n    const entries1 = performance.getEntries();\n    const initialCount = entries1.length;\n\n    performance.mark(\"test1\");\n    performance.mark(\"test2\");\n    performance.mark(\"test3\");\n\n    const entries2 = performance.getEntries();\n    testing.expectEqual(initialCount + 3, entries2.length);\n  }\n</script>\n\n<script id=getEntriesByType>\n  {\n    performance.clearMarks();\n\n    performance.mark(\"mark1\");\n    performance.mark(\"mark2\");\n\n    const marks = performance.getEntriesByType(\"mark\");\n    testing.expectEqual(2, marks.length);\n    testing.expectEqual('mark1', marks[0].name);\n    testing.expectEqual('mark2', marks[1].name);\n  }\n</script>\n\n<script id=getEntriesByName>\n  {\n    performance.clearMarks();\n\n    performance.mark(\"myMark\");\n    performance.mark(\"otherMark\");\n    performance.mark(\"myMark\");\n\n    const entries = performance.getEntriesByName(\"myMark\");\n    testing.expectEqual(2, entries.length);\n    testing.expectEqual('myMark', entries[0].name);\n    testing.expectEqual('myMark', entries[1].name);\n\n    const byType = performance.getEntriesByName(\"myMark\", \"mark\");\n    testing.expectEqual(2, byType.length);\n  }\n</script>\n\n<script id=clearMarks>\n  {\n    performance.clearMarks();\n\n    performance.mark(\"mark1\");\n    performance.mark(\"mark2\");\n    performance.mark(\"mark3\");\n\n    let marks = performance.getEntriesByType(\"mark\");\n    testing.expectEqual(3, marks.length);\n\n    // Clear specific mark\n    performance.clearMarks(\"mark2\");\n    marks = performance.getEntriesByType(\"mark\");\n    testing.expectEqual(2, marks.length);\n    testing.expectEqual('mark1', marks[0].name);\n    testing.expectEqual('mark3', marks[1].name);\n\n    // Clear all marks\n    performance.clearMarks();\n    marks = performance.getEntriesByType(\"mark\");\n    testing.expectEqual(0, marks.length);\n  }\n</script>\n\n<script id=measure_basic>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    let measure1 = performance.measure(\"basic-measure\");\n    testing.expectEqual(true, measure1 instanceof PerformanceMeasure);\n    testing.expectEqual('basic-measure', measure1.name);\n    testing.expectEqual('measure', measure1.entryType);\n    testing.expectEqual(null, measure1.detail);\n  }\n</script>\n\n<script id=measure_between_marks>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    performance.mark(\"start\");\n    performance.mark(\"end\");\n\n    let measure = performance.measure(\"duration\", {\n      start: \"start\",\n      end: \"end\"\n    });\n\n    testing.expectEqual('duration', measure.name);\n    testing.expectEqual('measure', measure.entryType);\n    testing.expectEqual(true, measure.duration >= 0);\n    testing.expectEqual(true, measure.startTime >= 0);\n  }\n</script>\n\n<script id=measure_no_arguments>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    let measure = performance.measure(\"from-start\");\n    testing.expectEqual('from-start', measure.name);\n    testing.expectEqual(0, measure.startTime);\n    testing.expectEqual(true, measure.duration > 0);\n  }\n</script>\n\n<script id=measure_with_duration>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    let measure = performance.measure(\"fixed-duration\", {\n      duration: 100.5\n    });\n\n    testing.expectEqual('fixed-duration', measure.name);\n    testing.expectEqual(100.5, measure.duration);\n    testing.expectEqual(0, measure.startTime);\n  }\n</script>\n\n<script id=measure_with_detail>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    let detail = { foo: \"bar\", count: 42 };\n    let measure = performance.measure(\"with-detail\", {\n      detail: detail\n    });\n\n    testing.expectEqual('with-detail', measure.name);\n    testing.expectEqual(true, measure.detail !== null);\n    testing.expectEqual('bar', measure.detail.foo);\n    testing.expectEqual(42, measure.detail.count);\n  }\n</script>\n\n<script id=getEntriesByType_measure>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    performance.measure(\"measure1\");\n    performance.measure(\"measure2\");\n    performance.measure(\"measure3\");\n\n    const measures = performance.getEntriesByType(\"measure\");\n    testing.expectEqual(3, measures.length);\n    testing.expectEqual('measure1', measures[0].name);\n    testing.expectEqual('measure2', measures[1].name);\n    testing.expectEqual('measure3', measures[2].name);\n  }\n</script>\n\n<script id=clearMeasures>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    performance.measure(\"measure1\");\n    performance.measure(\"measure2\");\n    performance.measure(\"measure3\");\n\n    let measures = performance.getEntriesByType(\"measure\");\n    testing.expectEqual(3, measures.length);\n\n    // Clear specific measure\n    performance.clearMeasures(\"measure2\");\n    measures = performance.getEntriesByType(\"measure\");\n    testing.expectEqual(2, measures.length);\n    testing.expectEqual('measure1', measures[0].name);\n    testing.expectEqual('measure3', measures[1].name);\n\n    // Clear all measures\n    performance.clearMeasures();\n    measures = performance.getEntriesByType(\"measure\");\n    testing.expectEqual(0, measures.length);\n  }\n</script>\n\n<script id=measure_with_navigation_timing_marks>\n  {\n    // performance.measure() must accept PerformanceTiming attribute names\n    // (e.g. \"fetchStart\") as start/end marks per the W3C User Timing Level 2 spec.\n    // https://www.w3.org/TR/user-timing/#dom-performance-measure\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    performance.mark(\"mark-dataComplete\");\n    let m = performance.measure(\"dataComplete\", \"fetchStart\", \"mark-dataComplete\");\n    testing.expectEqual('dataComplete', m.name);\n    testing.expectEqual('measure', m.entryType);\n    testing.expectEqual(true, m.duration >= 0);\n\n    // navigationStart is also a valid mark name (was already supported)\n    performance.mark(\"mark-end\");\n    let m2 = performance.measure(\"fromNav\", \"navigationStart\", \"mark-end\");\n    testing.expectEqual('fromNav', m2.name);\n    testing.expectEqual(true, m2.duration >= 0);\n  }\n</script>\n\n<script id=performance_timing_exists>\n  {\n    // performance.timing must not be undefined (used by sites like Bing)\n    testing.expectEqual(true, performance.timing !== undefined);\n    testing.expectEqual(true, performance.timing !== null);\n  }\n</script>\n\n<script id=performance_timing_navigationStart>\n  {\n    // navigationStart must be a number (sites access performance.timing.navigationStart)\n    const timing = performance.timing;\n    testing.expectEqual('number', typeof timing.navigationStart);\n    testing.expectEqual(0, timing.navigationStart);\n  }\n</script>\n\n<script id=performance_timing_all_properties>\n  {\n    // All PerformanceTiming properties must be accessible and return numbers\n    const timing = performance.timing;\n    const props = [\n      'navigationStart', 'unloadEventStart', 'unloadEventEnd',\n      'redirectStart', 'redirectEnd', 'fetchStart',\n      'domainLookupStart', 'domainLookupEnd',\n      'connectStart', 'connectEnd', 'secureConnectionStart',\n      'requestStart', 'responseStart', 'responseEnd',\n      'domLoading', 'domInteractive',\n      'domContentLoadedEventStart', 'domContentLoadedEventEnd',\n      'domComplete', 'loadEventStart', 'loadEventEnd',\n    ];\n    for (const prop of props) {\n      testing.expectEqual('number', typeof timing[prop]);\n    }\n  }\n</script>\n\n<script id=performance_timing_same_object>\n  {\n    // performance.timing should return the same object on each access\n    testing.expectEqual(true, performance.timing === performance.timing);\n  }\n</script>\n\n<script id=mixed_marks_and_measures>\n  {\n    performance.clearMarks();\n    performance.clearMeasures();\n\n    performance.mark(\"mark1\");\n    performance.measure(\"measure1\");\n    performance.mark(\"mark2\");\n    performance.measure(\"measure2\");\n\n    const allEntries = performance.getEntries();\n    testing.expectEqual(true, allEntries.length >= 4);\n\n    const marks = performance.getEntriesByType(\"mark\");\n    testing.expectEqual(2, marks.length);\n\n    const measures = performance.getEntriesByType(\"measure\");\n    testing.expectEqual(2, measures.length);\n\n    // Clearing marks shouldn't affect measures\n    performance.clearMarks();\n    const remainingMeasures = performance.getEntriesByType(\"measure\");\n    testing.expectEqual(2, remainingMeasures.length);\n\n    const remainingMarks = performance.getEntriesByType(\"mark\");\n    testing.expectEqual(0, remainingMarks.length);\n  }\n</script>\n\n<script id=performance_timing_exists>\n  {\n    // Navigation Timing Level 1: performance.timing must be an object, not undefined\n    testing.expectEqual('object', typeof performance.timing);\n    testing.expectEqual(false, performance.timing === null);\n  }\n</script>\n\n<script id=performance_timing_navigationStart>\n  {\n    // The most commonly used property — must be a number (not undefined)\n    testing.expectEqual('number', typeof performance.timing.navigationStart);\n    testing.expectEqual(0, performance.timing.navigationStart);\n  }\n</script>\n\n<script id=performance_navigation_exists>\n  {\n    // Navigation Timing Level 1: performance.navigation must be an object, not undefined\n    testing.expectEqual('object', typeof performance.navigation);\n    testing.expectEqual(false, performance.navigation === null);\n    testing.expectEqual('number', typeof performance.navigation.type);\n    testing.expectEqual('number', typeof performance.navigation.redirectCount);\n    testing.expectEqual(0, performance.navigation.type);\n    testing.expectEqual(0, performance.navigation.redirectCount);\n  }\n</script>\n\n<script id=performance_timing_all_properties>\n  {\n    const t = performance.timing;\n    const props = [\n      'navigationStart', 'unloadEventStart', 'unloadEventEnd',\n      'redirectStart', 'redirectEnd', 'fetchStart',\n      'domainLookupStart', 'domainLookupEnd',\n      'connectStart', 'connectEnd', 'secureConnectionStart',\n      'requestStart', 'responseStart', 'responseEnd',\n      'domLoading', 'domInteractive',\n      'domContentLoadedEventStart', 'domContentLoadedEventEnd',\n      'domComplete', 'loadEventStart', 'loadEventEnd',\n    ];\n    for (const prop of props) {\n      testing.expectEqual('number', typeof t[prop]);\n    }\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/performance_observer/performance_observer.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=\"observe_performance_mark\">\n{\n  const observer = new PerformanceObserver((list, observer) => {\n    testing.expectEqual(true, list instanceof PerformanceObserverEntryList);\n    testing.expectEqual(true, observer instanceof PerformanceObserver);\n\n    const entries = list.getEntries();\n    testing.expectEqual(true, entries instanceof Array);\n    testing.expectEqual(2, entries.length);\n\n    {\n      const { name, startTime, duration, entryType } = entries[0];\n      testing.expectEqual(\"operationStart\", name);\n      testing.expectEqual(20, startTime);\n      testing.expectEqual(0, duration);\n      testing.expectEqual(\"mark\", entryType);\n    }\n\n    {\n      const { name, startTime, duration, entryType } = entries[1];\n      testing.expectEqual(\"operationEnd\", name);\n      testing.expectEqual(34.0, startTime);\n      testing.expectEqual(0, duration);\n      testing.expectEqual(\"mark\", entryType);\n    }\n\n    observer.disconnect();\n  });\n\n  // Look for performance marks.\n  observer.observe({ type: \"mark\" });\n  performance.mark(\"operationStart\", { startTime: 20.0 });\n  performance.mark(\"operationEnd\", { startTime: 34.0 });\n}\n</script>\n\n<script id=\"microtask_access_to_list\">\n{\n\n  let savedList;\n  const promise = new Promise((resolve) => {\n    const observer = new PerformanceObserver((list, observer) => {\n      savedList = list;\n      resolve();\n      observer.disconnect();\n    });\n    observer.observe({ type: \"mark\" });\n    performance.mark(\"testMark\");\n  });\n\n  testing.async(async () => {\n    await promise;\n    // force a call_depth reset, which will clear the call_arena\n    document.getElementsByTagName('*');\n\n    const entries = savedList.getEntries();\n    testing.expectEqual(true, entries instanceof Array, {script_id: 'microtask_access_to_list'});\n    testing.expectEqual(1, entries.length);\n    testing.expectEqual(\"testMark\", entries[0].name);\n    testing.expectEqual(\"mark\", entries[0].entryType);\n  });\n}\n</script>\n\n<script>\n  testing.expectEqual(['mark', 'measure'], PerformanceObserver.supportedEntryTypes);\n</script>\n\n<script id=\"buffered_option\">\n{\n  // Clear marks from previous tests so we get a precise count\n  performance.clearMarks();\n\n  // Create marks BEFORE the observer\n  performance.mark(\"early1\", { startTime: 1.0 });\n  performance.mark(\"early2\", { startTime: 2.0 });\n\n  let receivedEntries = null;\n  const observer = new PerformanceObserver((list) => {\n    receivedEntries = list.getEntries();\n  });\n\n  // With buffered: true, existing marks should be delivered\n  observer.observe({ type: \"mark\", buffered: true });\n\n  testing.eventually(() => {\n    testing.expectEqual(true, receivedEntries !== null);\n    testing.expectEqual(2, receivedEntries.length);\n    testing.expectEqual(\"early1\", receivedEntries[0].name);\n    testing.expectEqual(\"early2\", receivedEntries[1].name);\n\n    observer.disconnect();\n  });\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/polyfill/webcomponents.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=main></div>\n\n<script id=webcomponents>\n  class LightPanda extends HTMLElement {\n    constructor() {\n      super();\n    }\n    connectedCallback() {\n      this.append('connected');\n    }\n  }\n\n  window.customElements.define(\"lightpanda-test\", LightPanda);\n  const main = document.getElementById('main');\n  main.appendChild(document.createElement('lightpanda-test'));\n\n  testing.expectEqual('<lightpanda-test>connected</lightpanda-test>', main.innerHTML)\n  testing.expectEqual('[object DataSet]', document.createElement('lightpanda-test').dataset.toString());\n  testing.expectEqual('[object DataSet]', document.createElement('lightpanda-test.x').dataset.toString());\n</script>\n"
  },
  {
    "path": "src/browser/tests/processing_instruction.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=basic>\n{\n  const pi = document.createProcessingInstruction('xml-stylesheet', 'href=\"style.css\"');\n  testing.expectEqual('object', typeof pi);\n  testing.expectEqual(true, pi instanceof ProcessingInstruction);\n  testing.expectEqual(true, pi instanceof Node);\n}\n</script>\n\n<script id=properties>\n{\n  const pi = document.createProcessingInstruction('foo', 'bar');\n  testing.expectEqual('foo', pi.target);\n  testing.expectEqual('bar', pi.data);\n  testing.expectEqual(3, pi.length);\n  testing.expectEqual(7, pi.nodeType);\n  testing.expectEqual('foo', pi.nodeName);\n  testing.expectEqual('bar', pi.nodeValue);\n  testing.expectEqual('bar', pi.textContent);\n}\n</script>\n\n<script id=empty_data>\n{\n  const pi = document.createProcessingInstruction('target', '');\n  testing.expectEqual('target', pi.target);\n  testing.expectEqual('', pi.data);\n  testing.expectEqual(0, pi.length);\n}\n</script>\n\n<script id=set_data>\n{\n  const pi = document.createProcessingInstruction('foo', 'bar');\n  pi.data = 'baz';\n  testing.expectEqual('baz', pi.data);\n  testing.expectEqual('foo', pi.target); // target shouldn't change\n  testing.expectEqual(3, pi.length);\n}\n</script>\n\n<script id=set_textContent>\n{\n  const pi = document.createProcessingInstruction('target', 'original');\n  pi.textContent = 'modified';\n  testing.expectEqual('modified', pi.data);\n  testing.expectEqual('modified', pi.textContent);\n}\n</script>\n\n<script id=set_nodeValue>\n{\n  const pi = document.createProcessingInstruction('target', 'original');\n  pi.nodeValue = 'changed';\n  testing.expectEqual('changed', pi.data);\n  testing.expectEqual('changed', pi.nodeValue);\n}\n</script>\n\n<script id=cloneNode>\n{\n  const pi = document.createProcessingInstruction('xml-stylesheet', 'href=\"style.css\"');\n  const clone = pi.cloneNode();\n  testing.expectEqual('xml-stylesheet', clone.target);\n  testing.expectEqual('href=\"style.css\"', clone.data);\n  testing.expectEqual(7, clone.nodeType);\n  testing.expectEqual(true, clone instanceof ProcessingInstruction);\n\n  // Clone should be a different object\n  testing.expectEqual(false, pi === clone);\n\n  // Modifying clone shouldn't affect original\n  clone.data = 'different';\n  testing.expectEqual('href=\"style.css\"', pi.data);\n  testing.expectEqual('different', clone.data);\n}\n</script>\n\n<script id=isEqualNode>\n{\n  const pi1 = document.createProcessingInstruction('target1', 'data1');\n  const pi2 = document.createProcessingInstruction('target2', 'data2');\n  const pi3 = document.createProcessingInstruction('target1', 'data1');\n  const pi4 = document.createProcessingInstruction('target1', 'data2');\n\n  testing.expectEqual(true, pi1.isEqualNode(pi1));\n  testing.expectEqual(true, pi1.isEqualNode(pi3));\n  testing.expectEqual(false, pi1.isEqualNode(pi2));\n  testing.expectEqual(false, pi2.isEqualNode(pi3));\n  testing.expectEqual(false, pi1.isEqualNode(pi4)); // different data\n  testing.expectEqual(false, pi1.isEqualNode(document));\n  testing.expectEqual(false, document.isEqualNode(pi1));\n}\n</script>\n\n<script id=invalid_target_question_mark_gt>\n{\n  try {\n    document.createProcessingInstruction('tar?>get', 'data');\n    testing.fail('Should throw InvalidCharacterError for \"?>\" in target');\n  } catch (e) {\n    testing.expectEqual('InvalidCharacterError', e.name);\n  }\n}\n</script>\n\n<script id=invalid_target_empty>\n{\n  try {\n    document.createProcessingInstruction('', 'data');\n    testing.fail('Should throw InvalidCharacterError for empty target');\n  } catch (e) {\n    testing.expectEqual('InvalidCharacterError', e.name);\n  }\n}\n</script>\n\n<script id=invalid_target_starts_with_number>\n{\n  try {\n    document.createProcessingInstruction('0target', 'data');\n    testing.fail('Should throw InvalidCharacterError for target starting with number');\n  } catch (e) {\n    testing.expectEqual('InvalidCharacterError', e.name);\n  }\n}\n</script>\n\n<script id=valid_target_with_colon>\n{\n  // xml:foo should be valid\n  const pi = document.createProcessingInstruction('xml:stylesheet', 'data');\n  testing.expectEqual('xml:stylesheet', pi.target);\n}\n</script>\n\n<script id=characterData_methods>\n{\n  const pi = document.createProcessingInstruction('target', 'abcdef');\n\n  // substringData\n  testing.expectEqual('bcd', pi.substringData(1, 3));\n  testing.expectEqual('def', pi.substringData(3, 10)); // should clamp to end\n\n  // appendData\n  pi.appendData('ghi');\n  testing.expectEqual('abcdefghi', pi.data);\n\n  // insertData\n  pi.insertData(3, 'XXX');\n  testing.expectEqual('abcXXXdefghi', pi.data);\n\n  // deleteData\n  pi.deleteData(3, 3);\n  testing.expectEqual('abcdefghi', pi.data);\n\n  // replaceData\n  pi.replaceData(3, 3, 'YYY');\n  testing.expectEqual('abcYYYghi', pi.data);\n}\n</script>\n\n<script id=owner_document>\n{\n  const pi = document.createProcessingInstruction('target', 'data');\n  testing.expectEqual(document, pi.ownerDocument);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/range.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<body>\n<div id=\"test-content\">\n  <p id=\"p1\">First paragraph</p>\n  <p id=\"p2\">Second paragraph</p>\n  <span id=\"s1\">Span content</span>\n</div>\n\n<script id=basic>\n{\n  const range = document.createRange();\n  testing.expectEqual('object', typeof range);\n  testing.expectEqual(true, range instanceof Range);\n  testing.expectEqual(true, range instanceof AbstractRange);\n}\n</script>\n\n<script id=initial_state>\n{\n  const range = document.createRange();\n\n  // New range should be collapsed at document position\n  testing.expectEqual(document, range.startContainer);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(document, range.endContainer);\n  testing.expectEqual(0, range.endOffset);\n  testing.expectEqual(true, range.collapsed);\n}\n</script>\n\n<script id=setStart_setEnd>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n  const p2 = $('#p2');\n\n  range.setStart(p1, 0);\n  testing.expectEqual(p1, range.startContainer);\n  testing.expectEqual(0, range.startOffset);\n  // After setStart, if new start is after original end, range collapses\n  // Since original end was at (document, 0) and p1 is inside document,\n  // the range auto-collapses to the new start\n  testing.expectEqual(p1, range.endContainer);\n  testing.expectEqual(0, range.endOffset);\n  testing.expectEqual(true, range.collapsed);\n\n  range.setEnd(p2, 0);\n  testing.expectEqual(p2, range.endContainer);\n  testing.expectEqual(0, range.endOffset);\n  testing.expectEqual(false, range.collapsed);\n}\n</script>\n\n<script id=collapse>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n  const p2 = $('#p2');\n\n  range.setStart(p1, 0);\n  range.setEnd(p2, 1);\n  testing.expectEqual(false, range.collapsed);\n\n  // Collapse to start\n  range.collapse(true);\n  testing.expectEqual(p1, range.startContainer);\n  testing.expectEqual(p1, range.endContainer);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(0, range.endOffset);\n  testing.expectEqual(true, range.collapsed);\n\n  // Reset and collapse to end\n  range.setStart(p1, 0);\n  range.setEnd(p2, 1);\n  range.collapse(false);\n  testing.expectEqual(p2, range.startContainer);\n  testing.expectEqual(p2, range.endContainer);\n  testing.expectEqual(1, range.startOffset);\n  testing.expectEqual(1, range.endOffset);\n  testing.expectEqual(true, range.collapsed);\n}\n</script>\n\n<script id=selectNode>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n  const parent = p1.parentNode;\n\n  range.selectNode(p1);\n\n  testing.expectEqual(parent, range.startContainer);\n  testing.expectEqual(parent, range.endContainer);\n  testing.expectEqual(false, range.collapsed);\n\n  // Start should be before p1, end should be after p1\n  const startOffset = range.startOffset;\n  const endOffset = range.endOffset;\n  testing.expectEqual(startOffset + 1, endOffset);\n  testing.expectEqual(p1, parent.childNodes[startOffset]);\n}\n</script>\n\n<script id=selectNodeContents>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n\n  range.selectNodeContents(p1);\n\n  testing.expectEqual(p1, range.startContainer);\n  testing.expectEqual(p1, range.endContainer);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(p1.childNodes.length, range.endOffset);\n  testing.expectEqual(false, range.collapsed);\n}\n</script>\n\n<script id=setStartBefore_setStartAfter>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n  const p2 = $('#p2');\n  const parent = p1.parentNode;\n\n  range.setStartBefore(p1);\n  testing.expectEqual(parent, range.startContainer);\n  const beforeOffset = range.startOffset;\n  testing.expectEqual(p1, parent.childNodes[beforeOffset]);\n\n  range.setStartAfter(p1);\n  testing.expectEqual(parent, range.startContainer);\n  const afterOffset = range.startOffset;\n  testing.expectEqual(afterOffset, beforeOffset + 1);\n}\n</script>\n\n<script id=setEndBefore_setEndAfter>\n{\n  const range = document.createRange();\n  const p2 = $('#p2');\n  const parent = p2.parentNode;\n\n  range.setEndBefore(p2);\n  testing.expectEqual(parent, range.endContainer);\n  const beforeOffset = range.endOffset;\n  testing.expectEqual(p2, parent.childNodes[beforeOffset]);\n\n  range.setEndAfter(p2);\n  testing.expectEqual(parent, range.endContainer);\n  const afterOffset = range.endOffset;\n  testing.expectEqual(afterOffset, beforeOffset + 1);\n}\n</script>\n\n<script id=cloneRange>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n  const p2 = $('#p2');\n\n  range.setStart(p1, 0);\n  range.setEnd(p2, 1);\n\n  const clone = range.cloneRange();\n\n  testing.expectTrue(clone !== range);\n  testing.expectEqual(range.startContainer, clone.startContainer);\n  testing.expectEqual(range.startOffset, clone.startOffset);\n  testing.expectEqual(range.endContainer, clone.endContainer);\n  testing.expectEqual(range.endOffset, clone.endOffset);\n  testing.expectEqual(range.collapsed, clone.collapsed);\n\n  // Modifying clone shouldn't affect original\n  clone.collapse(true);\n  testing.expectEqual(true, clone.collapsed);\n  testing.expectEqual(false, range.collapsed);\n}\n</script>\n\n<script id=toString_collapsed>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n\n  range.setStart(p1, 0);\n  range.setEnd(p1, 0);\n\n  testing.expectEqual('', range.toString());\n}\n</script>\n\n<script id=toString_sameText>\n{\n  const p = document.createElement('p');\n  p.textContent = 'Hello World';\n\n  const range = document.createRange();\n  range.setStart(p.firstChild, 3);\n  range.setEnd(p.firstChild, 8);\n\n  testing.expectEqual('lo Wo', range.toString());\n}\n</script>\n\n<script id=toString_sameElement>\n{\n  const div = document.createElement('div');\n  div.innerHTML = '<p>First</p><p>Second</p><p>Third</p>';\n\n  const range = document.createRange();\n  range.setStart(div, 0);\n  range.setEnd(div, 2);\n\n  testing.expectEqual('FirstSecond', range.toString());\n}\n</script>\n\n<script id=toString_crossContainer_siblings>\n{\n  const p = document.createElement('p');\n  p.appendChild(document.createTextNode('AAAA'));\n  p.appendChild(document.createTextNode('BBBB'));\n  p.appendChild(document.createTextNode('CCCC'));\n\n  const range = document.createRange();\n  range.setStart(p.childNodes[0], 2);\n  range.setEnd(p.childNodes[2], 2);\n\n  testing.expectEqual('AABBBBCC', range.toString());\n}\n</script>\n\n<script id=toString_crossContainer_nested>\n{\n  const div = document.createElement('div');\n  div.innerHTML = '<p>First paragraph</p><p>Second paragraph</p>';\n\n  const range = document.createRange();\n  range.setStart(div.querySelector('p').firstChild, 6);\n  range.setEnd(div.querySelectorAll('p')[1].firstChild, 6);\n\n  testing.expectEqual('paragraphSecond', range.toString());\n}\n</script>\n\n<script id=toString_excludes_comments>\n{\n  const div = document.createElement('div');\n  div.appendChild(document.createTextNode('before'));\n  div.appendChild(document.createComment('this is a comment'));\n  div.appendChild(document.createTextNode('after'));\n\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  testing.expectEqual('beforeafter', range.toString());\n}\n</script>\n\n<script id=insertNode>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n  const newSpan = document.createElement('span');\n  newSpan.textContent = 'INSERTED';\n\n  // Select p1's contents\n  range.selectNodeContents(p1);\n\n  // Collapse to start\n  range.collapse(true);\n\n  // Insert node\n  range.insertNode(newSpan);\n\n  // Check that span is first child of p1\n  testing.expectEqual(newSpan, p1.firstChild);\n  testing.expectEqual('INSERTED', newSpan.textContent);\n}\n</script>\n\n<script id=insertNode_splitText>\n{\n  const div = document.createElement('div');\n  div.textContent = 'Hello World';\n\n  const range = document.createRange();\n  const textNode = div.firstChild;\n\n  // Set range to middle of text (after \"Hello \")\n  range.setStart(textNode, 6);\n  range.collapse(true);\n\n  // Insert a span in the middle\n  const span = document.createElement('span');\n  span.textContent = 'MIDDLE';\n  range.insertNode(span);\n\n  // Should have 3 children: \"Hello \", span, \"World\"\n  testing.expectEqual(3, div.childNodes.length);\n  testing.expectEqual('Hello ', div.childNodes[0].textContent);\n  testing.expectEqual(span, div.childNodes[1]);\n  testing.expectEqual('MIDDLE', div.childNodes[1].textContent);\n  testing.expectEqual('World', div.childNodes[2].textContent);\n\n  // Full text should be correct\n  testing.expectEqual('Hello MIDDLEWorld', div.textContent);\n}\n</script>\n\n<script id=deleteContents>\n{\n  const div = document.createElement('div');\n  div.innerHTML = '<p>Hello</p><span>World</span>';\n\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  testing.expectEqual(2, div.childNodes.length);\n\n  range.deleteContents();\n\n  testing.expectEqual(0, div.childNodes.length);\n  testing.expectEqual(true, range.collapsed);\n}\n</script>\n\n<script id=cloneContents>\n{\n  const div = document.createElement('div');\n  div.innerHTML = '<p>First</p><span>Second</span>';\n\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  const fragment = range.cloneContents();\n\n  // Original should be unchanged\n  testing.expectEqual(2, div.childNodes.length);\n\n  // Fragment should have copies\n  testing.expectEqual(2, fragment.childNodes.length);\n  testing.expectEqual('P', fragment.childNodes[0].tagName);\n  testing.expectEqual('First', fragment.childNodes[0].textContent);\n  testing.expectEqual('SPAN', fragment.childNodes[1].tagName);\n  testing.expectEqual('Second', fragment.childNodes[1].textContent);\n}\n</script>\n\n<script id=extractContents>\n{\n  const div = document.createElement('div');\n  div.innerHTML = '<p>First</p><span>Second</span>';\n\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  const fragment = range.extractContents();\n\n  // Original should be empty\n  testing.expectEqual(0, div.childNodes.length);\n\n  // Fragment should have extracted content\n  testing.expectEqual(2, fragment.childNodes.length);\n  testing.expectEqual('P', fragment.childNodes[0].tagName);\n  testing.expectEqual('SPAN', fragment.childNodes[1].tagName);\n}\n</script>\n\n<script id=surroundContents>\n{\n  const div = document.createElement('div');\n  div.innerHTML = '<p>Content</p>';\n\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  const wrapper = document.createElement('section');\n  wrapper.className = 'wrapper';\n\n  range.surroundContents(wrapper);\n\n  // Div should now contain only the wrapper\n  testing.expectEqual(1, div.childNodes.length);\n  testing.expectEqual(wrapper, div.firstChild);\n  testing.expectEqual('wrapper', wrapper.className);\n\n  // Wrapper should contain original content\n  testing.expectEqual(1, wrapper.childNodes.length);\n  testing.expectEqual('P', wrapper.firstChild.tagName);\n  testing.expectEqual('Content', wrapper.firstChild.textContent);\n}\n</script>\n\n<script id=createContextualFragment_basic>\n{\n  const div = document.createElement('div');\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  const fragment = range.createContextualFragment('<p>Hello</p><span>World</span>');\n\n  // Fragment should contain the parsed elements\n  testing.expectEqual(2, fragment.childNodes.length);\n  testing.expectEqual('P', fragment.childNodes[0].tagName);\n  testing.expectEqual('Hello', fragment.childNodes[0].textContent);\n  testing.expectEqual('SPAN', fragment.childNodes[1].tagName);\n  testing.expectEqual('World', fragment.childNodes[1].textContent);\n\n  // Original div should be unchanged\n  testing.expectEqual(0, div.childNodes.length);\n}\n</script>\n\n<script id=createContextualFragment_empty>\n{\n  const div = document.createElement('div');\n  const range = document.createRange();\n  range.selectNodeContents(div);\n\n  const fragment = range.createContextualFragment('');\n\n  testing.expectEqual(0, fragment.childNodes.length);\n}\n</script>\n\n<script id=createContextualFragment_textContext>\n{\n  const div = document.createElement('div');\n  div.textContent = 'Some text';\n\n  const range = document.createRange();\n  const textNode = div.firstChild;\n  range.setStart(textNode, 5);\n  range.collapse(true);\n\n  // Even though range is in text node, should use parent (div) as context\n  const fragment = range.createContextualFragment('<b>Bold</b>');\n\n  testing.expectEqual(1, fragment.childNodes.length);\n  testing.expectEqual('B', fragment.childNodes[0].tagName);\n  testing.expectEqual('Bold', fragment.childNodes[0].textContent);\n}\n</script>\n\n<script id=offset_validation_setStart>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n\n  // Test setStart with offset beyond node length\n  testing.expectError('IndexSizeError:', () => {\n    range.setStart(p1, 999);\n  });\n\n  // Test with negative offset (wraps to large u32)\n  testing.expectError('IndexSizeError:', () => {\n    range.setStart(p1.firstChild, -1);\n  });\n}\n</script>\n\n<script id=offset_validation_setEnd>\n{\n  const range = document.createRange();\n  const p1 = $('#p1');\n\n  // Test setEnd with offset beyond node length\n  testing.expectError('IndexSizeError:', () => {\n    range.setEnd(p1, 999);\n  });\n\n  // Test with text node\n  testing.expectError('IndexSizeError:', () => {\n    range.setEnd(p1.firstChild, 9999);\n  });\n}\n</script>\n\n<script id=comparePoint_basic>\n{\n  // Create fresh elements to avoid DOM pollution from other tests\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  p1.textContent = 'First paragraph text';\n  p2.textContent = 'Second paragraph text';\n  div.appendChild(p1);\n  div.appendChild(p2);\n\n  const range = document.createRange();\n  range.setStart(p1.firstChild, 0);\n  range.setEnd(p2.firstChild, 5);\n\n  // Point before range\n  testing.expectEqual(-1, range.comparePoint(div, 0));\n\n  // Point at start boundary\n  testing.expectEqual(0, range.comparePoint(p1.firstChild, 0));\n\n  // Point inside range (in p1)\n  testing.expectEqual(0, range.comparePoint(p1.firstChild, 3));\n\n  // Point inside range (in p2)\n  testing.expectEqual(0, range.comparePoint(p2.firstChild, 2));\n\n  // Point at end boundary\n  testing.expectEqual(0, range.comparePoint(p2.firstChild, 5));\n\n  // Point after range\n  testing.expectEqual(1, range.comparePoint(p2.firstChild, 10));\n}\n</script>\n\n<script id=comparePoint_validation>\n{\n  // Create fresh element\n  const p1 = document.createElement('p');\n  p1.textContent = 'Test content';\n\n  const range = document.createRange();\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n\n  // Test comparePoint with invalid offset\n  testing.expectError('IndexSizeError:', () => {\n    range.comparePoint(p1, 20);\n  });\n\n  testing.expectError('IndexSizeError:', () => {\n    range.comparePoint(p1.firstChild, -1);\n  });\n}\n</script>\n\n<script id=different_document_collapse>\n{\n  // Create fresh element in current document\n  const p1 = document.createElement('p');\n  p1.textContent = 'Local content';\n\n  const range = document.createRange();\n\n  // Create a foreign document\n  const foreignDoc = document.implementation.createHTMLDocument('');\n  const foreignP = foreignDoc.createElement('p');\n  foreignP.textContent = 'Foreign';\n  foreignDoc.body.appendChild(foreignP);\n\n  // Set up range in current document\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n  testing.expectEqual(false, range.collapsed);\n\n  // Setting start to foreign document should collapse to that point\n  range.setStart(foreignP, 0);\n  testing.expectEqual(true, range.collapsed);\n  testing.expectEqual(foreignP, range.startContainer);\n  testing.expectEqual(foreignP, range.endContainer);\n}\n</script>\n\n<script id=detached_node_collapse>\n{\n  // Create fresh element\n  const p1 = document.createElement('p');\n  p1.textContent = 'Attached content';\n\n  const range = document.createRange();\n\n  // Create a detached element\n  const detached = document.createElement('div');\n  detached.textContent = 'Detached';\n\n  // Set up range in document\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n  testing.expectEqual(false, range.collapsed);\n\n  // Setting end to detached node should collapse\n  range.setEnd(detached.firstChild, 0);\n  testing.expectEqual(true, range.collapsed);\n  testing.expectEqual(detached.firstChild, range.startContainer);\n  testing.expectEqual(detached.firstChild, range.endContainer);\n}\n</script>\n\n<script id=isPointInRange_basic>\n{\n  // Create fresh elements\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  p1.textContent = 'First paragraph';\n  p2.textContent = 'Second paragraph';\n  div.appendChild(p1);\n  div.appendChild(p2);\n\n  const range = document.createRange();\n  range.setStart(p1.firstChild, 5);\n  range.setEnd(p2.firstChild, 6);\n\n  // Point before range\n  testing.expectEqual(false, range.isPointInRange(p1.firstChild, 0));\n\n  // Point at start boundary\n  testing.expectEqual(true, range.isPointInRange(p1.firstChild, 5));\n\n  // Point inside range\n  testing.expectEqual(true, range.isPointInRange(p1.firstChild, 7));\n  testing.expectEqual(true, range.isPointInRange(p2.firstChild, 3));\n\n  // Point at end boundary\n  testing.expectEqual(true, range.isPointInRange(p2.firstChild, 6));\n\n  // Point after range\n  testing.expectEqual(false, range.isPointInRange(p2.firstChild, 10));\n}\n</script>\n\n<script id=isPointInRange_different_root>\n{\n  // Create element in current document\n  const p1 = document.createElement('p');\n  p1.textContent = 'Local content';\n\n  const range = document.createRange();\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n\n  // Create element in different document\n  const foreignDoc = document.implementation.createHTMLDocument('');\n  const foreignP = foreignDoc.createElement('p');\n  foreignP.textContent = 'Foreign';\n\n  // Point in different root should return false (not throw)\n  testing.expectEqual(false, range.isPointInRange(foreignP, 0));\n}\n</script>\n\n<script id=isPointInRange_validation>\n{\n  const p1 = document.createElement('p');\n  p1.textContent = 'Test content';\n\n  const range = document.createRange();\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n\n  // Invalid offset should throw IndexSizeError\n  testing.expectError('IndexSizeError:', () => {\n    range.isPointInRange(p1, 999);\n  });\n\n  testing.expectError('IndexSizeError:', () => {\n    range.isPointInRange(p1.firstChild, 9999);\n  });\n}\n</script>\n\n<script id=intersectsNode_basic>\n{\n  // Create fresh elements\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  const p3 = document.createElement('p');\n  p1.textContent = 'First';\n  p2.textContent = 'Second';\n  p3.textContent = 'Third';\n  div.appendChild(p1);\n  div.appendChild(p2);\n  div.appendChild(p3);\n\n  const range = document.createRange();\n  range.setStart(p1.firstChild, 2);\n  range.setEnd(p2.firstChild, 3);\n\n  // Node that intersects (p1 contains the start)\n  testing.expectEqual(true, range.intersectsNode(p1));\n\n  // Node that intersects (p2 contains the end)\n  testing.expectEqual(true, range.intersectsNode(p2));\n\n  // Node that doesn't intersect (p3 is after the range)\n  testing.expectEqual(false, range.intersectsNode(p3));\n\n  // Container intersects\n  testing.expectEqual(true, range.intersectsNode(div));\n}\n</script>\n\n<script id=intersectsNode_detached>\n{\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  p1.textContent = 'Content';\n  div.appendChild(p1);\n\n  const range = document.createRange();\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n\n  // The root node (div) should return true when it has no parent\n  // (Note: div is detached, so it's in the same tree as the range)\n  testing.expectEqual(true, range.intersectsNode(div));\n}\n</script>\n\n<script id=intersectsNode_different_root>\n{\n  const p1 = document.createElement('p');\n  p1.textContent = 'Local';\n\n  const range = document.createRange();\n  range.setStart(p1, 0);\n  range.setEnd(p1, 1);\n\n  // Node in different document should return false\n  const foreignDoc = document.implementation.createHTMLDocument('');\n  const foreignP = foreignDoc.createElement('p');\n  testing.expectEqual(false, range.intersectsNode(foreignP));\n}\n</script>\n\n<script id=commonAncestorContainer_same_node>\n{\n  const p = document.createElement('p');\n  p.textContent = 'Content';\n\n  const range = document.createRange();\n  range.setStart(p.firstChild, 0);\n  range.setEnd(p.firstChild, 3);\n\n  // Both boundaries in same text node, so that's the common ancestor\n  testing.expectEqual(p.firstChild, range.commonAncestorContainer);\n}\n</script>\n\n<script id=commonAncestorContainer_siblings>\n{\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  p1.textContent = 'First';\n  p2.textContent = 'Second';\n  div.appendChild(p1);\n  div.appendChild(p2);\n\n  const range = document.createRange();\n  range.setStart(p1.firstChild, 0);\n  range.setEnd(p2.firstChild, 3);\n\n  // Start and end in different siblings, common ancestor is the parent div\n  testing.expectEqual(div, range.commonAncestorContainer);\n}\n</script>\n\n<script id=commonAncestorContainer_nested>\n{\n  const div = document.createElement('div');\n  const section = document.createElement('section');\n  const p = document.createElement('p');\n  const span = document.createElement('span');\n  p.textContent = 'Text';\n  span.textContent = 'Span';\n\n  div.appendChild(section);\n  section.appendChild(p);\n  div.appendChild(span);\n\n  const range = document.createRange();\n  range.setStart(p.firstChild, 0);\n  range.setEnd(span.firstChild, 2);\n\n  // Common ancestor of deeply nested p and sibling span is div\n  testing.expectEqual(div, range.commonAncestorContainer);\n}\n</script>\n\n<script id=compareBoundaryPoints_constants>\n{\n  // Test that the constants are defined\n  testing.expectEqual(0, Range.START_TO_START);\n  testing.expectEqual(1, Range.START_TO_END);\n  testing.expectEqual(2, Range.END_TO_END);\n  testing.expectEqual(3, Range.END_TO_START);\n}\n</script>\n\n<script id=compareBoundaryPoints_basic>\n{\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  p1.textContent = 'First paragraph';\n  p2.textContent = 'Second paragraph';\n  div.appendChild(p1);\n  div.appendChild(p2);\n\n  const range1 = document.createRange();\n  range1.setStart(p1.firstChild, 0);\n  range1.setEnd(p1.firstChild, 5);\n\n  const range2 = document.createRange();\n  range2.setStart(p1.firstChild, 3);\n  range2.setEnd(p2.firstChild, 5);\n\n  // range1 start is before range2 start\n  testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_START, range2));\n\n  // range1 end is after range2 start\n  testing.expectEqual(1, range1.compareBoundaryPoints(Range.START_TO_END, range2));\n\n  // range1 start is before range2 end\n  testing.expectEqual(-1, range1.compareBoundaryPoints(Range.END_TO_START, range2));\n\n  // range1 end is before range2 end\n  testing.expectEqual(-1, range1.compareBoundaryPoints(Range.END_TO_END, range2));\n}\n</script>\n\n<script id=compareBoundaryPoints_same_range>\n{\n  const p = document.createElement('p');\n  p.textContent = 'Content';\n\n  const range = document.createRange();\n  range.setStart(p.firstChild, 2);\n  range.setEnd(p.firstChild, 5);\n\n  // Comparing a range to itself should return 0\n  testing.expectEqual(0, range.compareBoundaryPoints(Range.START_TO_START, range));\n  testing.expectEqual(0, range.compareBoundaryPoints(Range.END_TO_END, range));\n\n  // End is after start\n  testing.expectEqual(1, range.compareBoundaryPoints(Range.START_TO_END, range));\n\n  // Start is before end\n  testing.expectEqual(-1, range.compareBoundaryPoints(Range.END_TO_START, range));\n}\n</script>\n\n<script id=compareBoundaryPoints_invalid_how>\n{\n  const p = document.createElement('p');\n  p.textContent = 'Test';\n\n  const range1 = document.createRange();\n  const range2 = document.createRange();\n  range1.setStart(p, 0);\n  range2.setStart(p, 0);\n\n  // Invalid how parameter should throw NotSupportedError\n  testing.expectError('NotSupportedError:', () => {\n    range1.compareBoundaryPoints(4, range2);\n  });\n\n  testing.expectError('NotSupportedError:', () => {\n    range1.compareBoundaryPoints(99, range2);\n  });\n}\n</script>\n\n<script id=compareBoundaryPoints_different_root>\n{\n  const p1 = document.createElement('p');\n  p1.textContent = 'Local';\n\n  const range1 = document.createRange();\n  range1.setStart(p1, 0);\n  range1.setEnd(p1, 1);\n\n  // Create range in different document\n  const foreignDoc = document.implementation.createHTMLDocument('');\n  const foreignP = foreignDoc.createElement('p');\n  foreignP.textContent = 'Foreign';\n\n  const range2 = foreignDoc.createRange();\n  range2.setStart(foreignP, 0);\n  range2.setEnd(foreignP, 1);\n\n  // Comparing ranges in different documents should throw WrongDocumentError\n  testing.expectError('WrongDocumentError:', () => {\n    range1.compareBoundaryPoints(Range.START_TO_START, range2);\n  });\n}\n</script>\n\n<script id=deleteContents_crossNode>\n{\n  // Test deleteContents across multiple sibling text nodes\n  const p = document.createElement('p');\n  p.appendChild(document.createTextNode('AAAA'));\n  p.appendChild(document.createTextNode('BBBB'));\n  p.appendChild(document.createTextNode('CCCC'));\n\n  testing.expectEqual(3, p.childNodes.length);\n  testing.expectEqual('AAAABBBBCCCC', p.textContent);\n\n  const range = document.createRange();\n  // Start at position 2 in first text node (\"AA|AA\")\n  range.setStart(p.childNodes[0], 2);\n  // End at position 2 in third text node (\"CC|CC\")\n  range.setEnd(p.childNodes[2], 2);\n\n  range.deleteContents();\n\n  // Should have truncated first node to \"AA\" and third node to \"CC\"\n  // Middle node should be removed\n  testing.expectEqual(2, p.childNodes.length);\n  testing.expectEqual('AA', p.childNodes[0].textContent);\n  testing.expectEqual('CC', p.childNodes[1].textContent);\n  testing.expectEqual('AACC', p.textContent);\n}\n</script>\n\n<script id=deleteContents_crossNode_partial>\n{\n  // Test deleteContents where start node is completely preserved\n  const p = document.createElement('p');\n  p.appendChild(document.createTextNode('KEEP'));\n  p.appendChild(document.createTextNode('DELETE'));\n  p.appendChild(document.createTextNode('PARTIAL'));\n\n  const range = document.createRange();\n  // Start at end of first text node\n  range.setStart(p.childNodes[0], 4);\n  // End in middle of third text node\n  range.setEnd(p.childNodes[2], 4);\n\n  range.deleteContents();\n\n  testing.expectEqual(2, p.childNodes.length);\n  testing.expectEqual('KEEP', p.childNodes[0].textContent);\n  testing.expectEqual('IAL', p.childNodes[1].textContent);\n  testing.expectEqual('KEEPIAL', p.textContent);\n}\n</script>\n\n<script id=extractContents_crossNode>\n{\n  // Test extractContents across multiple sibling text nodes\n  const p = document.createElement('p');\n  p.appendChild(document.createTextNode('AAAA'));\n  p.appendChild(document.createTextNode('BBBB'));\n  p.appendChild(document.createTextNode('CCCC'));\n\n  const range = document.createRange();\n  range.setStart(p.childNodes[0], 2);\n  range.setEnd(p.childNodes[2], 2);\n\n  const fragment = range.extractContents();\n\n  // Original should be truncated\n  testing.expectEqual(2, p.childNodes.length);\n  testing.expectEqual('AA', p.childNodes[0].textContent);\n  testing.expectEqual('CC', p.childNodes[1].textContent);\n\n  // Fragment should contain extracted content\n  testing.expectEqual(3, fragment.childNodes.length);\n  testing.expectEqual('AA', fragment.childNodes[0].textContent);\n  testing.expectEqual('BBBB', fragment.childNodes[1].textContent);\n  testing.expectEqual('CC', fragment.childNodes[2].textContent);\n}\n</script>\n\n<script id=cloneContents_crossNode>\n{\n  // Test cloneContents across multiple sibling text nodes\n  const p = document.createElement('p');\n  p.appendChild(document.createTextNode('AAAA'));\n  p.appendChild(document.createTextNode('BBBB'));\n  p.appendChild(document.createTextNode('CCCC'));\n\n  const range = document.createRange();\n  range.setStart(p.childNodes[0], 2);\n  range.setEnd(p.childNodes[2], 2);\n\n  const fragment = range.cloneContents();\n\n  // Original should be unchanged\n  testing.expectEqual(3, p.childNodes.length);\n  testing.expectEqual('AAAA', p.childNodes[0].textContent);\n  testing.expectEqual('BBBB', p.childNodes[1].textContent);\n  testing.expectEqual('CCCC', p.childNodes[2].textContent);\n\n  // Fragment should contain cloned content\n  testing.expectEqual(3, fragment.childNodes.length);\n  testing.expectEqual('AA', fragment.childNodes[0].textContent);\n  testing.expectEqual('BBBB', fragment.childNodes[1].textContent);\n  testing.expectEqual('CC', fragment.childNodes[2].textContent);\n}\n</script>\n\n<script id=deleteContents_crossNode_withElements>\n{\n  // Test deleteContents with mixed text and element nodes\n  const div = document.createElement('div');\n  div.appendChild(document.createTextNode('Start'));\n  const span = document.createElement('span');\n  span.textContent = 'Middle';\n  div.appendChild(span);\n  div.appendChild(document.createTextNode('End'));\n\n  testing.expectEqual(3, div.childNodes.length);\n\n  const range = document.createRange();\n  // Start in middle of first text node\n  range.setStart(div.childNodes[0], 2);\n  // End in middle of last text node\n  range.setEnd(div.childNodes[2], 1);\n\n  range.deleteContents();\n\n  // Should keep \"St\" from start, remove span, keep \"nd\" from end\n  testing.expectEqual(2, div.childNodes.length);\n  testing.expectEqual('St', div.childNodes[0].textContent);\n  testing.expectEqual('nd', div.childNodes[1].textContent);\n  testing.expectEqual('Stnd', div.textContent);\n}\n</script>\n\n<script id=getBoundingClientRect_collapsed>\n{\n  const range = new Range();\n  const rect = range.getBoundingClientRect();\n  testing.expectTrue(rect instanceof DOMRect);\n  testing.expectEqual(0, rect.x);\n  testing.expectEqual(0, rect.y);\n  testing.expectEqual(0, rect.width);\n  testing.expectEqual(0, rect.height);\n}\n</script>\n\n<script id=getBoundingClientRect_element>\n{\n  const range = new Range();\n  const p = document.getElementById('p1');\n  range.selectNodeContents(p);\n  const rect = range.getBoundingClientRect();\n  testing.expectTrue(rect instanceof DOMRect);\n  // Non-collapsed range delegates to the container element\n  const elemRect = p.getBoundingClientRect();\n  testing.expectEqual(elemRect.x, rect.x);\n  testing.expectEqual(elemRect.y, rect.y);\n  testing.expectEqual(elemRect.width, rect.width);\n  testing.expectEqual(elemRect.height, rect.height);\n}\n</script>\n\n<script id=getClientRects_collapsed>\n{\n  const range = new Range();\n  const rects = range.getClientRects();\n  testing.expectEqual(0, rects.length);\n}\n</script>\n\n<script id=getClientRects_element>\n{\n  const range = new Range();\n  const p = document.getElementById('p1');\n  range.selectNodeContents(p);\n  const rects = range.getClientRects();\n  const elemRects = p.getClientRects();\n  testing.expectEqual(elemRects.length, rects.length);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/range_mutations.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=insertData_adjusts_range_offsets>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n  // range covers \"cde\"\n\n  // Insert \"XX\" at offset 1 (before range start)\n  text.insertData(1, 'XX');\n  // \"aXXbcdef\" — range should shift right by 2\n  testing.expectEqual(4, range.startOffset);\n  testing.expectEqual(7, range.endOffset);\n  testing.expectEqual(text, range.startContainer);\n}\n</script>\n\n<script id=insertData_at_range_start>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n\n  // Insert at exactly the start offset — should not shift start\n  text.insertData(2, 'YY');\n  // \"abYYcdef\" — start stays at 2, end shifts by 2\n  testing.expectEqual(2, range.startOffset);\n  testing.expectEqual(7, range.endOffset);\n}\n</script>\n\n<script id=insertData_inside_range>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n\n  // Insert inside the range\n  text.insertData(3, 'Z');\n  // \"abcZdef\" — start unchanged, end shifts by 1\n  testing.expectEqual(2, range.startOffset);\n  testing.expectEqual(6, range.endOffset);\n}\n</script>\n\n<script id=insertData_after_range>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n\n  // Insert after range end — no change\n  text.insertData(5, 'ZZ');\n  testing.expectEqual(2, range.startOffset);\n  testing.expectEqual(5, range.endOffset);\n}\n</script>\n\n<script id=deleteData_before_range>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 3);\n  range.setEnd(text, 5);\n  // range covers \"de\"\n\n  // Delete \"ab\" (offset 0, count 2) — before range\n  text.deleteData(0, 2);\n  // \"cdef\" — range shifts left by 2\n  testing.expectEqual(1, range.startOffset);\n  testing.expectEqual(3, range.endOffset);\n}\n</script>\n\n<script id=deleteData_overlapping_range_start>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n\n  // Delete from offset 1, count 2 — overlaps range start\n  text.deleteData(1, 2);\n  // \"adef\" — start clamped to offset(1), end adjusted\n  testing.expectEqual(1, range.startOffset);\n  testing.expectEqual(3, range.endOffset);\n}\n</script>\n\n<script id=deleteData_inside_range>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 1);\n  range.setEnd(text, 5);\n\n  // Delete inside range: offset 2, count 2\n  text.deleteData(2, 2);\n  // \"abef\" — start unchanged, end shifts by -2\n  testing.expectEqual(1, range.startOffset);\n  testing.expectEqual(3, range.endOffset);\n}\n</script>\n\n<script id=replaceData_adjusts_range>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n\n  // Replace \"cd\" (offset 2, count 2) with \"XXXX\" (4 chars)\n  text.replaceData(2, 2, 'XXXX');\n  // \"abXXXXef\" — start clamped to 2, end adjusted by (4-2)=+2\n  testing.expectEqual(2, range.startOffset);\n  testing.expectEqual(7, range.endOffset);\n}\n</script>\n\n<script id=splitText_moves_range_to_new_node>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 4);\n  range.setEnd(text, 6);\n  // range covers \"ef\"\n\n  const newText = text.splitText(3);\n  // text = \"abc\", newText = \"def\"\n  // Range was at (text, 4)-(text, 6), with offset > 3:\n  // start moves to (newText, 4-3=1), end moves to (newText, 6-3=3)\n  testing.expectEqual(newText, range.startContainer);\n  testing.expectEqual(1, range.startOffset);\n  testing.expectEqual(newText, range.endContainer);\n  testing.expectEqual(3, range.endOffset);\n}\n</script>\n\n<script id=splitText_range_at_split_point>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 0);\n  range.setEnd(text, 3);\n  // range covers \"abc\"\n\n  const newText = text.splitText(3);\n  // text = \"abc\", newText = \"def\"\n  // Range end is at exactly the split offset — should stay on original node\n  testing.expectEqual(text, range.startContainer);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(text, range.endContainer);\n  testing.expectEqual(3, range.endOffset);\n}\n</script>\n\n<script id=appendChild_does_not_affect_range>\n{\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  div.appendChild(p1);\n  div.appendChild(p2);\n\n  const range = document.createRange();\n  range.setStart(div, 0);\n  range.setEnd(div, 2);\n\n  // Appending should not affect range offsets (spec: no update for append)\n  const p3 = document.createElement('p');\n  div.appendChild(p3);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(2, range.endOffset);\n}\n</script>\n\n<script id=insertBefore_shifts_range_offsets>\n{\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  div.appendChild(p1);\n  div.appendChild(p2);\n\n  const range = document.createRange();\n  range.setStart(div, 1);\n  range.setEnd(div, 2);\n\n  // Insert before p1 (index 0) — range offsets > 0 should increment\n  const span = document.createElement('span');\n  div.insertBefore(span, p1);\n  testing.expectEqual(2, range.startOffset);\n  testing.expectEqual(3, range.endOffset);\n}\n</script>\n\n<script id=removeChild_shifts_range_offsets>\n{\n  const div = document.createElement('div');\n  const p1 = document.createElement('p');\n  const p2 = document.createElement('p');\n  const p3 = document.createElement('p');\n  div.appendChild(p1);\n  div.appendChild(p2);\n  div.appendChild(p3);\n\n  const range = document.createRange();\n  range.setStart(div, 1);\n  range.setEnd(div, 3);\n\n  // Remove p1 (index 0) — offsets > 0 should decrement\n  div.removeChild(p1);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(2, range.endOffset);\n}\n</script>\n\n<script id=removeChild_moves_range_from_descendant>\n{\n  const div = document.createElement('div');\n  const p = document.createElement('p');\n  const text = document.createTextNode('hello');\n  p.appendChild(text);\n  div.appendChild(p);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 4);\n\n  // Remove p (which contains text) — range should move to (div, index_of_p)\n  div.removeChild(p);\n  testing.expectEqual(div, range.startContainer);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(div, range.endContainer);\n  testing.expectEqual(0, range.endOffset);\n}\n</script>\n\n<script id=multiple_ranges_updated>\n{\n  const text = document.createTextNode('abcdefgh');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range1 = document.createRange();\n  range1.setStart(text, 1);\n  range1.setEnd(text, 3);\n\n  const range2 = document.createRange();\n  range2.setStart(text, 5);\n  range2.setEnd(text, 7);\n\n  // Insert at offset 0 — both ranges should shift\n  text.insertData(0, 'XX');\n  testing.expectEqual(3, range1.startOffset);\n  testing.expectEqual(5, range1.endOffset);\n  testing.expectEqual(7, range2.startOffset);\n  testing.expectEqual(9, range2.endOffset);\n}\n</script>\n\n<script id=data_setter_updates_ranges>\n{\n  const text = document.createTextNode('abcdef');\n  const div = document.createElement('div');\n  div.appendChild(text);\n\n  const range = document.createRange();\n  range.setStart(text, 2);\n  range.setEnd(text, 5);\n\n  // Setting data replaces all content — range collapses to offset 0\n  text.data = 'new content';\n  testing.expectEqual(text, range.startContainer);\n  testing.expectEqual(0, range.startOffset);\n  testing.expectEqual(text, range.endContainer);\n  testing.expectEqual(0, range.endOffset);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/selection.html",
    "content": "<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n<script src=\"./testing.js\"></script>\n\n<div id=\"test-content\">\n  <p id=\"p1\">The quick brown fox</p>\n  <p id=\"p2\">jumps over the lazy dog</p>\n  <div id=\"nested\">\n    <span id=\"s1\">Hello</span>\n    <span id=\"s2\">World</span>\n  </div>\n</div>\n\n<script id=basic>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    \n    testing.expectEqual(0, sel.rangeCount);\n    testing.expectEqual(\"None\", sel.type);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(null, sel.anchorNode);\n    testing.expectEqual(null, sel.focusNode);\n    testing.expectEqual(0, sel.anchorOffset);\n    testing.expectEqual(0, sel.focusOffset);\n    testing.expectEqual(\"none\", sel.direction);\n  }\n</script>\n\n<script id=collapse>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    \n    // Collapse to a position\n    sel.collapse(textNode, 4);\n    \n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Caret\", sel.type);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(textNode, sel.focusNode);\n    testing.expectEqual(4, sel.anchorOffset);\n    testing.expectEqual(4, sel.focusOffset);\n    testing.expectEqual(\"none\", sel.direction);\n    \n    // Collapse to null removes all ranges\n    sel.collapse(null);\n    testing.expectEqual(0, sel.rangeCount);\n    testing.expectEqual(\"None\", sel.type);\n  }\n</script>\n\n<script id=setPosition>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    const p2 = document.getElementById(\"p2\");\n    const textNode = p2.firstChild;\n    \n    // setPosition is an alias for collapse\n    sel.setPosition(textNode, 10);\n    \n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Caret\", sel.type);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(10, sel.anchorOffset);\n    \n    // Test default offset\n    sel.setPosition(textNode);\n    testing.expectEqual(0, sel.anchorOffset);\n    \n    // Test null\n    sel.setPosition(null);\n    testing.expectEqual(0, sel.rangeCount);\n  }\n</script>\n\n<script id=addRange>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    \n    const range1 = document.createRange();\n    const p1 = document.getElementById(\"p1\");\n    range1.selectNodeContents(p1);\n    \n    sel.addRange(range1);\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Range\", sel.type);\n    testing.expectEqual(false, sel.isCollapsed);\n    \n    // Adding same range again should do nothing\n    sel.addRange(range1);\n    testing.expectEqual(1, sel.rangeCount);\n    \n    // Adding different range\n    const range2 = document.createRange();\n    const p2 = document.getElementById(\"p2\");\n    range2.selectNodeContents(p2);\n    \n    sel.addRange(range2);\n\n    // Firefox does support multiple ranges so it will be 2 here instead of 1.\n    // Chrome and Safari don't so we don't either.\n    testing.expectEqual(1, sel.rangeCount);\n  }\n</script>\n\n<script id=getRangeAt>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    \n    const range = document.createRange();\n    const p1 = document.getElementById(\"p1\");\n    range.selectNodeContents(p1);\n    sel.addRange(range);\n    \n    const retrieved = sel.getRangeAt(0);\n    testing.expectEqual(range, retrieved);\n  }\n</script>\n\n<script id=removeRange>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    \n    const range1 = document.createRange();\n    const range2 = document.createRange();\n    const p1 = document.getElementById(\"p1\");\n    const p2 = document.getElementById(\"p2\");\n    \n    range1.selectNodeContents(p1);\n    range2.selectNodeContents(p2);\n    \n    sel.addRange(range1);\n    sel.addRange(range2);\n\n    // Firefox does support multiple ranges so it will be 2 here instead of 1.\n    // Chrome and Safari don't so we don't either.\n    testing.expectEqual(1, sel.rangeCount);\n    \n    // Chrome doesn't throw an error here even though the spec defines it:\n    // https://w3c.github.io/selection-api/#dom-selection-removerange\n    testing.expectError('NotFoundError', () => { sel.removeRange(range2); });\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(range1, sel.getRangeAt(0));\n  }\n</script>\n\n<script id=removeAllRanges>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n\n    const range1 = document.createRange();\n    const range2 = document.createRange();\n    \n    range1.selectNodeContents(document.getElementById(\"p1\"));\n    range2.selectNodeContents(document.getElementById(\"p2\"));\n    \n    sel.addRange(range1);\n    sel.addRange(range2);\n\n    // Firefox does support multiple ranges so it will be 2 here instead of 1.\n    // Chrome and Safari don't so we don't either.\n    testing.expectEqual(1, sel.rangeCount);\n    \n    sel.removeAllRanges();\n    testing.expectEqual(0, sel.rangeCount);\n    testing.expectEqual(\"none\", sel.direction);\n  }\n</script>\n\n<script id=empty>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    const range = document.createRange();\n    range.selectNodeContents(document.getElementById(\"p1\"));\n    \n    sel.addRange(range);\n    testing.expectEqual(1, sel.rangeCount);\n    \n    // empty() is an alias for removeAllRanges()\n    sel.empty();\n    testing.expectEqual(0, sel.rangeCount);\n  }\n</script>\n\n<script id=collapseToStart>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    \n    const range = document.createRange();\n    range.setStart(textNode, 4);\n    range.setEnd(textNode, 15);\n    \n    sel.removeAllRanges();\n    sel.addRange(range);\n    \n    testing.expectEqual(false, sel.isCollapsed);\n    testing.expectEqual(4, sel.anchorOffset);\n    testing.expectEqual(15, sel.focusOffset);\n    \n    sel.collapseToStart();\n    \n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(4, sel.anchorOffset);\n    testing.expectEqual(4, sel.focusOffset);\n    testing.expectEqual(\"none\", sel.direction);\n  }\n</script>\n\n<script id=collapseToEnd>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    \n    const range = document.createRange();\n    range.setStart(textNode, 4);\n    range.setEnd(textNode, 15);\n    \n    sel.removeAllRanges();\n    sel.addRange(range);\n    \n    testing.expectEqual(false, sel.isCollapsed);\n    \n    sel.collapseToEnd();\n    \n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(15, sel.anchorOffset);\n    testing.expectEqual(15, sel.focusOffset);\n    testing.expectEqual(\"none\", sel.direction);\n  }\n</script>\n\n<script id=extend>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    \n    // Start with collapsed selection\n    sel.collapse(textNode, 10);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(10, sel.anchorOffset);\n    testing.expectEqual(\"none\", sel.direction);\n    \n    // Extend forward\n    sel.extend(textNode, 15);\n    testing.expectEqual(false, sel.isCollapsed);\n    testing.expectEqual(10, sel.anchorOffset);\n    testing.expectEqual(15, sel.focusOffset);\n    testing.expectEqual(\"forward\", sel.direction);\n    \n    // Extend backward from anchor\n    sel.extend(textNode, 5);\n    testing.expectEqual(false, sel.isCollapsed);\n    testing.expectEqual(10, sel.anchorOffset);\n    testing.expectEqual(5, sel.focusOffset);\n    testing.expectEqual(\"backward\", sel.direction);\n    \n    // Extend to same position as anchor\n    sel.extend(textNode, 10);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(10, sel.anchorOffset);\n    testing.expectEqual(10, sel.focusOffset);\n    testing.expectEqual(\"none\", sel.direction);\n  }\n</script>\n\n<script id=direction>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    \n    // Forward selection\n    sel.collapse(textNode, 5);\n    sel.extend(textNode, 10);\n    testing.expectEqual(\"forward\", sel.direction);\n    testing.expectEqual(5, sel.anchorOffset);\n    testing.expectEqual(10, sel.focusOffset);\n    \n    // Backward selection\n    sel.collapse(textNode, 10);\n    sel.extend(textNode, 5);\n    testing.expectEqual(\"backward\", sel.direction);\n    testing.expectEqual(10, sel.anchorOffset);\n    testing.expectEqual(5, sel.focusOffset);\n    \n    // None (collapsed)\n    sel.collapse(textNode, 7);\n    testing.expectEqual(\"none\", sel.direction);\n  }\n</script>\n\n<script id=containsNode>\n  {\n    const sel = window.getSelection();\n    const nested = document.getElementById(\"nested\");\n    const s1 = document.getElementById(\"s1\");\n    const s2 = document.getElementById(\"s2\");\n    \n    const range = document.createRange();\n    range.selectNodeContents(nested);\n    \n    sel.removeAllRanges();\n    sel.addRange(range);\n\n    // Partial containment\n    testing.expectEqual(true, sel.containsNode(s1, true));\n    testing.expectEqual(true, sel.containsNode(s2, true));\n    testing.expectEqual(true, sel.containsNode(nested, true));\n    \n    // Node outside selection\n    const p1 = document.getElementById(\"p1\");\n    testing.expectEqual(false, sel.containsNode(p1, false));\n    testing.expectEqual(false, sel.containsNode(p1, true));\n  }\n</script>\n\n\n<script id=deleteFromDocument>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    \n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    const originalText = textNode.textContent;\n    \n    const range = document.createRange();\n    range.setStart(textNode, 4);\n    range.setEnd(textNode, 15);\n    \n    sel.removeAllRanges();\n    sel.addRange(range);\n    \n    sel.deleteFromDocument();\n    \n    // Text should be deleted\n    const expectedText = originalText.slice(0, 4) + originalText.slice(15);\n    testing.expectEqual(expectedText, textNode.textContent);\n    \n    // Selection should be collapsed at deletion point\n    testing.expectEqual(true, sel.isCollapsed);\n    \n    // Restore original text for other tests\n    textNode.textContent = originalText;\n  }\n</script>\n\n<script id=typeProperty>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    \n    // None type\n    sel.removeAllRanges();\n    testing.expectEqual(\"None\", sel.type);\n    \n    // Caret type (collapsed)\n    sel.collapse(textNode, 5);\n    testing.expectEqual(\"Caret\", sel.type);\n    \n    // Range type (not collapsed)\n    sel.extend(textNode, 10);\n    testing.expectEqual(\"Range\", sel.type);\n  }\n</script>\n\n<script id=selectAllChildren>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n\n    const nested = document.getElementById(\"nested\");\n    const s1 = document.getElementById(\"s1\");\n    const s2 = document.getElementById(\"s2\");\n\n    // Select all children of nested div\n    sel.selectAllChildren(nested);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Range\", sel.type);\n    testing.expectEqual(false, sel.isCollapsed);\n\n    // Anchor and focus should be on the parent node\n    testing.expectEqual(nested, sel.anchorNode);\n    testing.expectEqual(nested, sel.focusNode);\n\n    // Should start at offset 0 (before first child)\n    testing.expectEqual(0, sel.anchorOffset);\n\n    const childrenCount = nested.childNodes.length;\n\n    // Should end at offset equal to number of children (after last child)\n    testing.expectEqual(childrenCount, sel.focusOffset);\n\n    // Direction should be forward\n    testing.expectEqual(\"forward\", sel.direction);\n\n    // Should not fully contain the parent itself\n    testing.expectEqual(false, sel.containsNode(nested, false));\n\n    // But should partially contain the parent\n    testing.expectEqual(true, sel.containsNode(nested, true));\n\n    // Verify the range\n    const range = sel.getRangeAt(0);\n    testing.expectEqual(nested, range.startContainer);\n    testing.expectEqual(nested, range.endContainer);\n    testing.expectEqual(0, range.startOffset);\n    testing.expectEqual(childrenCount, range.endOffset);\n  }\n</script>\n\n<script id=selectAllChildrenEmpty>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n\n    // Create an empty element\n    const empty = document.createElement(\"div\");\n    document.body.appendChild(empty);\n\n    // Select all children of empty element\n    sel.selectAllChildren(empty);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Caret\", sel.type); // Collapsed because no children\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(empty, sel.anchorNode);\n    testing.expectEqual(0, sel.anchorOffset);\n    testing.expectEqual(0, sel.focusOffset);\n\n    // Clean up\n    document.body.removeChild(empty);\n  }\n</script>\n\n<script id=selectAllChildrenReplacesSelection>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n\n    // Start with an existing selection\n    const p1 = document.getElementById(\"p1\");\n    sel.selectAllChildren(p1);\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(p1, sel.anchorNode);\n\n    // selectAllChildren should replace the existing selection\n    const p2 = document.getElementById(\"p2\");\n    sel.selectAllChildren(p2);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(p2, sel.anchorNode);\n    testing.expectEqual(p2, sel.focusNode);\n\n    // Verify old selection is gone\n    const range = sel.getRangeAt(0);\n    testing.expectEqual(p2, range.startContainer);\n    testing.expectEqual(false, p1 == range.startContainer);\n  }\n</script>\n\n<script id=setBaseAndExtent>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n\n    // Forward selection (anchor before focus)\n    sel.setBaseAndExtent(textNode, 4, textNode, 15);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Range\", sel.type);\n    testing.expectEqual(false, sel.isCollapsed);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(4, sel.anchorOffset);\n    testing.expectEqual(textNode, sel.focusNode);\n    testing.expectEqual(15, sel.focusOffset);\n    testing.expectEqual(\"forward\", sel.direction);\n\n    // Backward selection (anchor after focus)\n    sel.setBaseAndExtent(textNode, 15, textNode, 4);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Range\", sel.type);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(15, sel.anchorOffset);\n    testing.expectEqual(textNode, sel.focusNode);\n    testing.expectEqual(4, sel.focusOffset);\n    testing.expectEqual(\"backward\", sel.direction);\n\n    // Collapsed selection (anchor equals focus)\n    sel.setBaseAndExtent(textNode, 10, textNode, 10);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(\"Caret\", sel.type);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(10, sel.anchorOffset);\n    testing.expectEqual(10, sel.focusOffset);\n    testing.expectEqual(\"none\", sel.direction);\n\n    // Across different nodes\n    const p2 = document.getElementById(\"p2\");\n    const textNode2 = p2.firstChild;\n\n    sel.setBaseAndExtent(textNode, 4, textNode2, 5);\n\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(textNode, sel.anchorNode);\n    testing.expectEqual(4, sel.anchorOffset);\n    testing.expectEqual(textNode2, sel.focusNode);\n    testing.expectEqual(5, sel.focusOffset);\n    testing.expectEqual(\"forward\", sel.direction);\n\n    // Should replace existing selection\n    sel.setBaseAndExtent(textNode, 0, textNode, 3);\n    testing.expectEqual(1, sel.rangeCount);\n    testing.expectEqual(0, sel.anchorOffset);\n    testing.expectEqual(3, sel.focusOffset);\n  }\n</script>\n\n<script id=selectionChangeEvent>\n  {\n    const sel = window.getSelection();\n    sel.removeAllRanges();\n    let eventCount = 0;\n    let lastEvent = null;\n\n    const listener = (e) => {\n      eventCount++;\n      lastEvent = e;\n    };\n    document.addEventListener('selectionchange', listener);\n\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild;\n    const nested = document.getElementById(\"nested\");\n\n    sel.collapse(textNode, 5);\n    sel.extend(textNode, 10);\n    sel.collapseToStart();\n    sel.collapseToEnd();\n    sel.removeAllRanges();\n    const range = document.createRange();\n    range.setStart(textNode, 4);\n    range.setEnd(textNode, 15);\n    sel.addRange(range);\n    sel.removeRange(range);\n    const newRange = document.createRange();\n    newRange.selectNodeContents(p1);\n    sel.addRange(newRange);\n    sel.removeAllRanges();\n    sel.selectAllChildren(nested);\n    sel.setBaseAndExtent(textNode, 4, textNode, 15);\n    sel.collapse(textNode, 5);\n    sel.extend(textNode, 10);\n    sel.deleteFromDocument();\n\n    document.removeEventListener('selectionchange', listener);\n    textNode.textContent = \"The quick brown fox\";\n\n    testing.eventually(() => {\n      testing.expectEqual(14, eventCount);\n      testing.expectEqual('selectionchange', lastEvent.type);\n      testing.expectEqual(document, lastEvent.target);\n      testing.expectEqual(false, lastEvent.bubbles);\n      testing.expectEqual(false, lastEvent.cancelable);\n    });\n  }\n</script>\n\n<script id=modifyCharacterForward>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild; // \"The quick brown fox\"\n\n    // Collapse to position 4 (after \"The \")\n    sel.collapse(textNode, 4);\n    testing.expectEqual(4, sel.anchorOffset);\n\n    // Move forward one character\n    sel.modify(\"move\", \"forward\", \"character\");\n    testing.expectEqual(5, sel.anchorOffset);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(\"none\", sel.direction);\n\n    // Move forward again\n    sel.modify(\"move\", \"forward\", \"character\");\n    testing.expectEqual(6, sel.anchorOffset);\n  }\n</script>\n\n<script id=modifyWordForward>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild; // \"The quick brown fox\"\n\n    // Collapse to start\n    sel.collapse(textNode, 0);\n\n    // Move forward one word - should land at end of \"The\"\n    sel.modify(\"move\", \"forward\", \"word\");\n    testing.expectEqual(3, sel.anchorOffset);\n    testing.expectEqual(true, sel.isCollapsed);\n\n    // Move forward again - should skip space and land at end of \"quick\"\n    sel.modify(\"move\", \"forward\", \"word\");\n    testing.expectEqual(9, sel.anchorOffset);\n  }\n</script>\n\n<script id=modifyCharacterBackward>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild; // \"The quick brown fox\"\n\n    // Collapse to position 6\n    sel.collapse(textNode, 6);\n    testing.expectEqual(6, sel.anchorOffset);\n\n    // Move backward one character\n    sel.modify(\"move\", \"backward\", \"character\");\n    testing.expectEqual(5, sel.anchorOffset);\n    testing.expectEqual(true, sel.isCollapsed);\n    testing.expectEqual(\"none\", sel.direction);\n\n    // Move backward again\n    sel.modify(\"move\", \"backward\", \"character\");\n    testing.expectEqual(4, sel.anchorOffset);\n  }\n</script>\n\n<script id=modifyWordBackward>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    const textNode = p1.firstChild; // \"The quick brown fox\"\n\n    // Collapse to end of \"quick\" (offset 9)\n    sel.collapse(textNode, 9);\n\n    // Move backward one word - should land at start of \"quick\"\n    sel.modify(\"move\", \"backward\", \"word\");\n    testing.expectEqual(4, sel.anchorOffset);\n    testing.expectEqual(true, sel.isCollapsed);\n\n    // Move backward again - should land at start of \"The\"\n    sel.modify(\"move\", \"backward\", \"word\");\n    testing.expectEqual(0, sel.anchorOffset);\n  }\n</script>\n\n<script id=modifyCharacterForwardFromElementNode>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    sel.collapse(p1, 1);\n\n    testing.expectEqual(p1, sel.anchorNode);\n    testing.expectEqual(1, sel.anchorOffset);\n    testing.expectEqual(true, sel.isCollapsed);\n\n    sel.modify(\"move\", \"forward\", \"character\");\n\n    testing.expectEqual(3, sel.anchorNode.nodeType);\n    testing.expectEqual(true, sel.anchorNode !== p1.firstChild);\n  }\n</script>\n\n<script id=modifyCharacterForwardFromElementNodeMidChildren>\n  {\n    const sel = window.getSelection();\n    const nested = document.getElementById(\"nested\");\n\n    sel.collapse(nested, nested.childNodes.length);\n\n    testing.expectEqual(nested, sel.anchorNode);\n    testing.expectEqual(nested.childNodes.length, sel.anchorOffset);\n\n    sel.modify(\"move\", \"forward\", \"character\");\n\n    // Must land on a text node strictly after #nested\n    testing.expectEqual(3, sel.anchorNode.nodeType);\n    testing.expectEqual(false, nested.contains(sel.anchorNode));\n  }\n</script>\n\n<script id=modifyWordForwardFromElementNode>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n    sel.collapse(p1, 1);\n\n    sel.modify(\"move\", \"forward\", \"word\");\n\n    // Must land on a text node strictly after p1\n    testing.expectEqual(3, sel.anchorNode.nodeType);\n    testing.expectEqual(false, p1.contains(sel.anchorNode));\n    testing.expectEqual(true, sel.isCollapsed);\n  }\n</script>\n\n<script id=modifyCharacterForwardNewNodeOffsetNotElement>\n  {\n    const sel = window.getSelection();\n    const p1 = document.getElementById(\"p1\");\n\n    sel.collapse(p1, 1);\n    sel.modify(\"move\", \"forward\", \"character\");\n\n    testing.expectEqual(3, sel.anchorNode.nodeType);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/basic.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=\"host1\"></div>\n<div id=\"host2\"></div>\n<div id=\"host3\"></div>\n\n<script id=\"attachShadow_open\">\n{\n    const host = $('#host1');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    testing.expectEqual('open', shadow.mode);\n    testing.expectEqual(host, shadow.host);\n    testing.expectEqual(shadow, host.shadowRoot);\n}\n</script>\n\n<script id=\"attachShadow_closed\">\n{\n    const host = $('#host2');\n    const shadow = host.attachShadow({ mode: 'closed' });\n\n    testing.expectEqual('closed', shadow.mode);\n    testing.expectEqual(host, shadow.host);\n    testing.expectEqual(null, host.shadowRoot);\n}\n</script>\n\n<script id=\"innerHTML\">\n{\n    const host = $('#host3');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    shadow.innerHTML = '<p>Hello</p><span>World</span>';\n    testing.expectEqual('<p>Hello</p><span>World</span>', shadow.innerHTML);\n\n    const p = shadow.querySelector('p');\n    testing.expectEqual('Hello', p.textContent);\n\n    const span = shadow.querySelector('span');\n    testing.expectEqual('World', span.textContent);\n}\n</script>\n\n<script id=\"querySelector\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div class=\"test\"><p id=\"para\">Text</p></div>';\n\n    const para = shadow.querySelector('#para');\n    testing.expectEqual('Text', para.textContent);\n\n    const div = shadow.querySelector('.test');\n    testing.expectEqual('DIV', div.tagName);\n\n    testing.expectEqual(null, shadow.querySelector('.nonexistent'));\n}\n</script>\n\n<script id=\"querySelectorAll\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>One</p><p>Two</p><p>Three</p>';\n\n    const paras = shadow.querySelectorAll('p');\n    testing.expectEqual(3, paras.length);\n    testing.expectEqual('One', paras[0].textContent);\n    testing.expectEqual('Two', paras[1].textContent);\n    testing.expectEqual('Three', paras[2].textContent);\n}\n</script>\n\n<script id=\"children\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div>A</div><span>B</span><p>C</p>';\n\n    const children = shadow.children;\n    testing.expectEqual(3, children.length);\n    testing.expectEqual('DIV', children[0].tagName);\n    testing.expectEqual('SPAN', children[1].tagName);\n    testing.expectEqual('P', children[2].tagName);\n}\n</script>\n\n<script id=\"firstLastChild\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div>First</div><span>Last</span>';\n\n    testing.expectEqual('DIV', shadow.firstElementChild.tagName);\n    testing.expectEqual('First', shadow.firstElementChild.textContent);\n\n    testing.expectEqual('SPAN', shadow.lastElementChild.tagName);\n    testing.expectEqual('Last', shadow.lastElementChild.textContent);\n}\n</script>\n\n<script id=\"appendChild\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const p = document.createElement('p');\n    p.textContent = 'Test';\n    shadow.appendChild(p);\n\n    testing.expectEqual(1, shadow.childElementCount);\n    testing.expectEqual(p, shadow.firstElementChild);\n    testing.expectEqual('Test', shadow.firstElementChild.textContent);\n}\n</script>\n\n<script id=\"append_prepend\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    shadow.append('text1');\n    testing.expectEqual('text1', shadow.innerHTML);\n\n    shadow.prepend('text0');\n    testing.expectEqual('text0text1', shadow.innerHTML);\n}\n</script>\n\n<script id=\"replaceChildren\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div>Old</div>';\n\n    testing.expectEqual(1, shadow.childElementCount);\n\n    shadow.replaceChildren('New content');\n    testing.expectEqual('New content', shadow.innerHTML);\n}\n</script>\n\n<script id=\"getElementById\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"test\">Content</div>';\n\n    const el = shadow.getElementById('test');\n    testing.expectEqual('Content', el.textContent);\n\n    testing.expectEqual(null, shadow.getElementById('nonexistent'));\n}\n</script>\n\n\n<script id=adoptedStyleSheets>\n  {\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const acss = shadow.adoptedStyleSheets;\n    testing.expectEqual(0, acss.length);\n    acss.push(new CSSStyleSheet());\n    testing.expectEqual(1, acss.length);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/custom_elements.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"shadow_in_constructor\">\n{\n    class ShadowElement extends HTMLElement {\n        constructor() {\n            super();\n            this.shadow = this.attachShadow({ mode: 'open' });\n            this.shadow.innerHTML = '<p>Shadow content</p>';\n        }\n    }\n\n    customElements.define('shadow-element', ShadowElement);\n\n    const el = document.createElement('shadow-element');\n    testing.expectEqual('open', el.shadowRoot.mode);\n    testing.expectEqual('<p>Shadow content</p>', el.shadowRoot.innerHTML);\n}\n</script>\n\n<script id=\"shadow_in_connectedCallback\">\n{\n    let connectedCount = 0;\n\n    class ConnectedShadowElement extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n        }\n\n        connectedCallback() {\n            connectedCount++;\n            this.shadowRoot.innerHTML = `<div>Connected ${connectedCount}</div>`;\n        }\n    }\n\n    customElements.define('connected-shadow-element', ConnectedShadowElement);\n\n    const el = document.createElement('connected-shadow-element');\n    testing.expectEqual('', el.shadowRoot.innerHTML);\n\n    document.body.appendChild(el);\n    testing.expectEqual('<div>Connected 1</div>', el.shadowRoot.innerHTML);\n\n    el.remove();\n    document.body.appendChild(el);\n    testing.expectEqual('<div>Connected 2</div>', el.shadowRoot.innerHTML);\n}\n</script>\n\n<script id=\"closed_shadow\">\n{\n    class ClosedShadowElement extends HTMLElement {\n        constructor() {\n            super();\n            this._shadow = this.attachShadow({ mode: 'closed' });\n            this._shadow.innerHTML = '<span>Private</span>';\n        }\n\n        getContent() {\n            return this._shadow.innerHTML;\n        }\n    }\n\n    customElements.define('closed-shadow-element', ClosedShadowElement);\n\n    const el = document.createElement('closed-shadow-element');\n    testing.expectEqual(null, el.shadowRoot);\n    testing.expectEqual('<span>Private</span>', el.getContent());\n}\n</script>\n\n<script id=\"multiple_custom_elements_with_shadows\">\n{\n    class ComponentA extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = '<div class=\"a\">Component A</div>';\n        }\n    }\n\n    class ComponentB extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = '<div class=\"b\">Component B</div>';\n        }\n    }\n\n    customElements.define('component-a', ComponentA);\n    customElements.define('component-b', ComponentB);\n\n    const a = document.createElement('component-a');\n    const b = document.createElement('component-b');\n\n    testing.expectEqual('Component A', a.shadowRoot.querySelector('.a').textContent);\n    testing.expectEqual('Component B', b.shadowRoot.querySelector('.b').textContent);\n    testing.expectEqual(null, a.shadowRoot.querySelector('.b'));\n    testing.expectEqual(null, b.shadowRoot.querySelector('.a'));\n}\n</script>\n\n<script id=\"nested_custom_elements\">\n{\n    class InnerElement extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n            this.shadowRoot.innerHTML = '<p>Nested</p>';\n        }\n    }\n\n    class OuterElement extends HTMLElement {\n        constructor() {\n            super();\n            this.attachShadow({ mode: 'open' });\n        }\n\n        addInner() {\n            const inner = document.createElement('inner-element');\n            this.shadowRoot.appendChild(inner);\n            return inner;\n        }\n    }\n\n    customElements.define('inner-element', InnerElement);\n    customElements.define('outer-element', OuterElement);\n\n    const outer = document.createElement('outer-element');\n    const inner = outer.addInner();\n\n    testing.expectEqual('INNER-ELEMENT', inner.tagName);\n    testing.expectEqual('<p>Nested</p>', inner.shadowRoot.innerHTML);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/dom_traversal.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"parentNode_shadow_boundary\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"child\">Content</div>';\n\n    document.body.appendChild(host);\n\n    const child = shadow.getElementById('child');\n\n    // In actual browsers, parentNode DOES expose the shadow root\n    testing.expectEqual(shadow, child.parentNode);\n\n    host.remove();\n}\n</script>\n\n<script id=\"parentElement_shadow_boundary\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"child\">Content</div>';\n\n    document.body.appendChild(host);\n\n    const child = shadow.getElementById('child');\n\n    // parentElement should also not expose shadow root\n    testing.expectEqual(null, child.parentElement);\n\n    host.remove();\n}\n</script>\n\n<script id=\"getRootNode\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"child\">Content</div>';\n\n    document.body.appendChild(host);\n\n    const child = shadow.getElementById('child');\n\n    // getRootNode should return the shadow root\n    const root = child.getRootNode();\n    testing.expectEqual(shadow, root);\n\n    // Host's getRootNode should return document\n    const hostRoot = host.getRootNode();\n    testing.expectEqual(document, hostRoot);\n\n    host.remove();\n}\n</script>\n\n<script id=\"contains_crosses_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"child\">Content</div>';\n\n    document.body.appendChild(host);\n\n    const child = shadow.getElementById('child');\n\n    // Shadow root contains its children\n    testing.expectEqual(true, shadow.contains(child));\n\n    // In actual browsers, contains() does NOT cross shadow boundaries\n    testing.expectEqual(false, host.contains(child));\n    testing.expectEqual(false, document.body.contains(child));\n    testing.expectEqual(false, document.contains(child));\n\n    host.remove();\n}\n</script>\n\n<script id=\"closest_shadow_boundary\">\n{\n    const host = document.createElement('div');\n    host.className = 'host-class';\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div class=\"inner\"><p id=\"para\">Text</p></div>';\n\n    document.body.appendChild(host);\n\n    const para = shadow.getElementById('para');\n\n    // closest should find elements within shadow tree\n    const inner = para.closest('.inner');\n    testing.expectEqual('DIV', inner.tagName);\n\n    // closest should NOT cross shadow boundary to find host\n    const foundHost = para.closest('.host-class');\n    testing.expectEqual(null, foundHost);\n\n    host.remove();\n}\n</script>\n\n<script id=\"nextSibling_in_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"first\">First</div><div id=\"second\">Second</div>';\n\n    document.body.appendChild(host);\n\n    const first = shadow.getElementById('first');\n    const second = shadow.getElementById('second');\n\n    // Sibling traversal should work within shadow tree\n    testing.expectEqual(second, first.nextSibling);\n    testing.expectEqual(first, second.previousSibling);\n\n    host.remove();\n}\n</script>\n\n<script id=\"childNodes_of_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div>A</div><div>B</div>';\n\n    document.body.appendChild(host);\n\n    // Shadow root should expose its children\n    const children = shadow.childNodes;\n    testing.expectEqual(2, children.length);\n\n    // Host should have no child nodes (shadow tree is separate)\n    testing.expectEqual(0, host.childNodes.length);\n\n    host.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/dump.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"innerHTML_excludes_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Shadow content</p>';\n\n    document.body.appendChild(host);\n\n    // Per spec, innerHTML does NOT include shadow DOM\n    const html = host.innerHTML;\n    testing.expectEqual('', html);\n\n    host.remove();\n}\n</script>\n\n<script id=\"innerHTML_only_light_dom\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Shadow content with <slot></slot></p>';\n\n    const lightChild = document.createElement('span');\n    lightChild.textContent = 'Light DOM';\n    host.appendChild(lightChild);\n\n    document.body.appendChild(host);\n\n    const html = host.innerHTML;\n    // innerHTML only returns light DOM, not shadow DOM\n    testing.expectEqual(false, html.indexOf('Shadow content') >= 0);\n    testing.expectEqual(true, html.indexOf('Light DOM') >= 0);\n    testing.expectEqual(true, html.indexOf('<span>Light DOM</span>') >= 0);\n\n    host.remove();\n}\n</script>\n\n<script id=\"outerHTML_excludes_shadow\">\n{\n    const host = document.createElement('div');\n    host.id = 'test-host';\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Shadow content</p>';\n\n    document.body.appendChild(host);\n\n    // outerHTML also excludes shadow DOM per spec\n    const html = host.outerHTML;\n    testing.expectEqual(true, html.indexOf('<div id=\"test-host\">') >= 0);\n    testing.expectEqual(false, html.indexOf('Shadow content') >= 0);\n    testing.expectEqual(true, html.indexOf('</div>') >= 0);\n\n    host.remove();\n}\n</script>\n\n<script id=\"innerHTML_closed_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'closed' });\n    shadow.innerHTML = '<p>Closed shadow content</p>';\n\n    document.body.appendChild(host);\n\n    // innerHTML never includes shadow DOM (open or closed)\n    const html = host.innerHTML;\n    testing.expectEqual('', html);\n\n    host.remove();\n}\n</script>\n\n<script id=\"innerHTML_nested_shadow\">\n{\n    const outer = document.createElement('div');\n    const outerShadow = outer.attachShadow({ mode: 'open' });\n    outerShadow.innerHTML = '<div id=\"inner-host\"></div>';\n\n    const innerHost = outerShadow.getElementById('inner-host');\n    const innerShadow = innerHost.attachShadow({ mode: 'open' });\n    innerShadow.innerHTML = '<p>Nested shadow content</p>';\n\n    document.body.appendChild(outer);\n\n    // innerHTML on outer doesn't include its shadow DOM\n    const html = outer.innerHTML;\n    testing.expectEqual('', html);\n\n    outer.remove();\n}\n</script>\n\n<script id=\"shadowRoot_innerHTML\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Para 1</p><p>Para 2</p>';\n\n    document.body.appendChild(host);\n\n    const shadowHtml = shadow.innerHTML;\n    testing.expectEqual(true, shadowHtml.indexOf('Para 1') >= 0);\n    testing.expectEqual(true, shadowHtml.indexOf('Para 2') >= 0);\n\n    host.remove();\n}\n</script>\n\n<script id=\"innerHTML_with_light_dom\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<slot></slot>';\n\n    const lightChild = document.createElement('span');\n    lightChild.textContent = 'Light only';\n    host.appendChild(lightChild);\n\n    document.body.appendChild(host);\n\n    // innerHTML returns light DOM children\n    const html = host.innerHTML;\n    testing.expectEqual(true, html.indexOf('Light only') >= 0);\n    testing.expectEqual(true, html.indexOf('<span>Light only</span>') >= 0);\n\n    host.remove();\n}\n</script>\n\n<script id=\"shadowRoot_innerHTML_direct\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p class=\"test\" data-foo=\"bar\">Shadow text</p>';\n\n    document.body.appendChild(host);\n\n    // Accessing shadow.innerHTML directly DOES return shadow content\n    const html = shadow.innerHTML;\n    testing.expectEqual(true, html.indexOf('class=\"test\"') >= 0);\n    testing.expectEqual(true, html.indexOf('data-foo=\"bar\"') >= 0);\n    testing.expectEqual(true, html.indexOf('Shadow text') >= 0);\n\n    host.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/edge_cases.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body>\n<div id=\"container\"></div>\n</body>\n\n<script id=\"double_attach_error\">\n{\n    const el = document.createElement('div');\n    el.attachShadow({ mode: 'open' });\n\n    let threw = false;\n    try {\n        el.attachShadow({ mode: 'open' });\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n</script>\n\n<script id=\"shadow_isolation_querySelector\">\n{\n    const host = document.createElement('div');\n    host.id = 'test-host';\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p id=\"shadow-para\">In shadow</p>';\n\n    document.body.appendChild(host);\n\n    const fromDocument = document.querySelector('#shadow-para');\n    testing.expectEqual(null, fromDocument);\n\n    const fromShadow = shadow.querySelector('#shadow-para');\n    testing.expectEqual('In shadow', fromShadow.textContent);\n\n    host.remove();\n}\n</script>\n\n<script id=\"shadow_isolation_getElementById\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"shadow-div\">Shadow</div>';\n\n    document.body.appendChild(host);\n\n    const fromDocument = document.getElementById('shadow-div');\n    testing.expectEqual(null, fromDocument);\n\n    const fromShadow = shadow.getElementById('shadow-div');\n    testing.expectEqual('Shadow', fromShadow.textContent);\n\n    host.remove();\n}\n</script>\n\n<script id=\"moving_shadow_host\">\n{\n    const container = $('#container');\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<span>Content</span>';\n\n    container.appendChild(host);\n    testing.expectEqual('<span>Content</span>', shadow.innerHTML);\n    testing.expectEqual(container, host.parentElement);\n\n    document.body.appendChild(host);\n    testing.expectEqual('<span>Content</span>', shadow.innerHTML);\n    testing.expectEqual(document.body, host.parentElement);\n\n    host.remove();\n    testing.expectEqual('<span>Content</span>', shadow.innerHTML);\n    testing.expectEqual(null, host.parentElement);\n}\n</script>\n\n<script id=\"shadow_persists_after_disconnect\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Persistent</p>';\n\n    document.body.appendChild(host);\n    const shadowRef = host.shadowRoot;\n\n    host.remove();\n    testing.expectEqual(shadowRef, host.shadowRoot);\n    testing.expectEqual('<p>Persistent</p>', host.shadowRoot.innerHTML);\n\n    document.body.appendChild(host);\n    testing.expectEqual(shadowRef, host.shadowRoot);\n    testing.expectEqual('<p>Persistent</p>', host.shadowRoot.innerHTML);\n\n    host.remove();\n}\n</script>\n\n<script id=\"invalid_mode\">\n{\n    const el = document.createElement('div');\n    let threw = false;\n    try {\n        el.attachShadow({ mode: 'invalid' });\n    } catch (e) {\n        threw = true;\n    }\n    testing.expectEqual(true, threw);\n}\n</script>\n\n<script id=\"shadow_with_style\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<style>p { color: red; }</style><p>Styled</p>';\n\n    const para = shadow.querySelector('p');\n    testing.expectEqual('Styled', para.textContent);\n\n    const style = shadow.querySelector('style');\n    testing.expectEqual('p { color: red; }', style.textContent);\n}\n</script>\n\n<script id=\"innerHTML_clears_existing\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const p = document.createElement('p');\n    p.textContent = 'First';\n    shadow.appendChild(p);\n    testing.expectEqual(1, shadow.childElementCount);\n\n    shadow.innerHTML = '<span>Second</span>';\n    testing.expectEqual(1, shadow.childElementCount);\n    testing.expectEqual('SPAN', shadow.firstElementChild.tagName);\n    testing.expectEqual('Second', shadow.firstElementChild.textContent);\n}\n</script>\n\n<script id=\"empty_innerHTML\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Content</p>';\n\n    testing.expectEqual(1, shadow.childElementCount);\n\n    shadow.innerHTML = '';\n    testing.expectEqual(0, shadow.childElementCount);\n    testing.expectEqual('', shadow.innerHTML);\n}\n</script>\n\n<script id=\"querySelectorAll_isolation\">\n{\n    const host1 = document.createElement('div');\n    const shadow1 = host1.attachShadow({ mode: 'open' });\n    shadow1.innerHTML = '<p class=\"test\">Shadow 1</p>';\n\n    const host2 = document.createElement('div');\n    const shadow2 = host2.attachShadow({ mode: 'open' });\n    shadow2.innerHTML = '<p class=\"test\">Shadow 2</p>';\n\n    document.body.appendChild(host1);\n    document.body.appendChild(host2);\n\n    const fromDoc = document.querySelectorAll('.test');\n    testing.expectEqual(0, fromDoc.length);\n\n    const fromShadow1 = shadow1.querySelectorAll('.test');\n    testing.expectEqual(1, fromShadow1.length);\n    testing.expectEqual('Shadow 1', fromShadow1[0].textContent);\n\n    const fromShadow2 = shadow2.querySelectorAll('.test');\n    testing.expectEqual(1, fromShadow2.length);\n    testing.expectEqual('Shadow 2', fromShadow2[0].textContent);\n\n    host1.remove();\n    host2.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/events.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"event_bubbling_through_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<button id=\"btn\">Click</button>';\n    document.body.appendChild(host);\n\n    const button = shadow.getElementById('btn');\n\n    let insideCalled = false;\n    let shadowCalled = false;\n    let hostCalled = false;\n    let documentCalled = false;\n\n    let insideTarget = null;\n    let hostTarget = null;\n\n    button.addEventListener('click', (e) => {\n        insideCalled = true;\n        insideTarget = e.target;\n    });\n\n    shadow.addEventListener('click', (e) => {\n        shadowCalled = true;\n    });\n\n    host.addEventListener('click', (e) => {\n        hostCalled = true;\n        hostTarget = e.target;\n    });\n\n    document.addEventListener('click', (e) => {\n        documentCalled = true;\n    });\n\n    const event = new Event('click', { bubbles: true });\n    button.dispatchEvent(event);\n\n    testing.expectEqual(true, insideCalled);\n    testing.expectEqual(true, shadowCalled);\n\n    // Without composed:true, event should NOT escape shadow tree\n    testing.expectEqual(false, hostCalled);\n    testing.expectEqual(false, documentCalled);\n\n    host.remove();\n}\n</script>\n\n<script id=\"event_composed_escapes_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<button id=\"btn\">Click</button>';\n    document.body.appendChild(host);\n\n    const button = shadow.getElementById('btn');\n\n    let hostCalled = false;\n    let hostTarget = null;\n\n    host.addEventListener('click', (e) => {\n        hostCalled = true;\n        hostTarget = e.target;\n    });\n\n    // With composed:true, event SHOULD escape shadow tree\n    const event = new Event('click', { bubbles: true, composed: true });\n    button.dispatchEvent(event);\n\n    testing.expectEqual(true, hostCalled);\n\n    // Event target should be retargeted to host when outside shadow tree\n    testing.expectEqual(host, hostTarget);\n\n    host.remove();\n}\n</script>\n\n<script id=\"composedPath_basic\">\n{\n    const div1 = document.createElement('div');\n    const div2 = document.createElement('div');\n    const button = document.createElement('button');\n\n    div1.appendChild(div2);\n    div2.appendChild(button);\n    document.body.appendChild(div1);\n\n    let capturedPath = null;\n\n    button.addEventListener('click', (e) => {\n        capturedPath = e.composedPath();\n    });\n\n    const event = new Event('click', { bubbles: true });\n    button.dispatchEvent(event);\n\n    testing.expectEqual(7, capturedPath.length);\n    testing.expectEqual(button, capturedPath[0]);\n    testing.expectEqual(div2, capturedPath[1]);\n    testing.expectEqual(div1, capturedPath[2]);\n    testing.expectEqual(document.body, capturedPath[3]);\n    testing.expectEqual(document.documentElement, capturedPath[4]);\n    testing.expectEqual(document, capturedPath[5]);\n    testing.expectEqual(window, capturedPath[6]);\n\n    div1.remove();\n}\n</script>\n\n<script id=\"composedPath_after_dispatch\">\n{\n    const button = document.createElement('button');\n    document.body.appendChild(button);\n\n    const event = new Event('click', { bubbles: true });\n    button.dispatchEvent(event);\n\n    const path = event.composedPath();\n    testing.expectEqual(0, path.length);\n\n    button.remove();\n}\n</script>\n\n<script id=\"composedPath_with_shadow_not_composed\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    const button = document.createElement('button');\n\n    shadow.appendChild(button);\n    document.body.appendChild(host);\n\n    let capturedPath = null;\n\n    button.addEventListener('click', (e) => {\n        capturedPath = e.composedPath();\n    });\n\n    const event = new Event('click', { bubbles: true });\n    button.dispatchEvent(event);\n\n    testing.expectEqual(2, capturedPath.length);\n    testing.expectEqual(button, capturedPath[0]);\n    testing.expectEqual(shadow, capturedPath[1]);\n\n    host.remove();\n}\n</script>\n\n<script id=\"composedPath_with_shadow_composed\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    const button = document.createElement('button');\n\n    shadow.appendChild(button);\n    document.body.appendChild(host);\n\n    let capturedPath = null;\n\n    button.addEventListener('click', (e) => {\n        capturedPath = e.composedPath();\n    });\n\n    const event = new Event('click', { bubbles: true, composed: true });\n    button.dispatchEvent(event);\n\n    testing.expectEqual(7, capturedPath.length);\n    testing.expectEqual(button, capturedPath[0]);\n    testing.expectEqual(shadow, capturedPath[1]);\n    testing.expectEqual(host, capturedPath[2]);\n    testing.expectEqual(document.body, capturedPath[3]);\n    testing.expectEqual(document.documentElement, capturedPath[4]);\n    testing.expectEqual(document, capturedPath[5]);\n    testing.expectEqual(window, capturedPath[6]);\n\n    host.remove();\n}\n</script>\n\n<script id=\"composedPath_open_shadow_from_host\">\n{\n    // Test that open shadow root exposes internals in composedPath\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    const button = document.createElement('button');\n\n    shadow.appendChild(button);\n    document.body.appendChild(host);\n\n    let capturedPath = null;\n\n    host.addEventListener('click', (e) => {\n        capturedPath = e.composedPath();\n    });\n\n    const event = new Event('click', { bubbles: true, composed: true });\n    button.dispatchEvent(event);\n\n    // Open shadow: external listener should see shadow internals\n    testing.expectEqual(7, capturedPath.length);\n    testing.expectEqual(button, capturedPath[0]);\n    testing.expectEqual(shadow, capturedPath[1]);\n    testing.expectEqual(host, capturedPath[2]);\n    testing.expectEqual(document.body, capturedPath[3]);\n    testing.expectEqual(document.documentElement, capturedPath[4]);\n    testing.expectEqual(document, capturedPath[5]);\n    testing.expectEqual(window, capturedPath[6]);\n\n    host.remove();\n}\n</script>\n\n<script id=\"composedPath_closed_shadow_from_host\">\n{\n    // Test that closed shadow root hides internals in composedPath\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'closed' });\n    const button = document.createElement('button');\n\n    shadow.appendChild(button);\n    document.body.appendChild(host);\n\n    let capturedPath = null;\n\n    host.addEventListener('click', (e) => {\n        capturedPath = e.composedPath();\n    });\n\n    const event = new Event('click', { bubbles: true, composed: true });\n    button.dispatchEvent(event);\n\n    // Closed shadow: external listener should NOT see shadow internals\n    testing.expectEqual(5, capturedPath.length);\n    testing.expectEqual(host, capturedPath[0]);\n    testing.expectEqual(document.body, capturedPath[1]);\n    testing.expectEqual(document.documentElement, capturedPath[2]);\n    testing.expectEqual(document, capturedPath[3]);\n    testing.expectEqual(window, capturedPath[4]);\n\n    host.remove();\n}\n</script>\n\n<script id=\"composedPath_closed_shadow_from_inside\">\n{\n    // Test that closed shadow root still exposes internals to internal listeners\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'closed' });\n    const button = document.createElement('button');\n\n    shadow.appendChild(button);\n    document.body.appendChild(host);\n\n    let capturedPath = null;\n\n    button.addEventListener('click', (e) => {\n        capturedPath = e.composedPath();\n    });\n\n    const event = new Event('click', { bubbles: true, composed: true });\n    button.dispatchEvent(event);\n\n    // Inside the shadow: should see full path including shadow internals\n    testing.expectEqual(7, capturedPath.length);\n    testing.expectEqual(button, capturedPath[0]);\n    testing.expectEqual(shadow, capturedPath[1]);\n    testing.expectEqual(host, capturedPath[2]);\n    testing.expectEqual(document.body, capturedPath[3]);\n    testing.expectEqual(document.documentElement, capturedPath[4]);\n    testing.expectEqual(document, capturedPath[5]);\n    testing.expectEqual(window, capturedPath[6]);\n\n    host.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/id_collision.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body>\n<div id=\"collision-test\">Document</div>\n</body>\n\n<script id=\"id_collision_document_first\">\n{\n    // Document tree element exists with id=\"collision-test\"\n    // Create shadow tree with same ID\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"collision-test\">Shadow</div>';\n\n    document.body.appendChild(host);\n\n    // document.getElementById should find the document-tree element\n    const fromDoc = document.getElementById('collision-test');\n    testing.expectEqual('Document', fromDoc.textContent);\n\n    // shadow.getElementById should find the shadow-tree element\n    const fromShadow = shadow.getElementById('collision-test');\n    testing.expectEqual('Shadow', fromShadow.textContent);\n\n    host.remove();\n}\n</script>\n\n<script id=\"id_collision_shadow_first\">\n{\n    // Create shadow tree with ID first\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"unique-id\">Shadow</div>';\n\n    document.body.appendChild(host);\n\n    // Now add document element with same ID\n    const docEl = document.createElement('div');\n    docEl.id = 'unique-id';\n    docEl.textContent = 'Document';\n    document.body.appendChild(docEl);\n\n    // document.getElementById should find the document-tree element\n    const fromDoc = document.getElementById('unique-id');\n    testing.expectEqual('Document', fromDoc.textContent);\n\n    // shadow.getElementById should find the shadow-tree element\n    const fromShadow = shadow.getElementById('unique-id');\n    testing.expectEqual('Shadow', fromShadow.textContent);\n\n    host.remove();\n    docEl.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/id_management.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"set_id_after_append\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const div = document.createElement('div');\n    div.textContent = 'Content';\n    shadow.appendChild(div);\n\n    document.body.appendChild(host);\n\n    // Set ID after element is in connected shadow tree\n    div.id = 'dynamic-id';\n\n    const found = shadow.getElementById('dynamic-id');\n    testing.expectEqual('Content', found.textContent);\n\n    const fromDoc = document.getElementById('dynamic-id');\n    testing.expectEqual(null, fromDoc);\n\n    host.remove();\n}\n</script>\n\n<script id=\"change_id\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"old-id\">Text</div>';\n\n    document.body.appendChild(host);\n\n    const el = shadow.getElementById('old-id');\n    testing.expectEqual('Text', el.textContent);\n\n    // Change the ID\n    el.id = 'new-id';\n\n    testing.expectEqual(null, shadow.getElementById('old-id'));\n\n    const found = shadow.getElementById('new-id');\n    testing.expectEqual('Text', found.textContent);\n\n    host.remove();\n}\n</script>\n\n<script id=\"remove_id\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"removable\">Text</div>';\n\n    document.body.appendChild(host);\n\n    testing.expectEqual('Text', shadow.getElementById('removable').textContent);\n\n    const el = shadow.getElementById('removable');\n    el.removeAttribute('id');\n\n    testing.expectEqual(null, shadow.getElementById('removable'));\n\n    host.remove();\n}\n</script>\n\n<script id=\"multiple_shadow_roots_same_id\">\n{\n    // Create three shadow roots with same ID\n    const host1 = document.createElement('div');\n    const shadow1 = host1.attachShadow({ mode: 'open' });\n    shadow1.innerHTML = '<div id=\"shared\">Shadow 1</div>';\n\n    const host2 = document.createElement('div');\n    const shadow2 = host2.attachShadow({ mode: 'open' });\n    shadow2.innerHTML = '<div id=\"shared\">Shadow 2</div>';\n\n    const host3 = document.createElement('div');\n    const shadow3 = host3.attachShadow({ mode: 'open' });\n    shadow3.innerHTML = '<div id=\"shared\">Shadow 3</div>';\n\n    document.body.appendChild(host1);\n    document.body.appendChild(host2);\n    document.body.appendChild(host3);\n\n    // Each shadow root should find its own element\n    testing.expectEqual('Shadow 1', shadow1.getElementById('shared').textContent);\n    testing.expectEqual('Shadow 2', shadow2.getElementById('shared').textContent);\n    testing.expectEqual('Shadow 3', shadow3.getElementById('shared').textContent);\n\n    // querySelector should also be isolated\n    testing.expectEqual('Shadow 1', shadow1.querySelector('#shared').textContent);\n    testing.expectEqual('Shadow 2', shadow2.querySelector('#shared').textContent);\n    testing.expectEqual('Shadow 3', shadow3.querySelector('#shared').textContent);\n\n    // Document should not find any of them\n    testing.expectEqual(null, document.getElementById('shared'));\n\n    host1.remove();\n    host2.remove();\n    host3.remove();\n}\n</script>\n\n<script id=\"multiple_shadows_with_document_collision\">\n{\n    // Create document element with ID\n    const docEl = document.createElement('div');\n    docEl.id = 'collision';\n    docEl.textContent = 'Document';\n    document.body.appendChild(docEl);\n\n    // Create two shadow roots with same ID\n    const host1 = document.createElement('div');\n    const shadow1 = host1.attachShadow({ mode: 'open' });\n    shadow1.innerHTML = '<div id=\"collision\">Shadow 1</div>';\n\n    const host2 = document.createElement('div');\n    const shadow2 = host2.attachShadow({ mode: 'open' });\n    shadow2.innerHTML = '<div id=\"collision\">Shadow 2</div>';\n\n    document.body.appendChild(host1);\n    document.body.appendChild(host2);\n\n    // Document should find document element\n    testing.expectEqual('Document', document.getElementById('collision').textContent);\n    testing.expectEqual('Document', document.querySelector('#collision').textContent);\n\n    // Each shadow should find its own\n    testing.expectEqual('Shadow 1', shadow1.getElementById('collision').textContent);\n    testing.expectEqual('Shadow 1', shadow1.querySelector('#collision').textContent);\n\n    testing.expectEqual('Shadow 2', shadow2.getElementById('collision').textContent);\n    testing.expectEqual('Shadow 2', shadow2.querySelector('#collision').textContent);\n\n    docEl.remove();\n    host1.remove();\n    host2.remove();\n}\n</script>\n\n<script id=\"set_id_before_connected\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n\n    const div = document.createElement('div');\n    div.id = 'early-id';\n    div.textContent = 'Content';\n    shadow.appendChild(div);\n\n    // Should work even before host is connected\n    testing.expectEqual('Content', shadow.getElementById('early-id').textContent);\n\n    document.body.appendChild(host);\n\n    // Should still work after connected\n    testing.expectEqual('Content', shadow.getElementById('early-id').textContent);\n    testing.expectEqual(null, document.getElementById('early-id'));\n\n    host.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/innerHTML_spec.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"innerHTML_spec_compliance\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Before <slot></slot> After</p>';\n\n    const slotted = document.createElement('span');\n    slotted.textContent = 'SLOTTED';\n    host.appendChild(slotted);\n\n    document.body.appendChild(host);\n\n    // host.innerHTML returns only light DOM (spec compliant)\n    const hostHTML = host.innerHTML;\n    testing.expectEqual(true, hostHTML.indexOf('SLOTTED') >= 0);\n    testing.expectEqual(false, hostHTML.indexOf('Before') >= 0);\n    testing.expectEqual(false, hostHTML.indexOf('<slot>') >= 0);\n\n    // shadow.innerHTML returns shadow DOM content\n    const shadowHTML = shadow.innerHTML;\n    testing.expectEqual(true, shadowHTML.indexOf('Before') >= 0);\n    testing.expectEqual(true, shadowHTML.indexOf('<slot>') >= 0);\n    testing.expectEqual(false, shadowHTML.indexOf('SLOTTED') >= 0);\n\n    host.remove();\n}\n</script>\n\n<script id=\"innerHTML_named_slots\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<slot name=\"header\"></slot><slot name=\"footer\"></slot>';\n\n    const header = document.createElement('h1');\n    header.setAttribute('slot', 'header');\n    header.textContent = 'Header Content';\n    host.appendChild(header);\n\n    const footer = document.createElement('p');\n    footer.setAttribute('slot', 'footer');\n    footer.textContent = 'Footer Content';\n    host.appendChild(footer);\n\n    document.body.appendChild(host);\n\n    // host.innerHTML returns light DOM with slot attributes\n    const hostHTML = host.innerHTML;\n    testing.expectEqual(true, hostHTML.indexOf('Header Content') >= 0);\n    testing.expectEqual(true, hostHTML.indexOf('Footer Content') >= 0);\n    testing.expectEqual(true, hostHTML.indexOf('slot=\"header\"') >= 0);\n    testing.expectEqual(true, hostHTML.indexOf('slot=\"footer\"') >= 0);\n\n    // shadow.innerHTML returns slot elements\n    const shadowHTML = shadow.innerHTML;\n    testing.expectEqual(true, shadowHTML.indexOf('<slot name=\"header\"></slot>') >= 0);\n    testing.expectEqual(true, shadowHTML.indexOf('<slot name=\"footer\"></slot>') >= 0);\n    testing.expectEqual(false, shadowHTML.indexOf('Header Content') >= 0);\n\n    host.remove();\n}\n</script>\n\n<script id=\"innerHTML_no_shadow\">\n{\n    const host = document.createElement('div');\n    const child = document.createElement('span');\n    child.textContent = 'Regular content';\n    host.appendChild(child);\n\n    document.body.appendChild(host);\n\n    // Without shadow DOM, innerHTML works normally\n    const html = host.innerHTML;\n    testing.expectEqual(true, html.indexOf('Regular content') >= 0);\n    testing.expectEqual(true, html.indexOf('<span>') >= 0);\n\n    host.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/shadowroot/scoping.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<body></body>\n\n<script id=\"ownerDocument_in_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"child\">Content</div>';\n\n    document.body.appendChild(host);\n\n    const child = shadow.getElementById('child');\n\n    // Elements in shadow tree should still reference the document\n    testing.expectEqual(document, child.ownerDocument);\n\n    host.remove();\n}\n</script>\n\n<script id=\"host_children_vs_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Shadow content</p>';\n\n    // Try to add regular children to host (light DOM)\n    const lightChild = document.createElement('div');\n    lightChild.textContent = 'Light DOM';\n    host.appendChild(lightChild);\n\n    document.body.appendChild(host);\n\n    // Host should have light DOM children\n    testing.expectEqual(1, host.children.length);\n    testing.expectEqual('Light DOM', host.children[0].textContent);\n\n    // Shadow should have shadow DOM children\n    testing.expectEqual(1, shadow.children.length);\n    testing.expectEqual('Shadow content', shadow.children[0].textContent);\n\n    // querySelector on host should NOT find shadow children\n    testing.expectEqual(null, host.querySelector('p'));\n\n    // querySelector on shadow should NOT find light children\n    testing.expectEqual(null, shadow.querySelector('div'));\n\n    host.remove();\n}\n</script>\n\n<script id=\"cloneNode_shadow\">\n{\n    const host = document.createElement('div');\n    host.id = 'original-host';\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<p>Shadow content</p>';\n\n    document.body.appendChild(host);\n\n    // Clone the host element\n    const clone = host.cloneNode(true);\n\n    // Per spec, cloneNode should NOT clone the shadow root\n    testing.expectEqual(null, clone.shadowRoot);\n    testing.expectEqual(0, clone.children.length);\n\n    host.remove();\n}\n</script>\n\n<script id=\"compareDocumentPosition_shadow\">\n{\n    const host = document.createElement('div');\n    const shadow = host.attachShadow({ mode: 'open' });\n    shadow.innerHTML = '<div id=\"a\">A</div><div id=\"b\">B</div>';\n\n    document.body.appendChild(host);\n\n    const a = shadow.getElementById('a');\n    const b = shadow.getElementById('b');\n\n    // Elements within same shadow tree should have document position relationship\n    const pos = a.compareDocumentPosition(b);\n\n    // DOCUMENT_POSITION_FOLLOWING = 4\n    testing.expectEqual(4, pos);\n\n    host.remove();\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/storage.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<script id=local_storage>\n  testing.expectEqual(localStorage, window.localStorage);\n  testing.expectEqual('object', typeof localStorage);\n\n  testing.expectEqual(0, localStorage.length);\n\n  localStorage.setItem('key1', 'value1');\n  testing.expectEqual('value1', localStorage.getItem('key1'));\n  testing.expectEqual(1, localStorage.length);\n\n  localStorage.setItem('key2', 'value2');\n  localStorage.setItem('key3', 'value3');\n  testing.expectEqual('value2', localStorage.getItem('key2'));\n  testing.expectEqual('value3', localStorage.getItem('key3'));\n  testing.expectEqual(3, localStorage.length);\n\n  localStorage.setItem('key1', 'updated_value1');\n  testing.expectEqual('updated_value1', localStorage.getItem('key1'));\n  testing.expectEqual(3, localStorage.length);\n\n  testing.expectEqual(null, localStorage.getItem('nonexistent'));\n\n  const keys = [];\n  for (let i = 0; i < localStorage.length; i++) {\n    keys.push(localStorage.key(i));\n  }\n  testing.expectEqual(true, keys.includes('key1'));\n  testing.expectEqual(true, keys.includes('key2'));\n  testing.expectEqual(true, keys.includes('key3'));\n\n  testing.expectEqual(null, localStorage.key(999));\n\n  localStorage.removeItem('key2');\n  testing.expectEqual(null, localStorage.getItem('key2'));\n  testing.expectEqual(2, localStorage.length);\n\n  localStorage.removeItem('nonexistent');\n  testing.expectEqual(2, localStorage.length);\n\n  localStorage.clear();\n  testing.expectEqual(0, localStorage.length);\n  testing.expectEqual(null, localStorage.getItem('key1'));\n  testing.expectEqual(null, localStorage.getItem('key3'));\n\n  localStorage.setItem('empty', '');\n  testing.expectEqual('', localStorage.getItem('empty'));\n  testing.expectEqual(1, localStorage.length);\n\n  localStorage.setItem('number', '123');\n  testing.expectEqual('123', localStorage.getItem('number'));\n\n  localStorage.setItem(null, 'null_key_value');\n  testing.expectEqual(null, localStorage.getItem(null));\n\n  localStorage.setItem(undefined, 'undefined_key_value');\n  testing.expectEqual(null, localStorage.getItem(undefined));\n\n  localStorage.clear();\n  testing.expectEqual(0, localStorage.length);\n</script>\n\n<script id=\"legacy\">\n  localStorage.clear();\n  testing.expectEqual(0, localStorage.length);\n  testing.expectEqual(null, localStorage.getItem('foo'));\n  testing.expectEqual(null, localStorage.key(0));\n\n  localStorage.setItem('foo', 'bar');\n  testing.expectEqual(1, localStorage.length)\n  testing.expectEqual('bar', localStorage.getItem('foo'));\n  testing.expectEqual('foo', localStorage.key(0));\n  testing.expectEqual(null, localStorage.key(1));\n\n  localStorage.removeItem('foo');\n  testing.expectEqual(0, localStorage.length)\n  testing.expectEqual(null, localStorage.getItem('foo'));\n\n  localStorage['foo'] = 'bar';\n  testing.expectEqual(1, localStorage.length);\n  testing.expectEqual('bar', localStorage['foo']);\n\n  localStorage.setItem('a', '1');\n  localStorage.setItem('b', '2');\n  localStorage.setItem('c', '3');\n  testing.expectEqual(4, localStorage.length)\n  localStorage.clear();\n  testing.expectEqual(0, localStorage.length)\n</script>\n\n<script id=\"localstorage_limits\">\n  localStorage.clear();\n  for (i = 0; i < 5; i++) {\n    const v =  \"v\".repeat(1024 * 1024);\n    localStorage.setItem(v, v);\n  }\n  testing.expectError(\"QuotaExceededError\", () => localStorage.setItem(\"last\", \"v\"));\n</script>\n"
  },
  {
    "path": "src/browser/tests/streams/readable_stream.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=readable_stream_basic>\n  {\n    // Test basic stream creation\n    const stream = new ReadableStream();\n    testing.expectEqual('object', typeof stream);\n    testing.expectEqual('function', typeof stream.getReader);\n  }\n</script>\n\n<script id=readable_stream_reader>\n  {\n    // Test getting a reader\n    const stream = new ReadableStream();\n    const reader = stream.getReader();\n    testing.expectEqual('object', typeof reader);\n    testing.expectEqual('function', typeof reader.read);\n    testing.expectEqual('function', typeof reader.releaseLock);\n    testing.expectEqual('function', typeof reader.cancel);\n  }\n</script>\n\n<script id=response_body>\n  (async function() {\n    const response = new Response('hello world');\n    testing.expectEqual('object', typeof response.body);\n\n    const reader = response.body.getReader();\n    const result = await reader.read();\n\n    testing.expectEqual(false, result.done);\n    testing.expectEqual(true, result.value instanceof Uint8Array);\n\n    // Convert Uint8Array to string\n    const decoder = new TextDecoder();\n    const text = decoder.decode(result.value);\n    testing.expectEqual('hello world', text);\n\n    // Next read should be done\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n\n<script id=response_text>\n  (async function() {\n    const response = new Response('hello from text()');\n    const text = await response.text();\n    testing.expectEqual('hello from text()', text);\n  })();\n</script>\n\n<script id=response_json>\n  (async function() {\n    const response = new Response('{\"foo\":\"bar\",\"num\":42}');\n    const json = await response.json();\n    testing.expectEqual('object', typeof json);\n    testing.expectEqual('bar', json.foo);\n    testing.expectEqual(42, json.num);\n  })();\n</script>\n\n<script id=response_null_body>\n  {\n    // Response with no body should have null body\n    const response = new Response();\n    testing.expectEqual(null, response.body);\n  }\n</script>\n\n<script id=response_empty_string_body>\n  (async function() {\n    // Response with empty string should have a stream that's immediately closed\n    const response = new Response('');\n    testing.expectEqual('object', typeof response.body);\n\n    const reader = response.body.getReader();\n    const result = await reader.read();\n\n    // Stream should be closed immediately (done: true, no value)\n    testing.expectEqual(true, result.done);\n  })();\n</script>\n\n<script id=response_status>\n  (async function() {\n    const response = new Response('test', { status: 404 });\n    testing.expectEqual(404, response.status);\n    testing.expectEqual(false, response.ok);\n\n    const text = await response.text();\n    testing.expectEqual('test', text);\n  })();\n</script>\n\n<script id=async_iterator_exists>\n  (async function() {\n    const stream = new ReadableStream();\n    testing.expectEqual('function', typeof stream[Symbol.asyncIterator]);\n  })();\n</script>\n\n<script id=async_iterator_basic>\n  (async function() {\n    const stream = new ReadableStream();\n    const iterator = stream[Symbol.asyncIterator]();\n    testing.expectEqual('object', typeof iterator);\n    testing.expectEqual('function', typeof iterator.next);\n  })();\n</script>\n\n<script id=async_iterator_for_await>\n  (async function() {\n    const response = new Response('test data');\n    const stream = response.body;\n\n    const chunks = [];\n    for await (const chunk of stream) {\n      chunks.push(chunk);\n    }\n\n    testing.expectEqual(1, chunks.length);\n    testing.expectEqual(true, chunks[0] instanceof Uint8Array);\n\n    const decoder = new TextDecoder();\n    const text = decoder.decode(chunks[0]);\n    testing.expectEqual('test data', text);\n  })();\n</script>\n\n<script id=async_iterator_locks_stream>\n  (async function() {\n    const response = new Response('test');\n    const stream = response.body;\n\n    // Get async iterator (locks stream)\n    const iterator = stream[Symbol.asyncIterator]();\n\n    // Try to get reader - should fail\n    let errorThrown = false;\n    try {\n      stream.getReader();\n    } catch (e) {\n      errorThrown = true;\n    }\n    testing.expectEqual(true, errorThrown);\n  })();\n</script>\n\n<script id=async_iterator_manual_next>\n  (async function() {\n    const response = new Response('hello');\n    const stream = response.body;\n    const iterator = stream[Symbol.asyncIterator]();\n\n    const result = await iterator.next();\n    testing.expectEqual('object', typeof result);\n    testing.expectEqual(false, result.done);\n    testing.expectEqual(true, result.value instanceof Uint8Array);\n\n    // Second call should be done\n    const result2 = await iterator.next();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n\n<script id=async_iterator_early_break>\n  (async function() {\n    const response = new Response('test data');\n    const stream = response.body;\n\n    for await (const chunk of stream) {\n      break; // Early exit\n    }\n\n    // Should not throw errors\n    testing.expectEqual('object', typeof stream);\n  })();\n</script>\n\n<script id=readable_stream_locked>\n  const stream1 = new ReadableStream({\n    start(controller) {\n      controller.enqueue(\"data\");\n      controller.close();\n    }\n  });\n\n  testing.expectEqual(false, stream1.locked);\n  const reader1 = stream1.getReader();\n  testing.expectEqual(true, stream1.locked);\n  reader1.releaseLock();\n  testing.expectEqual(false, stream1.locked);\n</script>\n\n<script id=readable_stream_cancel>\n  (async function() {\n    var cancelCalled = false;\n    var cancelReason = null;\n\n    const stream2 = new ReadableStream({\n      start(controller) {\n        controller.enqueue(\"data1\");\n        controller.enqueue(\"data2\");\n      },\n      cancel(reason) {\n        cancelCalled = true;\n        cancelReason = reason;\n      }\n    });\n\n    const result = await stream2.cancel(\"user requested\");\n    testing.expectEqual(undefined, result);\n    testing.expectEqual(true, cancelCalled);\n    testing.expectEqual(\"user requested\", cancelReason);\n  })();\n</script>\n\n<script id=readable_stream_pull>\n  (async function() {\n    var pullCount = 0;\n\n    const stream3 = new ReadableStream({\n      start(controller) {\n        controller.enqueue(\"initial\");\n      },\n      pull(controller) {\n        pullCount++;\n        if (pullCount <= 2) {\n          controller.enqueue(\"pulled\" + pullCount);\n        }\n        if (pullCount === 2) {\n          controller.close();\n        }\n      }\n    }, { highWaterMark: 1 });\n\n    const reader3 = stream3.getReader();\n\n    const data1 = await reader3.read();\n    testing.expectEqual(\"initial\", data1.value);\n    testing.expectEqual(false, data1.done);\n\n    const data2 = await reader3.read();\n    testing.expectEqual(\"pulled1\", data2.value);\n    testing.expectEqual(false, data2.done);\n  })();\n</script>\n\n<script id=readable_stream_desired_size>\n  var desiredSizes = [];\n\n  const stream4 = new ReadableStream({\n    start(controller) {\n      desiredSizes.push(controller.desiredSize);\n      controller.enqueue(\"a\");\n      desiredSizes.push(controller.desiredSize);\n      controller.enqueue(\"b\");\n      desiredSizes.push(controller.desiredSize);\n    }\n  }, { highWaterMark: 2 });\n\n  testing.expectEqual(2, desiredSizes[0]);\n  testing.expectEqual(1, desiredSizes[1]);\n  testing.expectEqual(0, desiredSizes[2]);\n</script>\n\n<script id=readable_stream_start_with_pull>\n  (async function() {\n    var pullCount = 0;\n\n    const stream5 = new ReadableStream({\n      start(controller) {\n        controller.enqueue(\"start1\");\n        controller.enqueue(\"start2\");\n      },\n      pull(controller) {\n        pullCount++;\n        if (pullCount === 1) {\n          controller.enqueue(\"pull1\");\n        } else if (pullCount === 2) {\n          controller.enqueue(\"pull2\");\n          controller.close();\n        }\n      }\n    }, { highWaterMark: 2 });\n\n    const reader5 = stream5.getReader();\n\n    const data1 = await reader5.read();\n    testing.expectEqual(\"start1\", data1.value);\n    testing.expectEqual(false, data1.done);\n\n    const data2 = await reader5.read();\n    testing.expectEqual(\"start2\", data2.value);\n    testing.expectEqual(false, data2.done);\n\n    const data3 = await reader5.read();\n    testing.expectEqual(\"pull1\", data3.value);\n    testing.expectEqual(false, data3.done);\n  })();\n</script>\n\n<script id=enqueue_preserves_number>\n  (async function() {\n    const stream = new ReadableStream({\n      start(controller) {\n        controller.enqueue(42);\n        controller.enqueue(0);\n        controller.enqueue(3.14);\n        controller.close();\n      }\n    });\n\n    const reader = stream.getReader();\n\n    const r1 = await reader.read();\n    testing.expectEqual(false, r1.done);\n    testing.expectEqual('number', typeof r1.value);\n    testing.expectEqual(42, r1.value);\n\n    const r2 = await reader.read();\n    testing.expectEqual('number', typeof r2.value);\n    testing.expectEqual(0, r2.value);\n\n    const r3 = await reader.read();\n    testing.expectEqual('number', typeof r3.value);\n    testing.expectEqual(3.14, r3.value);\n\n    const r4 = await reader.read();\n    testing.expectEqual(true, r4.done);\n  })();\n</script>\n\n<script id=enqueue_preserves_bool>\n  (async function() {\n    const stream = new ReadableStream({\n      start(controller) {\n        controller.enqueue(true);\n        controller.enqueue(false);\n        controller.close();\n      }\n    });\n\n    const reader = stream.getReader();\n\n    const r1 = await reader.read();\n    testing.expectEqual('boolean', typeof r1.value);\n    testing.expectEqual(true, r1.value);\n\n    const r2 = await reader.read();\n    testing.expectEqual('boolean', typeof r2.value);\n    testing.expectEqual(false, r2.value);\n  })();\n</script>\n\n<script id=enqueue_preserves_object>\n  (async function() {\n    const stream = new ReadableStream({\n      start(controller) {\n        controller.enqueue({ key: 'value', num: 7 });\n        controller.close();\n      }\n    });\n\n    const reader = stream.getReader();\n\n    const r1 = await reader.read();\n    testing.expectEqual('object', typeof r1.value);\n    testing.expectEqual('value', r1.value.key);\n    testing.expectEqual(7, r1.value.num);\n  })();\n</script>\n"
  },
  {
    "path": "src/browser/tests/streams/text_decoder_stream.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=text_decoder_stream_encoding>\n  {\n    const tds = new TextDecoderStream();\n    testing.expectEqual('utf-8', tds.encoding);\n    testing.expectEqual('object', typeof tds.readable);\n    testing.expectEqual('object', typeof tds.writable);\n    testing.expectEqual(false, tds.fatal);\n    testing.expectEqual(false, tds.ignoreBOM);\n  }\n</script>\n\n<script id=text_decoder_stream_with_label>\n  {\n    const tds = new TextDecoderStream('utf-8');\n    testing.expectEqual('utf-8', tds.encoding);\n  }\n</script>\n\n<script id=text_decoder_stream_with_opts>\n  {\n    const tds = new TextDecoderStream('utf-8', { fatal: true, ignoreBOM: true });\n    testing.expectEqual(true, tds.fatal);\n    testing.expectEqual(true, tds.ignoreBOM);\n  }\n</script>\n\n<script id=text_decoder_stream_invalid_label>\n  {\n    let errorThrown = false;\n    try {\n      new TextDecoderStream('windows-1252');\n    } catch (e) {\n      errorThrown = true;\n    }\n    testing.expectEqual(true, errorThrown);\n  }\n</script>\n\n<script id=text_decoder_stream_decode>\n  (async function() {\n    const tds = new TextDecoderStream();\n\n    const writer = tds.writable.getWriter();\n    const reader = tds.readable.getReader();\n\n    // 'hello' in UTF-8 bytes\n    const bytes = new Uint8Array([104, 101, 108, 108, 111]);\n    await writer.write(bytes);\n    await writer.close();\n\n    const result = await reader.read();\n    testing.expectEqual(false, result.done);\n    testing.expectEqual('hello', result.value);\n\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n\n<script id=text_decoder_stream_empty_chunk>\n  (async function() {\n    const tds = new TextDecoderStream();\n    const writer = tds.writable.getWriter();\n    const reader = tds.readable.getReader();\n\n    // Write an empty chunk followed by real data\n    await writer.write(new Uint8Array([]));\n    await writer.write(new Uint8Array([104, 105]));\n    await writer.close();\n\n    // Empty chunk should be filtered out; first read gets \"hi\"\n    const result = await reader.read();\n    testing.expectEqual(false, result.done);\n    testing.expectEqual('hi', result.value);\n\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n"
  },
  {
    "path": "src/browser/tests/streams/transform_stream.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=transform_stream_basic>\n  {\n    const ts = new TransformStream();\n    testing.expectEqual('object', typeof ts);\n    testing.expectEqual('object', typeof ts.readable);\n    testing.expectEqual('object', typeof ts.writable);\n  }\n</script>\n\n<script id=transform_stream_with_transformer>\n  (async function() {\n    const ts = new TransformStream({\n      transform(chunk, controller) {\n        controller.enqueue(chunk.toUpperCase());\n      }\n    });\n\n    const writer = ts.writable.getWriter();\n    const reader = ts.readable.getReader();\n\n    await writer.write('hello');\n    await writer.close();\n\n    const result = await reader.read();\n    testing.expectEqual(false, result.done);\n    testing.expectEqual('HELLO', result.value);\n\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n\n<script id=writable_stream_basic>\n  {\n    const ws = new WritableStream();\n    testing.expectEqual('object', typeof ws);\n    testing.expectEqual(false, ws.locked);\n  }\n</script>\n\n<script id=writable_stream_writer>\n  {\n    const ws = new WritableStream();\n    const writer = ws.getWriter();\n    testing.expectEqual('object', typeof writer);\n    testing.expectEqual(true, ws.locked);\n  }\n</script>\n\n<script id=writable_stream_writer_desired_size>\n  {\n    const ws = new WritableStream();\n    const writer = ws.getWriter();\n    testing.expectEqual(1, writer.desiredSize);\n  }\n</script>\n\n<script id=text_encoder_stream_encoding>\n  {\n    const tes = new TextEncoderStream();\n    testing.expectEqual('utf-8', tes.encoding);\n    testing.expectEqual('object', typeof tes.readable);\n    testing.expectEqual('object', typeof tes.writable);\n  }\n</script>\n\n<script id=text_encoder_stream_encode>\n  (async function() {\n    const tes = new TextEncoderStream();\n\n    const writer = tes.writable.getWriter();\n    const reader = tes.readable.getReader();\n\n    await writer.write('hi');\n    await writer.close();\n\n    const result = await reader.read();\n    testing.expectEqual(false, result.done);\n    testing.expectEqual(true, result.value instanceof Uint8Array);\n    // 'hi' in UTF-8 is [104, 105]\n    testing.expectEqual(104, result.value[0]);\n    testing.expectEqual(105, result.value[1]);\n    testing.expectEqual(2, result.value.length);\n\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n\n<script id=pipe_through_basic>\n  (async function() {\n    const input = new ReadableStream({\n      start(controller) {\n        controller.enqueue('hello');\n        controller.close();\n      }\n    });\n\n    const ts = new TransformStream({\n      transform(chunk, controller) {\n        controller.enqueue(chunk.toUpperCase());\n      }\n    });\n\n    const output = input.pipeThrough(ts);\n    const reader = output.getReader();\n\n    const result = await reader.read();\n    testing.expectEqual(false, result.done);\n    testing.expectEqual('HELLO', result.value);\n\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n\n<script id=pipe_to_basic>\n  (async function() {\n    const chunks = [];\n    const input = new ReadableStream({\n      start(controller) {\n        controller.enqueue('a');\n        controller.enqueue('b');\n        controller.close();\n      }\n    });\n\n    const ws = new WritableStream({\n      write(chunk) {\n        chunks.push(chunk);\n      }\n    });\n\n    await input.pipeTo(ws);\n    testing.expectEqual(2, chunks.length);\n    testing.expectEqual('a', chunks[0]);\n    testing.expectEqual('b', chunks[1]);\n  })();\n</script>\n\n<script id=pipe_through_text_decoder>\n  (async function() {\n    const bytes = new Uint8Array([104, 101, 108, 108, 111]);\n    const input = new ReadableStream({\n      start(controller) {\n        controller.enqueue(bytes);\n        controller.close();\n      }\n    });\n\n    const output = input.pipeThrough(new TextDecoderStream());\n    const reader = output.getReader();\n\n    const result = await reader.read();\n    testing.expectEqual(false, result.done);\n    testing.expectEqual('hello', result.value);\n\n    const result2 = await reader.read();\n    testing.expectEqual(true, result2.done);\n  })();\n</script>\n"
  },
  {
    "path": "src/browser/tests/support/history.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<script id=history>\n  testing.expectEqual('auto', history.scrollRestoration);\n\n  history.scrollRestoration = 'manual';\n  testing.expectEqual('manual', history.scrollRestoration);\n\n  history.scrollRestoration = 'auto';\n  testing.expectEqual('auto', history.scrollRestoration);\n  testing.expectEqual(null, history.state)\n\n  history.pushState({ testInProgress: true }, null, testing.BASE_URL + 'history_after_nav.skip.html');\n  testing.expectEqual({ testInProgress: true }, history.state);\n\n  history.pushState({ testInProgress: false }, null, testing.ORIGIN + '/xhr/json');\n  history.replaceState({ \"new\": \"field\", testComplete: true }, null);\n\n  let state = { \"new\": \"field\", testComplete: true };\n  testing.expectEqual(state, history.state);\n\n  let popstateEventFired = false;\n  let popstateEventState = null;\n\n  window.top.support_history_completed = true;\n  window.addEventListener('popstate', (event) => {\n    window.top.window.support_history_popstateEventFired = true;\n    window.top.window.support_history_popstateEventState = event.state;\n  });\n\n  history.back();\n</script>\n\n"
  },
  {
    "path": "src/browser/tests/testing.js",
    "content": "(() => {\n  let failed = false;\n  let observed_ids = {};\n  let eventuallies = [];\n  let async_capture = null;\n  let current_script_id = null;\n\n  function expectTrue(actual) {\n     expectEqual(true, actual);\n  }\n\n  function expectFalse(actual) {\n     expectEqual(false, actual);\n  }\n\n  function expectEqual(expected, actual, opts) {\n    if (_equal(expected, actual)) {\n      _registerObservation('ok', opts);\n      return;\n    }\n    failed = true;\n    _registerObservation('fail', opts);\n    let err = `expected: ${_displayValue(expected)}, got: ${_displayValue(actual)}\\n  script_id: ${_currentScriptId()}`;\n    if (async_capture) {\n      err += `\\n stack: ${async_capture.stack}`;\n    }\n    console.error(err);\n    throw new Error('expectEqual failed');\n  }\n\n  function fail(reason) {\n    failed = true;\n    console.error(reason);\n    throw new Error('testing.fail()');\n  }\n\n  function expectError(expected, fn) {\n    withError((err) => {\n      expectEqual(true, err.toString().includes(expected));\n    }, fn);\n  }\n\n  function withError(cb, fn) {\n    try{\n      fn();\n    } catch (err) {\n      cb(err);\n      return;\n    }\n\n    console.error(`expected error but no error received\\n`);\n    throw new Error('no error');\n  }\n\n  function eventually(cb) {\n    const script_id = _currentScriptId();\n    if (!script_id) {\n      throw new Error('testing.eventually called outside of a script');\n    }\n    eventuallies.push({\n      callback: cb,\n      script_id: script_id,\n    });\n  }\n\n  async function async(cb) {\n    let capture = {script_id: document.currentScript.id, stack: new Error().stack};\n    await cb(() => { async_capture = capture; });\n    async_capture = null;\n  }\n\n  function assertOk() {\n    if (failed) {\n      throw new Error('Failed');\n    }\n\n    for (let e of eventuallies) {\n      current_script_id = e.script_id;\n      e.callback();\n      current_script_id = null;\n    }\n\n    const script_ids = Object.keys(observed_ids);\n    if (script_ids.length === 0) {\n      throw new Error('no test observations were recorded');\n    }\n\n    const scripts = document.getElementsByTagName('script');\n    for (let script of scripts) {\n      const script_id = script.id;\n      if (!script_id) {\n        continue;\n      }\n\n      const status = observed_ids[script_id];\n      if (status !== 'ok') {\n         throw new Error(`script id: '${script_id}' failed: ${status || 'no assertions'}`);\n      }\n    }\n  }\n\n  const IS_TEST_RUNNER = window.navigator.userAgent.startsWith(\"Lightpanda/\");\n\n  window.testing = {\n    fail: fail,\n    async: async,\n    assertOk: assertOk,\n    expectTrue: expectTrue,\n    expectFalse: expectFalse,\n    expectEqual: expectEqual,\n    expectError: expectError,\n    withError: withError,\n    eventually: eventually,\n    IS_TEST_RUNNER: IS_TEST_RUNNER,\n    HOST: '127.0.0.1',\n    ORIGIN: 'http://127.0.0.1:9582',\n    BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',\n  };\n\n  if (IS_TEST_RUNNER === false) {\n    // The page is running in a different browser. Probably a developer making sure\n    // a test is correct. There are a few tweaks we need to do to make this a\n    // seemless, namely around adapting paths/urls.\n    console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);\n    window.testing.HOST = location.hostname;\n    window.testing.ORIGIN = location.origin;\n    window.testing.BASE_URL = location.origin + '/src/browser/tests/';\n    window.addEventListener('load', testing.assertOk);\n  }\n\n\n  window.$ = function(sel) {\n    return document.querySelector(sel);\n  }\n\n  window.$$ = function(sel) {\n    return document.querySelectorAll(sel);\n  }\n\n  function _equal(expected, actual) {\n    if (expected === actual) {\n      return true;\n    }\n    if (expected === null || actual === null) {\n      return false;\n    }\n    if (typeof expected !== 'object' || typeof actual !== 'object') {\n      return false;\n    }\n\n    if (Object.keys(expected).length != Object.keys(actual).length) {\n      return false;\n    }\n\n    if (expected instanceof Node) {\n      if (!(actual instanceof Node)) {\n         return false;\n      }\n      return expected.isSameNode(actual);\n    }\n\n    for (property in expected) {\n      if (actual.hasOwnProperty(property) === false) {\n        return false;\n      }\n      if (_equal(expected[property], actual[property]) === false) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  function _registerObservation(status, opts) {\n    script_id = opts?.script_id || _currentScriptId();\n    if (!script_id) {\n      return;\n    }\n    if (observed_ids[script_id] === 'fail') {\n      return;\n    }\n\n    observed_ids[script_id] = status;\n\n    if (document.currentScript != null) {\n      if (document.currentScript.onerror === null) {\n        document.currentScript.onerror = function() {\n          observed_ids[document.currentScript.id] = 'fail';\n          failed = true;\n        }\n      }\n    }\n  }\n\n  function _currentScriptId() {\n    if (current_script_id) {\n      return current_script_id;\n    }\n\n    if (async_capture) {\n      return async_capture.script_id;\n    }\n\n    const current_script = document.currentScript;\n\n    if (!current_script) {\n      return null;\n    }\n    return current_script.id;\n  }\n\n  function _displayValue(value) {\n    if (value instanceof Element) {\n      return `HTMLElement: ${value.outerHTML}`;\n    }\n    if (value instanceof Attr) {\n      return `Attribute: ${value.name}: ${value.value}`;\n    }\n    if (value instanceof Node) {\n      return value.nodeName;\n    }\n    if (value === window) {\n      return '#window';\n    }\n    if (value instanceof Array) {\n      return `array: \\n${value.map(_displayValue).join('\\n')}\\n`;\n    }\n\n    const seen = [];\n    return JSON.stringify(value, function(key, val) {\n      if (val != null && typeof val == \"object\") {\n          if (seen.indexOf(val) >= 0) {\n              return;\n          }\n          seen.push(val);\n      }\n      return val;\n    });\n  }\n})();\n"
  },
  {
    "path": "src/browser/tests/url.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n<script id=url>\n  testing.expectEqual(\"function\", typeof window.URL);\n\n  {\n    const url = new URL('http://example.com/path');\n    testing.expectEqual('http://example.com/path', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path/';\n    const url = new URL('subpath', base);\n    testing.expectEqual('http://example.com/path/subpath', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path/subpath';\n    const url = new URL('/newpath', base);\n    testing.expectEqual('http://example.com/newpath', url.toString());\n  }\n\n  {\n    const url = new URL('?a=b', 'http://example.com/path');\n    testing.expectEqual('http://example.com/path?a=b', url.toString());\n  }\n\n  {\n    const url = new URL('#hash', 'http://example.com/path');\n    testing.expectEqual('http://example.com/path#hash', url.toString());\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('foo.js');\n    });\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('/path/to/file');\n    });\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('?query=value');\n    });\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('#fragment');\n    });\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('');\n    });\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('foo.js', 'not a url');\n    });\n  }\n\n  {\n    testing.withError((err) => {\n      testing.expectEqual(true, err.toString().includes('TypeError'));\n    }, () => {\n      const url = new URL('foo.js', 'bar/baz');\n    });\n  }\n\n  {\n    const url = new URL('http://example.com/absolute', 'invalid base');\n    testing.expectEqual('http://example.com/absolute', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/a/b/c/d';\n    const url = new URL('../foo', base);\n    testing.expectEqual('http://example.com/a/b/foo', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/a/b/c/d';\n    const url = new URL('../../../../../foo', base);\n    testing.expectEqual('http://example.com/foo', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path?a=b';\n    const url = new URL('?c=d', base);\n    testing.expectEqual('http://example.com/path?c=d', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path?a=b';\n    const url = new URL('#hash', base);\n    testing.expectEqual('http://example.com/path?a=b#hash', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path#hash';\n    const url = new URL('?c=d', base);\n    testing.expectEqual('http://example.com/path?c=d', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path#hash';\n    const url = new URL('#newhash', base);\n    testing.expectEqual('http://example.com/path#newhash', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path?a=b#hash';\n    const url = new URL('?c=d', base);\n    testing.expectEqual('http://example.com/path?c=d', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path?a=b#hash';\n    const url = new URL('#newhash', base);\n    testing.expectEqual('http://example.com/path?a=b#newhash', url.toString());\n  }\n\n  {\n    const base = 'http://example.com/path/?a=b';\n    const url = new URL('sub', base);\n    testing.expectEqual('http://example.com/path/sub', url.toString());\n  }\n\n  {\n    const url = new URL('http://example.com/path?a=b#hash');\n    testing.expectEqual(url.toString(), url.href);\n    testing.expectEqual('?a=b', url.search);\n    testing.expectEqual('#hash', url.hash);\n  }\n\n  {\n    const url = new URL('http://example.com/path');\n    testing.expectEqual('', url.search);\n    testing.expectEqual('', url.hash);\n  }\n\n  {\n    const url = new URL('http://example.com/path?a=b');\n    testing.expectEqual('?a=b', url.search);\n    testing.expectEqual('', url.hash);\n  }\n\n  {\n    const url = new URL('http://example.com/path#hash');\n    testing.expectEqual('', url.search);\n    testing.expectEqual('#hash', url.hash);\n  }\n\n  {\n    const url = new URL('http://example.com/path/to/resource?query=1#fragment');\n    testing.expectEqual('/path/to/resource', url.pathname);\n  }\n\n  {\n    const url = new URL('http://example.com');\n    testing.expectEqual('/', url.pathname);\n  }\n\n  {\n    const url = new URL('http://user:pass@example.com/path');\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('pass', url.password);\n    testing.expectEqual('/path', url.pathname);\n  }\n\n  {\n    const url = new URL('http://user@example.com/');\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('', url.password);\n  }\n\n  {\n    const url = new URL('http://user:@example.com/');\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('', url.password);\n  }\n\n  {\n    const url = new URL('http://example.com/');\n    testing.expectEqual('', url.username);\n    testing.expectEqual('', url.password);\n  }\n\n  {\n    const url = new URL('https://user:pass@example.com/path');\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('pass', url.password);\n    testing.expectEqual('/path', url.pathname);\n  }\n\n  {\n    const url = new URL('https://user@example.com/');\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('', url.password);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.username = 'newuser';\n    testing.expectEqual('newuser', url.username);\n    testing.expectEqual('https://newuser@example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://olduser@example.com/path');\n    url.username = 'newuser';\n    testing.expectEqual('newuser', url.username);\n    testing.expectEqual('https://newuser@example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://olduser:pass@example.com/path');\n    url.username = 'newuser';\n    testing.expectEqual('newuser', url.username);\n    testing.expectEqual('pass', url.password);\n    testing.expectEqual('https://newuser:pass@example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://user@example.com/path');\n    url.password = 'secret';\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('secret', url.password);\n    testing.expectEqual('https://user:secret@example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://user:oldpass@example.com/path');\n    url.password = 'newpass';\n    testing.expectEqual('user', url.username);\n    testing.expectEqual('newpass', url.password);\n    testing.expectEqual('https://user:newpass@example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://user:pass@example.com/path');\n    url.username = '';\n    url.password = '';\n    testing.expectEqual('', url.username);\n    testing.expectEqual('', url.password);\n    testing.expectEqual('https://example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.username = 'user@domain';\n    testing.expectEqual('user%40domain', url.username);\n    testing.expectEqual('https://user%40domain@example.com/path', url.href);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.username = 'user:name';\n    testing.expectEqual('user%3Aname', url.username);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.password = 'pass@word';\n    testing.expectEqual('pass%40word', url.password);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.password = 'pass:word';\n    testing.expectEqual('pass%3Aword', url.password);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.username = 'user/name';\n    testing.expectEqual('user%2Fname', url.username);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    url.password = 'pass?word';\n    testing.expectEqual('pass%3Fword', url.password);\n  }\n\n  {\n    const url = new URL('https://user%40domain:pass%3Aword@example.com/path');\n    testing.expectEqual('user%40domain', url.username);\n    testing.expectEqual('pass%3Aword', url.password);\n  }\n\n  {\n    const url = new URL('https://example.com:8080/path?a=b#hash');\n    url.username = 'user';\n    url.password = 'pass';\n    testing.expectEqual('https://user:pass@example.com:8080/path?a=b#hash', url.href);\n    testing.expectEqual('8080', url.port);\n    testing.expectEqual('?a=b', url.search);\n    testing.expectEqual('#hash', url.hash);\n  }\n\n  {\n    const url = new URL('http://user:pass@example.com:8080/path?query=1#hash');\n    testing.expectEqual('http:', url.protocol);\n    testing.expectEqual('example.com', url.hostname);\n    testing.expectEqual('8080', url.port);\n    testing.expectEqual('http://example.com:8080', url.origin);\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    testing.expectEqual('https:', url.protocol);\n    testing.expectEqual('example.com', url.hostname);\n    testing.expectEqual('', url.port);\n    testing.expectEqual('https://example.com', url.origin);\n  }\n\n  {\n    const url = new URL('https://example.com:443/path');\n    testing.expectEqual('https:', url.protocol);\n    testing.expectEqual('example.com', url.hostname);\n    testing.expectEqual('443', url.port);\n    testing.expectEqual('https://example.com', url.origin);\n  }\n\n  {\n    const url = new URL('http://example.com:80/path');\n    testing.expectEqual('http:', url.protocol);\n    testing.expectEqual('example.com', url.hostname);\n    testing.expectEqual('80', url.port);\n    testing.expectEqual('http://example.com', url.origin);\n  }\n\n  {\n    const url = new URL('ftp://example.com/path');\n    testing.expectEqual('ftp:', url.protocol);\n    testing.expectEqual('null', url.origin);\n  }\n</script>\n\n<script id=searchParams>\n  {\n    const url = new URL('https://example.com/path?foo=bar&baz=qux');\n    testing.expectEqual('bar', url.searchParams.get('foo'));\n    testing.expectEqual('qux', url.searchParams.get('baz'));\n    testing.expectEqual(2, url.searchParams.size);\n  }\n\n  {\n    const url = new URL('https://example.com/path?foo=bar&baz=qux');\n    url.searchParams.set('foo', 'updated');\n    url.searchParams.append('new', 'param');\n    const href = url.href;\n    testing.expectEqual(true, href.includes('foo=updated'));\n    testing.expectEqual(true, href.includes('new=param'));\n  }\n\n  {\n    const url = new URL('https://example.com/path');\n    testing.expectEqual(0, url.searchParams.size);\n    url.searchParams.set('added', 'value');\n    testing.expectEqual(true, url.href.includes('?added=value'));\n  }\n\n  {\n    const url = new URL('https://example.com/path?a=1#section');\n    testing.expectEqual('1', url.searchParams.get('a'));\n    url.searchParams.set('b', '2');\n    const href = url.href;\n    testing.expectEqual(true, href.includes('a=1'));\n    testing.expectEqual(true, href.includes('b=2'));\n    testing.expectEqual(true, href.endsWith('#section'));\n  }\n\n  {\n    const url = new URL('https://example.com/?x=1&y=2&z=3');\n    url.searchParams.delete('y');\n    const href = url.href;\n    testing.expectEqual(true, href.includes('x=1'));\n    testing.expectEqual(false, href.includes('y=2'));\n    testing.expectEqual(true, href.includes('z=3'));\n  }\n\n  {\n    const url = new URL('https://example.com/?test=1');\n    const sp1 = url.searchParams;\n    const sp2 = url.searchParams;\n    testing.expectEqual(true, sp1 === sp2);\n  }\n\n  {\n    const url = new URL('https://example.com/path?a=1&b=2');\n    url.searchParams.delete('a');\n    url.searchParams.delete('b');\n    testing.expectEqual('https://example.com/path', url.href);\n  }\n\n  {\n    let url = new URL(\"https://foo.bar\");\n    const searchParams = url.searchParams;\n\n    // SearchParams should be empty.\n    testing.expectEqual(0, searchParams.size);\n\n    url.href = \"https://lightpanda.io?over=9000&light=panda\";\n    // It won't hurt to check href and host too.\n    testing.expectEqual(\"https://lightpanda.io/?over=9000&light=panda\", url.href);\n    testing.expectEqual(\"lightpanda.io\", url.host);\n    // SearchParams should be updated too when URL is set.\n    testing.expectEqual(2, searchParams.size);\n    testing.expectEqual(\"9000\", searchParams.get(\"over\"));\n    testing.expectEqual(\"panda\", searchParams.get(\"light\"));\n  }\n</script>\n\n<script id=stringifier>\n{\n  const url1 = new URL('https://example.com/api');\n  const url2 = new URL(url1);\n  testing.expectEqual('https://example.com/api', url2.href);\n}\n\n{\n  const obj = {\n    toString() {\n      return 'https://example.com/custom';\n    }\n  };\n  const url = new URL(obj);\n  testing.expectEqual('https://example.com/custom', url.href);\n}\n\n{\n  const baseUrl = new URL('https://example.com/');\n  const url = new URL('/path', baseUrl);\n  testing.expectEqual('https://example.com/path', url.href);\n}\n\n{\n  const obj = {\n    toString() {\n      return 'https://example.com/';\n    }\n  };\n  const url = new URL('/relative', obj);\n  testing.expectEqual('https://example.com/relative', url.href);\n}\n\n{\n  const anchor = document.createElement('a');\n  anchor.href = 'https://example.com/test';\n  const url = new URL(anchor);\n  testing.expectEqual('https://example.com/test', url.href);\n}\n\n{\n  const anchor = document.createElement('a');\n  anchor.href = 'https://example.com/base/';\n  const url = new URL('relative', anchor);\n  testing.expectEqual('https://example.com/base/relative', url.href);\n}\n</script>\n\n<script id=setters>\n{\n  const url = new URL('https://example.com/path');\n  url.href = 'https://newdomain.com/newpath';\n  testing.expectEqual('https://newdomain.com/newpath', url.href);\n  testing.expectEqual('newdomain.com', url.hostname);\n  testing.expectEqual('/newpath', url.pathname);\n}\n\n{\n  const url = new URL('https://example.com/path?a=b#hash');\n  url.href = 'http://other.com/';\n  testing.expectEqual('http://other.com/', url.href);\n  testing.expectEqual('', url.search);\n  testing.expectEqual('', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.protocol = 'http';\n  testing.expectEqual('http://example.com/path', url.href);\n  testing.expectEqual('http:', url.protocol);\n}\n\n{\n  const url = new URL('http://example.com/path');\n  url.protocol = 'https:';\n  testing.expectEqual('https://example.com/path', url.href);\n  testing.expectEqual('https:', url.protocol);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.hostname = 'newhost.com';\n  testing.expectEqual('https://newhost.com/path', url.href);\n  testing.expectEqual('newhost.com', url.hostname);\n}\n\n{\n  const url = new URL('https://example.com:8080/path');\n  url.hostname = 'newhost.com';\n  testing.expectEqual('https://newhost.com:8080/path', url.href);\n  testing.expectEqual('newhost.com', url.hostname);\n  testing.expectEqual('8080', url.port);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.host = 'newhost.com:9000';\n  testing.expectEqual('https://newhost.com:9000/path', url.href);\n  testing.expectEqual('newhost.com', url.hostname);\n  testing.expectEqual('9000', url.port);\n}\n\n{\n  const url = new URL('https://example.com:8080/path');\n  url.host = 'newhost.com';\n  testing.expectEqual('https://newhost.com:8080/path', url.href);\n  testing.expectEqual('newhost.com', url.hostname);\n  testing.expectEqual('8080', url.port);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.port = '8080';\n  testing.expectEqual('https://example.com:8080/path', url.href);\n  testing.expectEqual('8080', url.port);\n}\n\n{\n  const url = new URL('https://example.com:8080/path');\n  url.port = '';\n  testing.expectEqual('https://example.com/path', url.href);\n  testing.expectEqual('', url.port);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.port = '443';\n  testing.expectEqual('https://example.com/path', url.href);\n  testing.expectEqual('', url.port);\n}\n\n{\n  const url = new URL('http://example.com/path');\n  url.port = '80';\n  testing.expectEqual('http://example.com/path', url.href);\n  testing.expectEqual('', url.port);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.pathname = '/newpath';\n  testing.expectEqual('https://example.com/newpath', url.href);\n  testing.expectEqual('/newpath', url.pathname);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.pathname = 'newpath';\n  testing.expectEqual('https://example.com/newpath', url.href);\n  testing.expectEqual('/newpath', url.pathname);\n}\n\n{\n  const url = new URL('https://example.com/path?a=b#hash');\n  url.pathname = '/new/path';\n  testing.expectEqual('https://example.com/new/path?a=b#hash', url.href);\n  testing.expectEqual('/new/path', url.pathname);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.search = '?a=b';\n  testing.expectEqual('https://example.com/path?a=b', url.href);\n  testing.expectEqual('?a=b', url.search);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.search = 'a=b';\n  testing.expectEqual('https://example.com/path?a=b', url.href);\n  testing.expectEqual('?a=b', url.search);\n}\n\n{\n  const url = new URL('https://example.com/path?old=value');\n  url.search = 'new=value';\n  testing.expectEqual('https://example.com/path?new=value', url.href);\n  testing.expectEqual('?new=value', url.search);\n}\n\n{\n  const url = new URL('https://example.com/path?a=b#hash');\n  url.search = 'c=d';\n  testing.expectEqual('https://example.com/path?c=d#hash', url.href);\n  testing.expectEqual('?c=d', url.search);\n  testing.expectEqual('#hash', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.hash = '#section';\n  testing.expectEqual('https://example.com/path#section', url.href);\n  testing.expectEqual('#section', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path');\n  url.hash = 'section';\n  testing.expectEqual('https://example.com/path#section', url.href);\n  testing.expectEqual('#section', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path#old');\n  url.hash = 'new';\n  testing.expectEqual('https://example.com/path#new', url.href);\n  testing.expectEqual('#new', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path?a=b#hash');\n  url.hash = 'newhash';\n  testing.expectEqual('https://example.com/path?a=b#newhash', url.href);\n  testing.expectEqual('?a=b', url.search);\n  testing.expectEqual('#newhash', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path#hash');\n  url.hash = '';\n  testing.expectEqual('https://example.com/path', url.href);\n  testing.expectEqual('', url.hash);\n}\n\n{\n  const url = new URL('https://example.com/path?a=b');\n  url.search = '';\n  testing.expectEqual('https://example.com/path', url.href);\n  testing.expectEqual('', url.search);\n}\n\n{\n  const url = new URL('https://example.com/path?a=b');\n  const sp = url.searchParams;\n  testing.expectEqual('b', sp.get('a'));\n  url.search = 'c=d';\n\n  testing.expectEqual('d', url.searchParams.get('c'));\n  testing.expectEqual(null, url.searchParams.get('a'));\n}\n\n{\n  const url = new URL('https://example.com/path?a=b');\n  const sp = url.searchParams;\n  testing.expectEqual('b', sp.get('a'));\n  url.href = 'https://example.com/other?x=y';\n\n  testing.expectEqual('y', url.searchParams.get('x'));\n  testing.expectEqual(null, url.searchParams.get('a'));\n}\n</script>\n\n<script id=canParse>\n{\n  testing.expectEqual('function', typeof URL.canParse);\n}\n\n{\n  testing.expectEqual(true, URL.canParse('https://example.com'));\n  testing.expectEqual(true, URL.canParse('http://example.com/path'));\n  testing.expectEqual(true, URL.canParse('https://example.com:8080/path?a=b#hash'));\n  testing.expectEqual(true, URL.canParse('ftp://example.com'));\n}\n\n{\n  testing.expectEqual(false, URL.canParse(''));\n  testing.expectEqual(false, URL.canParse('foo.js'));\n  testing.expectEqual(false, URL.canParse('/path/to/file'));\n  testing.expectEqual(false, URL.canParse('?query=value'));\n  testing.expectEqual(false, URL.canParse('#fragment'));\n}\n\n{\n  testing.expectEqual(true, URL.canParse('subpath', 'http://example.com/path/'));\n  testing.expectEqual(true, URL.canParse('/newpath', 'http://example.com/path'));\n  testing.expectEqual(true, URL.canParse('?a=b', 'http://example.com/path'));\n  testing.expectEqual(true, URL.canParse('#hash', 'http://example.com/path'));\n  testing.expectEqual(true, URL.canParse('../foo', 'http://example.com/a/b/c'));\n}\n\n{\n  testing.expectEqual(false, URL.canParse('foo.js', undefined));\n  testing.expectEqual(false, URL.canParse('subpath', undefined));\n  testing.expectEqual(false, URL.canParse('../foo', undefined));\n}\n\n{\n  testing.expectEqual(false, URL.canParse('foo.js', 'not a url'));\n  testing.expectEqual(false, URL.canParse('foo.js', 'bar/baz'));\n  testing.expectEqual(false, URL.canParse('subpath', 'relative/path'));\n}\n\n{\n  testing.expectEqual(false, URL.canParse('http://example.com/absolute', 'invalid base'));\n  testing.expectEqual(false, URL.canParse('https://example.com/path', 'not-a-url'));\n}\n\n{\n  testing.expectEqual(false, URL.canParse('http://example.com', ''));\n  testing.expectEqual(false, URL.canParse('http://example.com', 'relative'));\n}\n\n{\n  testing.expectEqual(true, URL.canParse('http://example.com/absolute', 'http://base.com'));\n  testing.expectEqual(true, URL.canParse('https://example.com/path', 'https://other.com'));\n}\n</script>\n\n<script id=query_encode>\n  {\n    let url = new URL('https://foo.bar/path?a=~&b=%7E#fragment');\n    testing.expectEqual(\"~\", url.searchParams.get('a'));\n    testing.expectEqual(\"~\", url.searchParams.get('b'));\n\n    url.searchParams.append('c', 'foo');\n    testing.expectEqual(\"foo\", url.searchParams.get('c'));\n    testing.expectEqual(1, url.searchParams.getAll('c').length);\n    testing.expectEqual(\"foo\", url.searchParams.getAll('c')[0]);\n    testing.expectEqual(3, url.searchParams.size);\n\n    // search is dynamic\n    testing.expectEqual(\"?a=%7E&b=%7E&c=foo\", url.search);\n  }\n</script>\n\n<script id=objectURL>\n{\n  // Test createObjectURL with Blob\n  const blob = new Blob(['<html><body>Hello</body></html>'], { type: 'text/html' });\n  const url = URL.createObjectURL(blob);\n\n  testing.expectEqual('string', typeof url);\n  testing.expectEqual(true, url.startsWith('blob:'));\n  testing.expectEqual(true, url.includes('http'));\n}\n\n{\n  // Test revokeObjectURL\n  const blob = new Blob(['test'], { type: 'text/plain' });\n  const url = URL.createObjectURL(blob);\n\n  testing.expectEqual(true, url.startsWith('blob:'));\n\n  URL.revokeObjectURL(url);\n  URL.revokeObjectURL(url); // Second revoke should be safe\n}\n\n{\n  // Test with non-blob URL (should be safe/silent)\n  URL.revokeObjectURL('http://example.com/notblob');\n  URL.revokeObjectURL('');\n  testing.expectEqual(true, true);\n}\n\n{\n  // Test uniqueness\n  const blob1 = new Blob(['test1']);\n  const blob2 = new Blob(['test2']);\n  const url1 = URL.createObjectURL(blob1);\n  const url2 = URL.createObjectURL(blob2);\n\n  testing.expectEqual(false, url1 === url2);\n  testing.expectEqual(true, url1.startsWith('blob:'));\n  testing.expectEqual(true, url2.startsWith('blob:'));\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/body_onload1.html",
    "content": "<!DOCTYPE html>\n<body onload=\"func1(event)\"></body>\n<script src=\"../testing.js\"></script>\n\n<script id=bodyOnLoad1>\n  let called = 0;\n  function func1(e) {\n    called += 1;\n    testing.expectEqual(document, e.target);\n    testing.expectEqual(window, e.currentTarget);\n  }\n\n  testing.eventually(() => {\n    testing.expectEqual(1, called);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/body_onload2.html",
    "content": "<!DOCTYPE html>\n<body onload=\"loadEvent = event\"></body>\n<script src=\"../testing.js\"></script>\n\n<script id=bodyOnLoad2>\n  // Per spec, the handler is compiled as: function(event) { loadEvent = event }\n  // Verify: handler fires, \"event\" parameter is a proper Event, and handler is a function.\n  let loadEvent = null;\n\n  testing.eventually(() => {\n    testing.expectEqual(\"function\", typeof document.body.onload);\n    testing.expectTrue(loadEvent instanceof Event);\n    testing.expectEqual(\"load\", loadEvent.type);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/body_onload3.html",
    "content": "<!DOCTYPE html>\n<body onload=\"called++\"></body>\n<script src=\"../testing.js\"></script>\n\n<script id=bodyOnLoad3>\n  // Per spec, the handler is compiled as: function(event) { called++ }\n  // Verify: handler fires exactly once, and body.onload reflects to window.onload.\n  let called = 0;\n\n  testing.eventually(() => {\n    // The attribute handler should have fired exactly once.\n    testing.expectEqual(1, called);\n\n    // body.onload is a Window-reflecting handler per spec.\n    testing.expectEqual(\"function\", typeof document.body.onload);\n    testing.expectEqual(document.body.onload, window.onload);\n\n    // Setting body.onload via property replaces the attribute handler.\n    let propertyCalled = false;\n    document.body.onload = function() { propertyCalled = true; };\n    testing.expectEqual(document.body.onload, window.onload);\n\n    // Setting onload to null removes the handler.\n    document.body.onload = null;\n    testing.expectEqual(null, document.body.onload);\n    testing.expectEqual(null, window.onload);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/location.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=location>\n  testing.expectEqual(testing.BASE_URL + 'window/location.html', window.location.href);\n  testing.expectEqual(document.location, window.location);\n</script>\n\n<script id=location_hash>\n  location.hash = \"\";\n  testing.expectEqual(\"\", location.hash);\n  testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);\n\n  location.hash = \"#abcdef\";\n  testing.expectEqual(\"#abcdef\", location.hash);\n  testing.expectEqual(testing.BASE_URL + 'window/location.html#abcdef', location.href);\n\n  location.hash = \"xyzxyz\";\n  testing.expectEqual(\"#xyzxyz\", location.hash);\n  testing.expectEqual(testing.BASE_URL + 'window/location.html#xyzxyz', location.href);\n\n  location.hash = \"\";\n  testing.expectEqual(\"\", location.hash);\n  testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/named_access.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<div id=i></div>\n<div id=testDiv></div>\n<span id=mySpan></span>\n<p id=paragraph></p>\n\n<script id=named_access_global>\n  testing.expectEqual('i', i.id);\n  testing.expectEqual('testDiv',testDiv.id);\n  testing.expectEqual('mySpan', mySpan.id);\n  testing.expectEqual('paragraph', paragraph.id);\n</script>\n\n<script id=named_access_window>\n  testing.expectEqual('i', window.i.id);\n  testing.expectEqual('testDiv', window.testDiv.id);\n  testing.expectEqual('mySpan', window.mySpan.id);\n  testing.expectEqual('paragraph', window.paragraph.id);\n</script>\n\n<script id=named_access_shadowing>\n  const i = 100;\n  testing.expectEqual(100, i);\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/onerror.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=onerrorBasicCallback>\n{\n  let callbackCalled = false;\n  let receivedArgs = null;\n\n  window.onerror = function(message, source, lineno, colno, error) {\n    callbackCalled = true;\n    receivedArgs = { message, source, lineno, colno, error };\n  };\n\n  const err = new Error('Test error');\n  window.reportError(err);\n\n  testing.expectEqual(true, callbackCalled);\n  testing.expectEqual(true, receivedArgs.message.includes('Test error'));\n  testing.expectEqual(err, receivedArgs.error);\n\n  window.onerror = null;\n}\n</script>\n\n<script id=onerrorFiveArguments>\n{\n  let argCount = 0;\n\n  window.onerror = function() {\n    argCount = arguments.length;\n  };\n\n  window.reportError(new Error('Five args test'));\n\n  // Per WHATWG spec, onerror receives exactly 5 arguments\n  testing.expectEqual(5, argCount);\n\n  window.onerror = null;\n}\n</script>\n\n<script id=onerrorReturnTrueCancelsEvent>\n{\n  let listenerCalled = false;\n\n  window.onerror = function() {\n    return true; // Should cancel the event\n  };\n\n  const listener = function() {\n    listenerCalled = true;\n  };\n  window.addEventListener('error', listener);\n\n  window.reportError(new Error('Should be cancelled'));\n\n  // The event listener should still be called (onerror returning true\n  // only prevents default, not propagation)\n  testing.expectEqual(true, listenerCalled);\n\n  window.onerror = null;\n  window.removeEventListener('error', listener);\n}\n</script>\n\n<script id=onerrorAndEventListenerBothCalled>\n{\n  let onerrorCalled = false;\n  let listenerCalled = false;\n\n  window.onerror = function() {\n    onerrorCalled = true;\n  };\n\n  const listener = function() {\n    listenerCalled = true;\n  };\n  window.addEventListener('error', listener);\n\n  window.reportError(new Error('Both should fire'));\n\n  testing.expectEqual(true, onerrorCalled);\n  testing.expectEqual(true, listenerCalled);\n\n  window.onerror = null;\n  window.removeEventListener('error', listener);\n}\n</script>\n\n<script id=onerrorCalledBeforeEventListener>\n{\n  let callOrder = [];\n\n  window.onerror = function() {\n    callOrder.push('onerror');\n  };\n\n  const listener = function() {\n    callOrder.push('listener');\n  };\n  window.addEventListener('error', listener);\n\n  window.reportError(new Error('Order test'));\n\n  // onerror should be called before addEventListener handlers\n  testing.expectEqual('onerror', callOrder[0]);\n  testing.expectEqual('listener', callOrder[1]);\n\n  window.onerror = null;\n  window.removeEventListener('error', listener);\n}\n</script>\n\n<script id=onerrorGetterSetter>\n{\n  const handler = function() {};\n\n  testing.expectEqual(null, window.onerror);\n\n  window.onerror = handler;\n  testing.expectEqual(handler, window.onerror);\n\n  window.onerror = null;\n  testing.expectEqual(null, window.onerror);\n}\n</script>\n\n<script id=onerrorWithNonFunction>\n{\n  // Setting onerror to a non-function should not throw\n  // but should not be stored as the handler\n  window.onerror = \"not a function\";\n  testing.expectEqual(null, window.onerror);\n\n  window.onerror = {};\n  testing.expectEqual(null, window.onerror);\n\n  window.onerror = 123;\n  testing.expectEqual(null, window.onerror);\n}\n</script>\n\n<script id=onerrorArgumentTypes>\n{\n  let receivedTypes = null;\n\n  window.onerror = function(message, source, lineno, colno, error) {\n    receivedTypes = {\n      message: typeof message,\n      source: typeof source,\n      lineno: typeof lineno,\n      colno: typeof colno,\n      error: typeof error\n    };\n  };\n\n  window.reportError(new Error('Type check'));\n\n  testing.expectEqual('string', receivedTypes.message);\n  testing.expectEqual('string', receivedTypes.source);\n  testing.expectEqual('number', receivedTypes.lineno);\n  testing.expectEqual('number', receivedTypes.colno);\n  testing.expectEqual('object', receivedTypes.error);\n\n  window.onerror = null;\n}\n</script>\n\n<script id=onerrorReturnFalseDoesNotCancel>\n{\n  let eventDefaultPrevented = false;\n\n  window.onerror = function() {\n    return false; // Should NOT cancel the event\n  };\n\n  const listener = function(e) {\n    eventDefaultPrevented = e.defaultPrevented;\n  };\n  window.addEventListener('error', listener);\n\n  window.reportError(new Error('Return false test'));\n\n  testing.expectEqual(false, eventDefaultPrevented);\n\n  window.onerror = null;\n  window.removeEventListener('error', listener);\n}\n</script>\n\n<script id=onerrorReturnTruePreventsDefault>\n{\n  let eventDefaultPrevented = false;\n\n  window.onerror = function() {\n    return true; // Should cancel (prevent default)\n  };\n\n  const listener = function(e) {\n    eventDefaultPrevented = e.defaultPrevented;\n  };\n  window.addEventListener('error', listener);\n\n  window.reportError(new Error('Return true test'));\n\n  testing.expectEqual(true, eventDefaultPrevented);\n\n  window.onerror = null;\n  window.removeEventListener('error', listener);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/report_error.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=reportErrorBasic>\n{\n  let errorEventFired = false;\n  let capturedEvent = null;\n\n  window.addEventListener('error', (e) => {\n    errorEventFired = true;\n    capturedEvent = e;\n  });\n\n  const err = new Error('Test error');\n  window.reportError(err);\n\n  testing.expectEqual(true, errorEventFired);\n  testing.expectEqual('error', capturedEvent.type);\n  testing.expectEqual(true, capturedEvent.message.includes('Test error'));\n}\n</script>\n\n<script id=reportErrorWithProperties>\n{\n  let evt = null;\n  window.addEventListener('error', (e) => {\n    evt = e;\n  });\n\n  const err = new Error('Detailed error');\n  err.fileName = 'script.js';\n  err.lineNumber = 100;\n  err.columnNumber = 25;\n\n  window.reportError(err);\n\n  testing.expectEqual(true, evt.message.includes('Detailed error'));\n  testing.expectEqual(err, evt.error);\n}\n</script>\n\n<script id=reportErrorCancelable>\n{\n  let eventCancelable = false;\n\n  window.addEventListener('error', (e) => {\n    eventCancelable = e.cancelable;\n    e.preventDefault();\n  });\n\n  window.reportError(new Error('Cancelable error'));\n\n  testing.expectEqual(true, eventCancelable);\n}\n</script>\n\n<script id=reportErrorNotBubbling>\n{\n  let parentCalled = false;\n  let windowCalled = false;\n\n  document.addEventListener('error', () => {\n    parentCalled = true;\n  });\n\n  window.addEventListener('error', () => {\n    windowCalled = true;\n  });\n\n  window.reportError(new Error('Non-bubbling error'));\n\n  testing.expectEqual(false, parentCalled);\n  testing.expectEqual(true, windowCalled);\n}\n</script>\n\n<script id=reportErrorWithString>\n{\n  let evt = null;\n\n  window.addEventListener('error', (e) => {\n    evt = e;\n  });\n\n  window.reportError('Plain string error');\n\n  testing.expectEqual('Plain string error', evt.message);\n  testing.expectEqual('', evt.filename);\n  testing.expectEqual(0, evt.lineno);\n  testing.expectEqual(0, evt.colno);\n}\n</script>\n\n<script id=reportErrorMultipleCalls>\n{\n  let callCount = 0;\n\n  window.addEventListener('error', () => {\n    callCount++;\n  });\n\n  window.reportError(new Error('First error'));\n  window.reportError(new Error('Second error'));\n  window.reportError(new Error('Third error'));\n\n  testing.expectEqual(3, callCount);\n}\n</script>\n\n<script id=reportErrorEventTarget>\n{\n  let evt = null;\n\n  window.addEventListener('error', (e) => {\n    evt = e;\n  });\n\n  const err = new Error('Target test');\n  window.reportError(err);\n\n  testing.expectEqual(window, evt.target);\n  testing.expectEqual(window, evt.currentTarget);\n}\n</script>\n\n<script id=reportErrorWithNull>\n{\n  let eventFired = false;\n  let evt = null;\n\n  window.addEventListener('error', (e) => {\n    eventFired = true;\n    evt = e;\n  });\n\n  window.reportError(null);\n\n  testing.expectEqual(true, eventFired);\n  testing.expectEqual('error', evt.type);\n  testing.expectEqual('', evt.filename);\n  testing.expectEqual(0, evt.lineno);\n  testing.expectEqual(0, evt.colno);\n}\n</script>\n\n<script id=reportErrorWithUndefined>\n{\n  let eventFired = false;\n  let evt = null;\n\n  window.addEventListener('error', (e) => {\n    eventFired = true;\n    evt = e;\n  });\n\n  window.reportError(undefined);\n\n  testing.expectEqual(true, eventFired);\n  testing.expectEqual('error', evt.type);\n  testing.expectEqual('', evt.filename);\n  testing.expectEqual(0, evt.lineno);\n  testing.expectEqual(0, evt.colno);\n}\n</script>\n\n<script id=reportErrorPreventDefault>\n{\n  let preventDefaultCalled = false;\n  let listenerCalled = false;\n\n  window.addEventListener('error', (e) => {\n    listenerCalled = true;\n    testing.expectEqual(true, e.cancelable);\n    e.preventDefault();\n    preventDefaultCalled = true;\n    testing.expectEqual(true, e.defaultPrevented);\n  });\n\n  window.reportError(new Error('Should be prevented'));\n\n  testing.expectEqual(true, listenerCalled);\n  testing.expectEqual(true, preventDefaultCalled);\n}\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/screen.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=screen>\n  let screen = window.screen;\n  if (testing.IS_TEST_RUNNER) {\n    testing.expectEqual(1920, screen.width);\n    testing.expectEqual(1080, screen.height);\n  }\n\n  let orientation = screen.orientation;\n  testing.expectEqual(0, orientation.angle);\n  testing.expectEqual('landscape-primary', orientation.type);\n\n  // this shouldn't crash (it used to)\n  screen.addEventListener('change', () => {});\n</script>\n\n<script id=orientation>\n  screen.orientation.addEventListener('change', () => {})\n  // above shouldn't crash (it used to)\n  testing.expectEqual(true, true);\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/scroll.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n<!-- Chrome don't scroll if the body isn't big enough. -->\n<body style=height:4000px;width:4000px></body>\n\n<script id=scroll_evt>\n  testing.async(async (restore) => {\n    let scrollevt = 0;\n    let scrollendevt = 0;\n    await new Promise((resolve) => {\n      document.addEventListener(\"scroll\", (event) => {\n        scrollevt++;\n      });\n      document.addEventListener(\"scrollend\", (event) => {\n        scrollendevt++;\n      });\n\n      window.scrollTo(10, 20);\n      testing.expectEqual(0, scrollevt);\n      testing.expectEqual(0, scrollendevt);\n\n      // scroll immediately: the scroll event must be throttled.\n      window.scrollTo(20, 40);\n      testing.expectEqual(0, scrollevt);\n      testing.expectEqual(0, scrollendevt);\n\n      // wait 10ms and scroll again: we should have a 2nd scroll, but no scrollend.\n      window.setTimeout(() => {\n        window.scrollTo(30, 40);\n      }, 10);\n\n      // wait until scrollend happens.\n      window.setTimeout(() => {\n        resolve();\n      }, 100);\n    });\n\n    restore();\n    testing.expectEqual(2, scrollevt);\n    // This test used to assert that scrollendevt == 1. The idea being that\n    // the above resolve() would stop the test running after \"scroll\" fires but\n    // before \"scrollend\" fires. That timing is pretty sensitive/fragile. If\n    // the browser gets delayed and doesn't figure the scroll event exactly when\n    // schedule, it could easily execute in the same sheduler.run call as the\n    // scrollend.\n    testing.expectEqual(true, scrollendevt === 1 || scrollendevt === 2);\n  });\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/stubs.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=\"alert\">\n  {\n    // alert should be callable without error\n    window.alert('hello');\n    window.alert();\n    testing.expectEqual(undefined, window.alert('test'));\n  }\n</script>\n\n<script id=\"confirm\">\n  {\n    // confirm returns false in headless mode\n    testing.expectEqual(false, window.confirm('proceed?'));\n    testing.expectEqual(false, window.confirm());\n  }\n</script>\n\n<script id=\"prompt\">\n  {\n    // prompt returns null in headless mode\n    testing.expectEqual(null, window.prompt('enter value'));\n    testing.expectEqual(null, window.prompt('enter value', 'default'));\n    testing.expectEqual(null, window.prompt());\n  }\n</script>\n\n<script id=\"devicePixelRatio\">\n  {\n    testing.expectEqual(1, window.devicePixelRatio);\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/timers.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=setInterval>\n  let set_interval1 = false\n  let timer1 = window.setInterval(function() {\n    set_interval1 = true;\n    testing.expectEqual(window, this);\n  }, 1);\n\n  let set_interval2 = false\n  let timer2 = window.setInterval(function() {\n    set_interval2 = true;\n  }, 1)\n  window.clearInterval(timer2);\n\n  testing.expectEqual(true, timer1 != timer2);\n\n\n  testing.eventually(() => {\n    testing.expectEqual(true, set_interval1);\n    testing.expectEqual(false, set_interval2);\n  });\n</script>\n\n<script id=setTimeout>\n  testing.expectEqual(1, window.setTimeout.length);\n  let wst2 = 1;\n  window.setTimeout((a, b) => {\n    wst2 = a + b;\n  }, 1, 2, 3);\n  testing.eventually(() => testing.expectEqual(5, wst2));\n</script>\n\n<script id=invalid-timer-clear>\n  // Surprisingly, these don't fail but silently ignored.\n  clearTimeout(-1);\n  clearInterval(-2);\n  clearImmediate(-3);\n  testing.expectEqual(true, true);\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/visual_viewport.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=visual_viewport>\n  const vp = window.visualViewport;\n  testing.expectEqual(vp, window.visualViewport);\n  testing.expectEqual(0, vp.offsetLeft);\n  testing.expectEqual(0, vp.offsetTop);\n  testing.expectEqual(0, vp.pageLeft);\n  testing.expectEqual(0, vp.pageTop);\n  testing.expectEqual(1920, vp.width);\n  testing.expectEqual(1080, vp.height);\n  testing.expectEqual(1.0, vp.scale);\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/window.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=window>\n  testing.expectEqual(window, globalThis);\n  testing.expectEqual(window, self);\n  testing.expectEqual(window, window.self);\n  testing.expectEqual(null, window.opener);\n\n  testing.expectEqual(1080, innerHeight);\n  testing.expectEqual(1920, innerWidth);\n</script>\n\n<script id=load>\n  let call1 = 0;\n  let call2 = 0;\n  window.onload = function(e) {\n    testing.expectEqual(false, true); // should not be called\n  }\n\n  const fn = function(e) {\n    call1 += 1;\n    testing.expectEqual(window, this);\n    testing.expectEqual(document, e.target);\n    testing.expectEqual(window, e.currentTarget);\n  };\n  window.onload = fn;\n  testing.expectEqual(fn, window.onload);\n\n  window.addEventListener('load', (e) => {\n    call2 += 1;\n    testing.expectEqual(window, this);\n    testing.expectEqual(document, e.target);\n    testing.expectEqual(window, e.currentTarget);\n  });\n\n  window.addEventListener('load', function(e) {\n    call2 += 1;\n    testing.expectEqual(window, this);\n    testing.expectEqual(document, e.target);\n    testing.expectEqual(window, e.currentTarget);\n  });\n\n  // noop\n  window.removeEventListener('load', fn);\n  testing.eventually(() => {\n    testing.expectEqual(1, call1);\n    testing.expectEqual(2, call2);\n  });\n</script>\n\n<script id=btoa>\n  testing.expectEqual('SGVsbG8gV29ybGQh', btoa('Hello World!'));\n  testing.expectEqual('', btoa(''));\n  testing.expectEqual('IA==', btoa(' '));\n  testing.expectEqual('YQ==', btoa('a'));\n  testing.expectEqual('YWI=', btoa('ab'));\n  testing.expectEqual('YWJj', btoa('abc'));\n  testing.expectEqual('MDEyMzQ1Njc4OQ==', btoa('0123456789'));\n  testing.expectEqual('VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==', btoa('The quick brown fox jumps over the lazy dog'));\n</script>\n\n<script id=atob>\n  testing.expectEqual('Hello World!', atob('SGVsbG8gV29ybGQh'));\n  testing.expectEqual('', atob(''));\n\n  // atob must trim input\n  testing.expectEqual('', atob(' '));\n  testing.expectEqual(' ', atob('IA=='));\n  testing.expectEqual(' ', atob(' IA=='));\n  testing.expectEqual(' ', atob('IA== '));\n\n  testing.expectEqual('a', atob('YQ=='));\n  testing.expectEqual('ab', atob('YWI='));\n  testing.expectEqual('abc', atob('YWJj'));\n  testing.expectEqual('0123456789', atob('MDEyMzQ1Njc4OQ=='));\n  testing.expectEqual('The quick brown fox jumps over the lazy dog', atob('VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=='));\n\n  // atob must accept unpadded base64 (forgiving-base64 decode per HTML spec)\n  testing.expectEqual('a', atob('YQ'));      // 2 chars, len%4==2, needs '=='\n  testing.expectEqual('ab', atob('YWI'));    // 3 chars, len%4==3, needs '='\n  testing.expectEqual('ceil', atob('Y2VpbA'));  // 6 chars, len%4==2, needs '=='\n\n  // length % 4 == 1 must still throw\n  testing.expectError('InvalidCharacterError', () => {\n    atob('Y');\n  });\n</script>\n\n<script id=btoa_atob_roundtrip>\n  const testStrings = [\n    'Hello World!',\n    'Test 123',\n    '',\n    'a',\n    'The quick brown fox jumps over the lazy dog',\n    '!@#$%^&*()',\n  ];\n\n  for (const str of testStrings) {\n    testing.expectEqual(str, atob(btoa(str)));\n  }\n\n  const testEncoded = [\n    'SGVsbG8gV29ybGQh',\n    'VGVzdCAxMjM=',\n    '',\n    'YQ==',\n    'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==',\n    'IUAjJCVeJiooKQ==',\n  ];\n\n  for (const enc of testEncoded) {\n    testing.expectEqual(enc, btoa(atob(enc)));\n  }\n</script>\n\n<script id=screen>\n  testing.expectEqual(1920, screen.width);\n  testing.expectEqual(1080, screen.height);\n  testing.expectEqual(1920, screen.availWidth);\n  testing.expectEqual(1040, screen.availHeight);\n  testing.expectEqual(24, screen.colorDepth);\n  testing.expectEqual(24, screen.pixelDepth);\n  testing.expectEqual(screen, window.screen);\n</script>\n\n<script id=structuredClone>\n  // Basic types\n  testing.expectEqual(42, structuredClone(42));\n  testing.expectEqual('hello', structuredClone('hello'));\n  testing.expectEqual(true, structuredClone(true));\n  testing.expectEqual(null, structuredClone(null));\n  testing.expectEqual(undefined, structuredClone(undefined));\n\n  // Objects and arrays (these work with JSON too, but verify they're cloned)\n  const obj = { a: 1, b: { c: 2 } };\n  const clonedObj = structuredClone(obj);\n  testing.expectEqual(1, clonedObj.a);\n  testing.expectEqual(2, clonedObj.b.c);\n  clonedObj.b.c = 999;\n  testing.expectEqual(2, obj.b.c); // original unchanged\n\n  const arr = [1, [2, 3]];\n  const clonedArr = structuredClone(arr);\n  testing.expectEqual(1, clonedArr[0]);\n  testing.expectEqual(2, clonedArr[1][0]);\n  clonedArr[1][0] = 999;\n  testing.expectEqual(2, arr[1][0]); // original unchanged\n\n  // Date - JSON would stringify to ISO string\n  const date = new Date('2024-01-15T12:30:00Z');\n  const clonedDate = structuredClone(date);\n  testing.expectEqual(true, clonedDate instanceof Date);\n  testing.expectEqual(date.getTime(), clonedDate.getTime());\n  testing.expectEqual(date.toISOString(), clonedDate.toISOString());\n\n  // RegExp - JSON would stringify to {}\n  const regex = /test\\d+/gi;\n  const clonedRegex = structuredClone(regex);\n  testing.expectEqual(true, clonedRegex instanceof RegExp);\n  testing.expectEqual(regex.source, clonedRegex.source);\n  testing.expectEqual(regex.flags, clonedRegex.flags);\n  testing.expectEqual(true, clonedRegex.test('test123'));\n\n  // Map - JSON can't handle\n  const map = new Map([['a', 1], ['b', 2]]);\n  const clonedMap = structuredClone(map);\n  testing.expectEqual(true, clonedMap instanceof Map);\n  testing.expectEqual(2, clonedMap.size);\n  testing.expectEqual(1, clonedMap.get('a'));\n  testing.expectEqual(2, clonedMap.get('b'));\n\n  // Set - JSON can't handle\n  const set = new Set([1, 2, 3]);\n  const clonedSet = structuredClone(set);\n  testing.expectEqual(true, clonedSet instanceof Set);\n  testing.expectEqual(3, clonedSet.size);\n  testing.expectEqual(true, clonedSet.has(1));\n  testing.expectEqual(true, clonedSet.has(2));\n  testing.expectEqual(true, clonedSet.has(3));\n\n  // ArrayBuffer\n  const buffer = new ArrayBuffer(8);\n  const view = new Uint8Array(buffer);\n  view[0] = 42;\n  view[7] = 99;\n  const clonedBuffer = structuredClone(buffer);\n  testing.expectEqual(true, clonedBuffer instanceof ArrayBuffer);\n  testing.expectEqual(8, clonedBuffer.byteLength);\n  const clonedView = new Uint8Array(clonedBuffer);\n  testing.expectEqual(42, clonedView[0]);\n  testing.expectEqual(99, clonedView[7]);\n\n  // TypedArray\n  const typedArr = new Uint32Array([100, 200, 300]);\n  const clonedTypedArr = structuredClone(typedArr);\n  testing.expectEqual(true, clonedTypedArr instanceof Uint32Array);\n  testing.expectEqual(3, clonedTypedArr.length);\n  testing.expectEqual(100, clonedTypedArr[0]);\n  testing.expectEqual(200, clonedTypedArr[1]);\n  testing.expectEqual(300, clonedTypedArr[2]);\n\n  // Special number values - JSON can't preserve these\n  testing.expectEqual(true, Number.isNaN(structuredClone(NaN)));\n  testing.expectEqual(Infinity, structuredClone(Infinity));\n  testing.expectEqual(-Infinity, structuredClone(-Infinity));\n\n  // Object with undefined value - JSON would omit it\n  const objWithUndef = { a: 1, b: undefined, c: 3 };\n  const clonedObjWithUndef = structuredClone(objWithUndef);\n  testing.expectEqual(1, clonedObjWithUndef.a);\n  testing.expectEqual(undefined, clonedObjWithUndef.b);\n  testing.expectEqual(true, 'b' in clonedObjWithUndef);\n  testing.expectEqual(3, clonedObjWithUndef.c);\n\n  // Error objects\n  const error = new Error('test error');\n  const clonedError = structuredClone(error);\n  testing.expectEqual(true, clonedError instanceof Error);\n  testing.expectEqual('test error', clonedError.message);\n\n  // TypeError\n  const typeError = new TypeError('type error');\n  const clonedTypeError = structuredClone(typeError);\n  testing.expectEqual(true, clonedTypeError instanceof TypeError);\n  testing.expectEqual('type error', clonedTypeError.message);\n\n  // BigInt\n  const bigInt = BigInt('9007199254740993');\n  const clonedBigInt = structuredClone(bigInt);\n  testing.expectEqual(bigInt, clonedBigInt);\n\n  // Circular references ARE supported by structuredClone (unlike JSON)\n  const circular = { a: 1 };\n  circular.self = circular;\n  const clonedCircular = structuredClone(circular);\n  testing.expectEqual(1, clonedCircular.a);\n  testing.expectEqual(clonedCircular, clonedCircular.self); // circular ref preserved\n\n  // Functions cannot be cloned - should throw\n  {\n    let threw = false;\n    try {\n      structuredClone(() => {});\n    } catch (err) {\n      threw = true;\n      // Just verify an error was thrown - V8's message format may vary\n    }\n    testing.expectEqual(true, threw);\n  }\n\n  // Symbols cannot be cloned - should throw\n  {\n    let threw = false;\n    try {\n      structuredClone(Symbol('test'));\n    } catch (err) {\n      threw = true;\n    }\n    testing.expectEqual(true, threw);\n  }\n</script>\n\n<script id=unhandled_rejection>\n  {\n    let unhandledCalled = 0;\n    window.onunhandledrejection = function(e) {\n      testing.expectEqual(true, e instanceof PromiseRejectionEvent);\n      testing.expectEqual({x: 'Fail'}, e.reason);\n      testing.expectEqual('unhandledrejection', e.type);\n      testing.expectEqual(window, e.target);\n      testing.expectEqual(window, e.srcElement);\n      testing.expectEqual(window, e.currentTarget);\n      unhandledCalled += 1;\n    }\n\n    window.addEventListener('unhandledrejection', function(e) {\n      testing.expectEqual(true, e instanceof PromiseRejectionEvent);\n      testing.expectEqual({x: 'Fail'}, e.reason);\n      testing.expectEqual('unhandledrejection', e.type);\n      testing.expectEqual(window, e.target);\n      testing.expectEqual(window, e.srcElement);\n      testing.expectEqual(window, e.currentTarget);\n      unhandledCalled += 1;\n    });\n    Promise.reject({x: 'Fail'});\n    testing.eventually(() => testing.expectEqual(2, unhandledCalled));\n  }\n</script>\n"
  },
  {
    "path": "src/browser/tests/window/window_event.html",
    "content": "<!DOCTYPE html>\n<script src=\"../testing.js\"></script>\n\n<script id=windowEventUndefinedOutsideHandler>\ntesting.expectEqual(undefined, window.event);\n</script>\n\n<script id=windowEventSetDuringWindowHandler>\nvar capturedEvent = null;\n\nwindow.addEventListener('test-event', function(e) {\n  capturedEvent = window.event;\n});\n\nvar ev = new Event('test-event');\nwindow.dispatchEvent(ev);\n\ntesting.expectEqual(ev, capturedEvent);\ntesting.expectEqual(undefined, window.event);\n</script>\n\n<script id=windowEventRestoredAfterHandler>\nvar captured2 = null;\n\nwindow.addEventListener('test-event-2', function(e) {\n  captured2 = window.event;\n});\n\nvar ev2 = new Event('test-event-2');\nwindow.dispatchEvent(ev2);\n\ntesting.expectEqual(ev2, captured2);\ntesting.expectEqual(undefined, window.event);\n</script>\n"
  },
  {
    "path": "src/browser/tests/window_scroll.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=scrollBy_exists>\n  testing.expectEqual('function', typeof window.scrollBy);\n</script>\n\n<script id=scrollBy_xy>\n  window.scrollTo(0, 0);\n  testing.expectEqual(0, window.scrollX);\n  testing.expectEqual(0, window.scrollY);\n  window.scrollBy(100, 200);\n  testing.expectEqual(100, window.scrollX);\n  testing.expectEqual(200, window.scrollY);\n</script>\n\n<script id=scrollBy_relative>\n  window.scrollTo(100, 100);\n  window.scrollBy(50, 50);\n  testing.expectEqual(150, window.scrollX);\n  testing.expectEqual(150, window.scrollY);\n</script>\n\n<script id=scrollBy_opts>\n  window.scrollTo(0, 0);\n  window.scrollBy({ left: 30, top: 40 });\n  testing.expectEqual(30, window.scrollX);\n  testing.expectEqual(40, window.scrollY);\n</script>\n\n<script id=scrollBy_negative_clamp>\n  window.scrollTo(10, 10);\n  window.scrollBy(-100, -100);\n  testing.expectEqual(0, window.scrollX);\n  testing.expectEqual(0, window.scrollY);\n</script>\n"
  },
  {
    "path": "src/browser/tests/xmlserializer.html",
    "content": "<!DOCTYPE html>\n<script src=\"testing.js\"></script>\n\n<script id=basic>\n{\n  const serializer = new XMLSerializer();\n  testing.expectEqual('object', typeof serializer);\n  testing.expectEqual('function', typeof serializer.serializeToString);\n}\n</script>\n\n<script id=serializeSimpleElement>\n{\n  const serializer = new XMLSerializer();\n  const div = document.createElement('div');\n  div.textContent = 'Hello World';\n\n  const result = serializer.serializeToString(div);\n  testing.expectEqual('<div>Hello World</div>', result);\n}\n</script>\n\n<script id=serializeElementWithAttributes>\n{\n  const serializer = new XMLSerializer();\n  const div = document.createElement('div');\n  div.id = 'test';\n  div.className = 'foo bar';\n  div.textContent = 'Content';\n\n  const result = serializer.serializeToString(div);\n  testing.expectEqual('<div id=\"test\" class=\"foo bar\">Content</div>', result);\n}\n</script>\n\n<script id=serializeNestedElements>\n{\n  const serializer = new XMLSerializer();\n  const div = document.createElement('div');\n  const p = document.createElement('p');\n  const span = document.createElement('span');\n  span.textContent = 'Nested';\n  p.appendChild(span);\n  div.appendChild(p);\n\n  const result = serializer.serializeToString(div);\n  testing.expectEqual('<div><p><span>Nested</span></p></div>', result);\n}\n</script>\n\n<script id=serializeEmptyElement>\n{\n  const serializer = new XMLSerializer();\n  const div = document.createElement('div');\n\n  const result = serializer.serializeToString(div);\n  testing.expectEqual('<div></div>', result);\n}\n</script>\n\n<script id=serializeVoidElements>\n{\n  const serializer = new XMLSerializer();\n  const br = document.createElement('br');\n  const img = document.createElement('img');\n  img.src = 'test.png';\n\n  const brResult = serializer.serializeToString(br);\n  testing.expectEqual('<br>', brResult);\n\n  const imgResult = serializer.serializeToString(img);\n  testing.expectEqual('<img src=\"test.png\">', imgResult);\n}\n</script>\n\n<script id=serializeMultipleSiblings>\n{\n  const serializer = new XMLSerializer();\n  const container = document.createElement('div');\n  const span1 = document.createElement('span');\n  span1.textContent = 'First';\n  const span2 = document.createElement('span');\n  span2.textContent = 'Second';\n  container.appendChild(span1);\n  container.appendChild(span2);\n\n  const result = serializer.serializeToString(container);\n  testing.expectEqual('<div><span>First</span><span>Second</span></div>', result);\n}\n</script>\n\n<script id=serializeDocumentFragment>\n{\n  const serializer = new XMLSerializer();\n  const fragment = document.createDocumentFragment();\n  const div = document.createElement('div');\n  div.textContent = 'In fragment';\n  const span = document.createElement('span');\n  span.textContent = 'Also in fragment';\n  fragment.appendChild(div);\n  fragment.appendChild(span);\n\n  const result = serializer.serializeToString(fragment);\n  testing.expectEqual('<div>In fragment</div><span>Also in fragment</span>', result);\n}\n</script>\n\n<script id=serializeFromDOM>\n{\n  const serializer = new XMLSerializer();\n  const testDiv = document.createElement('div');\n  testDiv.id = 'serialize-test';\n  testDiv.innerHTML = '<p class=\"test\">Hello <strong>World</strong></p>';\n\n  const result = serializer.serializeToString(testDiv);\n  testing.expectEqual('<div id=\"serialize-test\"><p class=\"test\">Hello <strong>World</strong></p></div>', result);\n}\n</script>\n\n<script id=roundtripWithInnerHTML>\n{\n  const serializer = new XMLSerializer();\n  const original = '<div class=\"container\"><p>Text</p><span id=\"x\">More</span></div>';\n\n  const div = document.createElement('div');\n  div.innerHTML = original;\n\n  const serialized = serializer.serializeToString(div.firstChild);\n  testing.expectEqual(original, serialized);\n}\n</script>\n\n<script id=serializeAttribute>\n{\n  const div = document.createElement('div');\n  div.setAttribute('over', '9000');\n\n  const serializer = new XMLSerializer();\n  const serialized = serializer.serializeToString(div.getAttributeNode('over'));\n  testing.expectEqual('', serialized);\n}\n</script>\n"
  },
  {
    "path": "src/browser/webapi/AbortController.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst AbortSignal = @import(\"AbortSignal.zig\");\n\nconst AbortController = @This();\n\n_signal: *AbortSignal,\n\npub fn init(page: *Page) !*AbortController {\n    const signal = try AbortSignal.init(page);\n    return page._factory.create(AbortController{\n        ._signal = signal,\n    });\n}\n\npub fn getSignal(self: *const AbortController) *AbortSignal {\n    return self._signal;\n}\n\npub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {\n    try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(AbortController);\n\n    pub const Meta = struct {\n        pub const name = \"AbortController\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(AbortController.init, .{});\n    pub const signal = bridge.accessor(AbortController.getSignal, null, .{});\n    pub const abort = bridge.function(AbortController.abort, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: AbortController\" {\n    try testing.htmlRunner(\"event/abort_controller.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/AbortSignal.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Event = @import(\"Event.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\n\nconst AbortSignal = @This();\n\n_proto: *EventTarget,\n_aborted: bool = false,\n_reason: Reason = .undefined,\n_on_abort: ?js.Function.Global = null,\n\npub fn init(page: *Page) !*AbortSignal {\n    return page._factory.eventTarget(AbortSignal{\n        ._proto = undefined,\n    });\n}\n\npub fn getAborted(self: *const AbortSignal) bool {\n    return self._aborted;\n}\n\npub fn getReason(self: *const AbortSignal) Reason {\n    return self._reason;\n}\n\npub fn getOnAbort(self: *const AbortSignal) ?js.Function.Global {\n    return self._on_abort;\n}\n\npub fn setOnAbort(self: *AbortSignal, cb: ?js.Function.Global) !void {\n    self._on_abort = cb;\n}\n\npub fn asEventTarget(self: *AbortSignal) *EventTarget {\n    return self._proto;\n}\n\npub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {\n    if (self._aborted) {\n        return;\n    }\n\n    self._aborted = true;\n\n    // Store the abort reason (default to a simple string if none provided)\n    if (reason_) |reason| {\n        switch (reason) {\n            .js_val => |js_val| self._reason = .{ .js_val = js_val },\n            .string => |str| self._reason = .{ .string = try page.dupeString(str) },\n            .undefined => self._reason = reason,\n        }\n    } else {\n        self._reason = .{ .string = \"AbortError\" };\n    }\n\n    // Dispatch abort event\n    const target = self.asEventTarget();\n    if (page._event_manager.hasDirectListeners(target, \"abort\", self._on_abort)) {\n        const event = try Event.initTrusted(comptime .wrap(\"abort\"), .{}, page);\n        try page._event_manager.dispatchDirect(target, event, self._on_abort, .{ .context = \"abort signal\" });\n    }\n}\n\n// Static method to create an already-aborted signal\npub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {\n    const signal = try init(page);\n    try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);\n    return signal;\n}\n\npub fn createTimeout(delay: u32, page: *Page) !*AbortSignal {\n    const callback = try page.arena.create(TimeoutCallback);\n    callback.* = .{\n        .page = page,\n        .signal = try init(page),\n    };\n\n    try page.js.scheduler.add(callback, TimeoutCallback.run, delay, .{\n        .name = \"AbortSignal.timeout\",\n    });\n\n    return callback.signal;\n}\n\nconst ThrowIfAborted = union(enum) {\n    exception: js.Exception,\n    undefined: void,\n};\npub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {\n    const local = page.js.local.?;\n\n    if (self._aborted) {\n        const exception = switch (self._reason) {\n            .string => |str| local.throw(str),\n            .js_val => |js_val| local.throw(try local.toLocal(js_val).toStringSlice()),\n            .undefined => local.throw(\"AbortError\"),\n        };\n        return .{ .exception = exception };\n    }\n    return .undefined;\n}\n\nconst Reason = union(enum) {\n    js_val: js.Value.Global,\n    string: []const u8,\n    undefined: void,\n};\n\nconst TimeoutCallback = struct {\n    page: *Page,\n    signal: *AbortSignal,\n\n    fn run(ctx: *anyopaque) !?u32 {\n        const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));\n        self.signal.abort(.{ .string = \"TimeoutError\" }, self.page) catch |err| {\n            log.warn(.app, \"abort signal timeout\", .{ .err = err });\n        };\n        return null;\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(AbortSignal);\n\n    pub const Meta = struct {\n        pub const name = \"AbortSignal\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const Prototype = EventTarget;\n\n    pub const constructor = bridge.constructor(AbortSignal.init, .{});\n    pub const aborted = bridge.accessor(AbortSignal.getAborted, null, .{});\n    pub const reason = bridge.accessor(AbortSignal.getReason, null, .{});\n    pub const onabort = bridge.accessor(AbortSignal.getOnAbort, AbortSignal.setOnAbort, .{});\n    pub const throwIfAborted = bridge.function(AbortSignal.throwIfAborted, .{});\n\n    // Static method\n    pub const abort = bridge.function(AbortSignal.createAborted, .{ .static = true });\n    pub const timeout = bridge.function(AbortSignal.createTimeout, .{ .static = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/AbstractRange.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Session = @import(\"../Session.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst Range = @import(\"Range.zig\");\n\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst AbstractRange = @This();\n\npub const _prototype_root = true;\n\n_rc: u8,\n_type: Type,\n_page_id: u32,\n_arena: Allocator,\n_end_offset: u32,\n_start_offset: u32,\n_end_container: *Node,\n_start_container: *Node,\n\n// Intrusive linked list node for tracking live ranges on the Page.\n_range_link: std.DoublyLinkedList.Node = .{},\n\npub fn acquireRef(self: *AbstractRange) void {\n    self._rc += 1;\n}\n\npub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void {\n    _ = shutdown;\n    const rc = self._rc;\n    if (comptime IS_DEBUG) {\n        std.debug.assert(rc != 0);\n    }\n\n    if (rc == 1) {\n        if (session.findPageById(self._page_id)) |page| {\n            page._live_ranges.remove(&self._range_link);\n        }\n        session.releaseArena(self._arena);\n        return;\n    }\n    self._rc = rc - 1;\n}\n\npub const Type = union(enum) {\n    range: *Range,\n    // TODO: static_range: *StaticRange,\n};\n\npub fn as(self: *AbstractRange, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn is(self: *AbstractRange, comptime T: type) ?*T {\n    switch (self._type) {\n        .range => |r| return if (T == Range) r else null,\n    }\n}\n\npub fn getStartContainer(self: *const AbstractRange) *Node {\n    return self._start_container;\n}\n\npub fn getStartOffset(self: *const AbstractRange) u32 {\n    return self._start_offset;\n}\n\npub fn getEndContainer(self: *const AbstractRange) *Node {\n    return self._end_container;\n}\n\npub fn getEndOffset(self: *const AbstractRange) u32 {\n    return self._end_offset;\n}\n\npub fn getCollapsed(self: *const AbstractRange) bool {\n    return self._start_container == self._end_container and\n        self._start_offset == self._end_offset;\n}\n\npub fn getCommonAncestorContainer(self: *const AbstractRange) *Node {\n    // Let container be start container\n    var container = self._start_container;\n\n    // While container is not an inclusive ancestor of end container\n    while (!isInclusiveAncestorOf(container, self._end_container)) {\n        // Let container be container's parent\n        container = container.parentNode() orelse break;\n    }\n\n    return container;\n}\n\npub fn isStartAfterEnd(self: *const AbstractRange) bool {\n    return compareBoundaryPoints(\n        self._start_container,\n        self._start_offset,\n        self._end_container,\n        self._end_offset,\n    ) == .after;\n}\n\nconst BoundaryComparison = enum {\n    before,\n    equal,\n    after,\n};\n\npub fn compareBoundaryPoints(\n    node_a: *Node,\n    offset_a: u32,\n    node_b: *Node,\n    offset_b: u32,\n) BoundaryComparison {\n    // If same container, just compare offsets\n    if (node_a == node_b) {\n        if (offset_a < offset_b) return .before;\n        if (offset_a > offset_b) return .after;\n        return .equal;\n    }\n\n    // Check if one contains the other\n    if (isAncestorOf(node_a, node_b)) {\n        // A contains B, so A's position comes before B\n        // But we need to check if the offset in A comes after B\n        var child = node_b;\n        var parent = child.parentNode();\n        while (parent) |p| {\n            if (p == node_a) {\n                const child_index = p.getChildIndex(child) orelse unreachable;\n                if (offset_a <= child_index) {\n                    return .before;\n                }\n                return .after;\n            }\n            child = p;\n            parent = p.parentNode();\n        }\n        unreachable;\n    }\n\n    if (isAncestorOf(node_b, node_a)) {\n        // B contains A, so B's position comes before A\n        var child = node_a;\n        var parent = child.parentNode();\n        while (parent) |p| {\n            if (p == node_b) {\n                const child_index = p.getChildIndex(child) orelse unreachable;\n                if (child_index < offset_b) {\n                    return .before;\n                }\n                return .after;\n            }\n            child = p;\n            parent = p.parentNode();\n        }\n        unreachable;\n    }\n\n    // Neither contains the other, find their relative position in tree order\n    // Walk up from A to find all ancestors\n    var current = node_a;\n    var a_count: usize = 0;\n    var a_ancestors: [64]*Node = undefined;\n    while (a_count < 64) {\n        a_ancestors[a_count] = current;\n        a_count += 1;\n        current = current.parentNode() orelse break;\n    }\n\n    // Walk up from B and find first common ancestor\n    current = node_b;\n    while (current.parentNode()) |parent| {\n        for (a_ancestors[0..a_count]) |ancestor| {\n            if (ancestor != parent) {\n                continue;\n            }\n\n            // Found common ancestor\n            // Now compare positions of the children in this ancestor\n            const a_child = blk: {\n                var node = node_a;\n                while (node.parentNode()) |p| {\n                    if (p == parent) break :blk node;\n                    node = p;\n                }\n                unreachable;\n            };\n            const b_child = current;\n\n            const a_index = parent.getChildIndex(a_child) orelse unreachable;\n            const b_index = parent.getChildIndex(b_child) orelse unreachable;\n\n            if (a_index < b_index) {\n                return .before;\n            }\n            if (a_index > b_index) {\n                return .after;\n            }\n            return .equal;\n        }\n        current = parent;\n    }\n\n    // Should not reach here if nodes are in the same tree\n    return .before;\n}\n\nfn isAncestorOf(potential_ancestor: *Node, node: *Node) bool {\n    var current = node.parentNode();\n    while (current) |parent| {\n        if (parent == potential_ancestor) {\n            return true;\n        }\n        current = parent.parentNode();\n    }\n    return false;\n}\n\nfn isInclusiveAncestorOf(potential_ancestor: *Node, node: *Node) bool {\n    if (potential_ancestor == node) {\n        return true;\n    }\n    return isAncestorOf(potential_ancestor, node);\n}\n\n/// Update this range's boundaries after a replaceData mutation on target.\n/// All parameters are in UTF-16 code unit offsets.\npub fn updateForCharacterDataReplace(self: *AbstractRange, target: *Node, offset: u32, count: u32, data_len: u32) void {\n    if (self._start_container == target) {\n        if (self._start_offset > offset and self._start_offset <= offset + count) {\n            self._start_offset = offset;\n        } else if (self._start_offset > offset + count) {\n            // Use i64 intermediate to avoid u32 underflow when count > data_len\n            self._start_offset = @intCast(@as(i64, self._start_offset) + @as(i64, data_len) - @as(i64, count));\n        }\n    }\n\n    if (self._end_container == target) {\n        if (self._end_offset > offset and self._end_offset <= offset + count) {\n            self._end_offset = offset;\n        } else if (self._end_offset > offset + count) {\n            self._end_offset = @intCast(@as(i64, self._end_offset) + @as(i64, data_len) - @as(i64, count));\n        }\n    }\n}\n\n/// Update this range's boundaries after a splitText operation.\n/// Steps 7b-7e of the DOM spec splitText algorithm.\npub fn updateForSplitText(self: *AbstractRange, target: *Node, new_node: *Node, offset: u32, parent: *Node, node_index: u32) void {\n    // Step 7b: ranges on the original node with start > offset move to new node\n    if (self._start_container == target and self._start_offset > offset) {\n        self._start_container = new_node;\n        self._start_offset = self._start_offset - offset;\n    }\n    // Step 7c: ranges on the original node with end > offset move to new node\n    if (self._end_container == target and self._end_offset > offset) {\n        self._end_container = new_node;\n        self._end_offset = self._end_offset - offset;\n    }\n    // Step 7d: ranges on parent with start == node_index + 1 increment\n    if (self._start_container == parent and self._start_offset == node_index + 1) {\n        self._start_offset += 1;\n    }\n    // Step 7e: ranges on parent with end == node_index + 1 increment\n    if (self._end_container == parent and self._end_offset == node_index + 1) {\n        self._end_offset += 1;\n    }\n}\n\n/// Update this range's boundaries after a node insertion.\npub fn updateForNodeInsertion(self: *AbstractRange, parent: *Node, child_index: u32) void {\n    if (self._start_container == parent and self._start_offset > child_index) {\n        self._start_offset += 1;\n    }\n    if (self._end_container == parent and self._end_offset > child_index) {\n        self._end_offset += 1;\n    }\n}\n\n/// Update this range's boundaries after a node removal.\npub fn updateForNodeRemoval(self: *AbstractRange, parent: *Node, child: *Node, child_index: u32) void {\n    // Steps 4-5: ranges whose start/end is an inclusive descendant of child\n    // get moved to (parent, child_index).\n    if (isInclusiveDescendantOf(self._start_container, child)) {\n        self._start_container = parent;\n        self._start_offset = child_index;\n    }\n    if (isInclusiveDescendantOf(self._end_container, child)) {\n        self._end_container = parent;\n        self._end_offset = child_index;\n    }\n\n    // Steps 6-7: ranges on parent at offsets > child_index get decremented.\n    if (self._start_container == parent and self._start_offset > child_index) {\n        self._start_offset -= 1;\n    }\n    if (self._end_container == parent and self._end_offset > child_index) {\n        self._end_offset -= 1;\n    }\n}\n\nfn isInclusiveDescendantOf(node: *Node, potential_ancestor: *Node) bool {\n    var current: ?*Node = node;\n    while (current) |n| {\n        if (n == potential_ancestor) return true;\n        current = n.parentNode();\n    }\n    return false;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(AbstractRange);\n\n    pub const Meta = struct {\n        pub const name = \"AbstractRange\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(AbstractRange.deinit);\n    };\n\n    pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{});\n    pub const startOffset = bridge.accessor(AbstractRange.getStartOffset, null, .{});\n    pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{});\n    pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{});\n    pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{});\n    pub const commonAncestorContainer = bridge.accessor(AbstractRange.getCommonAncestorContainer, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/Blob.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Writer = std.Io.Writer;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst Mime = @import(\"../Mime.zig\");\n\nconst Allocator = std.mem.Allocator;\n\n/// https://w3c.github.io/FileAPI/#blob-section\n/// https://developer.mozilla.org/en-US/docs/Web/API/Blob\nconst Blob = @This();\n\npub const _prototype_root = true;\n\n_type: Type,\n\n_arena: Allocator,\n\n/// Immutable slice of blob.\n/// Note that another blob may hold a pointer/slice to this,\n/// so its better to leave the deallocation of it to arena allocator.\n_slice: []const u8,\n/// MIME attached to blob. Can be an empty string.\n_mime: []const u8,\n\npub const Type = union(enum) {\n    generic,\n    file: *@import(\"File.zig\"),\n};\n\nconst InitOptions = struct {\n    /// MIME type.\n    type: []const u8 = \"\",\n    /// How to handle line endings (CR and LF).\n    /// `transparent` means do nothing, `native` expects CRLF (\\r\\n) on Windows.\n    endings: []const u8 = \"transparent\",\n};\n\n/// Creates a new Blob (JS constructor).\npub fn init(\n    maybe_blob_parts: ?[]const []const u8,\n    maybe_options: ?InitOptions,\n    page: *Page,\n) !*Blob {\n    return initWithMimeValidation(maybe_blob_parts, maybe_options, false, page);\n}\n\n/// Creates a new Blob with optional MIME validation.\n/// When validate_mime is true, uses full MIME parsing (for Response/Request).\n/// When false, uses simple ASCII validation per FileAPI spec (for Blob constructor).\npub fn initWithMimeValidation(\n    maybe_blob_parts: ?[]const []const u8,\n    maybe_options: ?InitOptions,\n    validate_mime: bool,\n    page: *Page,\n) !*Blob {\n    const arena = try page.getArena(.{ .debug = \"Blob\" });\n    errdefer page.releaseArena(arena);\n\n    const options: InitOptions = maybe_options orelse .{};\n\n    const mime: []const u8 = blk: {\n        const t = options.type;\n        if (t.len == 0) {\n            break :blk \"\";\n        }\n\n        const buf = try arena.dupe(u8, t);\n\n        if (validate_mime) {\n            // Full MIME parsing per MIME sniff spec (for Content-Type headers)\n            _ = Mime.parse(buf) catch break :blk \"\";\n        } else {\n            // Simple validation per FileAPI spec (for Blob constructor):\n            // - If any char is outside U+0020-U+007E, return empty string\n            // - Otherwise lowercase\n            for (t) |c| {\n                if (c < 0x20 or c > 0x7E) {\n                    break :blk \"\";\n                }\n            }\n            _ = std.ascii.lowerString(buf, buf);\n        }\n\n        break :blk buf;\n    };\n\n    const data = blk: {\n        if (maybe_blob_parts) |blob_parts| {\n            var w: Writer.Allocating = .init(arena);\n            const use_native_endings = std.mem.eql(u8, options.endings, \"native\");\n            try writeBlobParts(&w.writer, blob_parts, use_native_endings);\n\n            break :blk w.written();\n        }\n\n        break :blk \"\";\n    };\n\n    const self = try arena.create(Blob);\n    self.* = .{\n        ._arena = arena,\n        ._type = .generic,\n        ._slice = data,\n        ._mime = mime,\n    };\n    return self;\n}\n\npub fn deinit(self: *Blob, shutdown: bool, session: *Session) void {\n    _ = shutdown;\n    session.releaseArena(self._arena);\n}\n\nconst largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);\n/// Array of possible vector sizes for the current arch in decrementing order.\n/// We may move this to some file for SIMD helpers in the future.\nconst vector_sizes = blk: {\n    // Required for length calculation.\n    var n: usize = largest_vector;\n    var total: usize = 0;\n    while (n != 2) : (n /= 2) total += 1;\n    // Populate an array with vector sizes.\n    n = largest_vector;\n    var i: usize = 0;\n    var items: [total]usize = undefined;\n    while (n != 2) : (n /= 2) {\n        defer i += 1;\n        items[i] = n;\n    }\n\n    break :blk items;\n};\n\n/// Writes blob parts to given `Writer` with desired endings.\nfn writeBlobParts(\n    writer: *Writer,\n    blob_parts: []const []const u8,\n    use_native_endings: bool,\n) !void {\n    // Transparent.\n    if (!use_native_endings) {\n        for (blob_parts) |part| {\n            try writer.writeAll(part);\n        }\n\n        return;\n    }\n\n    // TODO: Windows support.\n\n    // Linux & Unix.\n    // Both Firefox and Chrome implement it as such:\n    // CRLF => LF\n    // CR   => LF\n    // So even though CR is not followed by LF, it gets replaced.\n    //\n    // I believe this is because such scenario is possible:\n    // ```\n    // let parts = [ \"the quick\\r\", \"\\nbrown fox\" ];\n    // ```\n    // In the example, one should have to check the part before in order to\n    // understand that CRLF is being presented in the final buffer.\n    // So they took a simpler approach, here's what given blob parts produce:\n    // ```\n    // \"the quick\\n\\nbrown fox\"\n    // ```\n    scan_parts: for (blob_parts) |part| {\n        var end: usize = 0;\n\n        inline for (vector_sizes) |vector_len| {\n            const Vec = @Vector(vector_len, u8);\n\n            while (end + vector_len <= part.len) : (end += vector_len) {\n                const cr: Vec = @splat('\\r');\n                // Load chunk as vectors.\n                const data = part[end..][0..vector_len];\n                const chunk: Vec = data.*;\n                // Look for CR.\n                const match = chunk == cr;\n\n                // Create a bitset out of match vector.\n                const bitset = std.bit_set.IntegerBitSet(vector_len){\n                    .mask = @bitCast(@intFromBool(match)),\n                };\n\n                var iter = bitset.iterator(.{});\n                var relative_start: usize = 0;\n                while (iter.next()) |index| {\n                    _ = try writer.writeVec(&.{ data[relative_start..index], \"\\n\" });\n\n                    if (index + 1 != data.len and data[index + 1] == '\\n') {\n                        relative_start = index + 2;\n                    } else {\n                        relative_start = index + 1;\n                    }\n                }\n\n                _ = try writer.writeVec(&.{data[relative_start..]});\n            }\n        }\n\n        // Scalar scan fallback.\n        var relative_start: usize = end;\n        while (end < part.len) {\n            if (part[end] == '\\r') {\n                _ = try writer.writeVec(&.{ part[relative_start..end], \"\\n\" });\n\n                // Part ends with CR. We can continue to next part.\n                if (end + 1 == part.len) {\n                    continue :scan_parts;\n                }\n\n                // If next char is LF, skip it too.\n                if (part[end + 1] == '\\n') {\n                    relative_start = end + 2;\n                } else {\n                    relative_start = end + 1;\n                }\n            }\n\n            end += 1;\n        }\n\n        // Write the remaining. We get this in such situations:\n        // `the quick brown\\rfox`\n        // `the quick brown\\r\\nfox`\n        try writer.writeAll(part[relative_start..end]);\n    }\n}\n\n/// Returns a Promise that resolves with the contents of the blob\n/// as binary data contained in an ArrayBuffer.\npub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice });\n}\n\nconst ReadableStream = @import(\"streams/ReadableStream.zig\");\n/// Returns a ReadableStream which upon reading returns the data\n/// contained within the Blob.\npub fn stream(self: *const Blob, page: *Page) !*ReadableStream {\n    return ReadableStream.initWithData(self._slice, page);\n}\n\n/// Returns a Promise that resolves with a string containing\n/// the contents of the blob, interpreted as UTF-8.\npub fn text(self: *const Blob, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(self._slice);\n}\n\n/// Extension to Blob; works on Firefox and Safari.\n/// https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes\n/// Returns a Promise that resolves with a Uint8Array containing\n/// the contents of the blob as an array of bytes.\npub fn bytes(self: *const Blob, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice });\n}\n\n/// Returns a new Blob object which contains data\n/// from a subset of the blob on which it's called.\npub fn slice(\n    self: *const Blob,\n    start_: ?i32,\n    end_: ?i32,\n    content_type_: ?[]const u8,\n    page: *Page,\n) !*Blob {\n    const data = self._slice;\n\n    const start = blk: {\n        const requested_start = start_ orelse break :blk 0;\n        if (requested_start < 0) {\n            break :blk data.len -| @abs(requested_start);\n        }\n        break :blk @min(data.len, @as(u31, @intCast(requested_start)));\n    };\n\n    const end: usize = blk: {\n        const requested_end = end_ orelse break :blk data.len;\n        if (requested_end < 0) {\n            break :blk @max(start, data.len -| @abs(requested_end));\n        }\n\n        break :blk @min(data.len, @max(start, @as(u31, @intCast(requested_end))));\n    };\n\n    return Blob.init(&.{data[start..end]}, .{ .type = content_type_ orelse \"\" }, page);\n}\n\n/// Returns the size of the Blob in bytes.\npub fn getSize(self: *const Blob) usize {\n    return self._slice.len;\n}\n\n/// Returns the type of Blob; likely a MIME type, yet anything can be given.\npub fn getType(self: *const Blob) []const u8 {\n    return self._mime;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Blob);\n\n    pub const Meta = struct {\n        pub const name = \"Blob\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(Blob.deinit);\n    };\n\n    pub const constructor = bridge.constructor(Blob.init, .{});\n    pub const text = bridge.function(Blob.text, .{});\n    pub const bytes = bridge.function(Blob.bytes, .{});\n    pub const slice = bridge.function(Blob.slice, .{});\n    pub const size = bridge.accessor(Blob.getSize, null, .{});\n    pub const @\"type\" = bridge.accessor(Blob.getType, null, .{});\n    pub const stream = bridge.function(Blob.stream, .{});\n    pub const arrayBuffer = bridge.function(Blob.arrayBuffer, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Blob\" {\n    try testing.htmlRunner(\"blob.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/CData.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Node = @import(\"Node.zig\");\npub const Text = @import(\"cdata/Text.zig\");\npub const Comment = @import(\"cdata/Comment.zig\");\npub const CDATASection = @import(\"cdata/CDATASection.zig\");\npub const ProcessingInstruction = @import(\"cdata/ProcessingInstruction.zig\");\n\nconst CData = @This();\n\n_type: Type,\n_proto: *Node,\n_data: String = .empty,\n\n/// Count UTF-16 code units in a UTF-8 string.\n/// 4-byte UTF-8 sequences (codepoints >= U+10000) produce 2 UTF-16 code units (surrogate pair),\n/// everything else produces 1.\npub fn utf16Len(data: []const u8) usize {\n    var count: usize = 0;\n    var i: usize = 0;\n    while (i < data.len) {\n        const byte = data[i];\n        const seq_len = std.unicode.utf8ByteSequenceLength(byte) catch {\n            // Invalid UTF-8 byte — count as 1 code unit, advance 1 byte\n            i += 1;\n            count += 1;\n            continue;\n        };\n        if (i + seq_len > data.len) {\n            // Truncated sequence\n            count += 1;\n            i += 1;\n            continue;\n        }\n        if (seq_len == 4) {\n            count += 2; // surrogate pair\n        } else {\n            count += 1;\n        }\n        i += seq_len;\n    }\n    return count;\n}\n\n/// Convert a UTF-16 code unit offset to a UTF-8 byte offset.\n/// Returns IndexSizeError if utf16_offset > utf16 length of data.\npub fn utf16OffsetToUtf8(data: []const u8, utf16_offset: usize) error{IndexSizeError}!usize {\n    var utf16_pos: usize = 0;\n    var i: usize = 0;\n    while (i < data.len) {\n        if (utf16_pos == utf16_offset) return i;\n        const byte = data[i];\n        const seq_len = std.unicode.utf8ByteSequenceLength(byte) catch {\n            i += 1;\n            utf16_pos += 1;\n            continue;\n        };\n        if (i + seq_len > data.len) {\n            utf16_pos += 1;\n            i += 1;\n            continue;\n        }\n        if (seq_len == 4) {\n            utf16_pos += 2;\n        } else {\n            utf16_pos += 1;\n        }\n        i += seq_len;\n    }\n    // At end of string — valid only if offset equals total length\n    if (utf16_pos == utf16_offset) return i;\n    return error.IndexSizeError;\n}\n\n/// Convert a UTF-16 code unit range to UTF-8 byte offsets in a single pass.\n/// Returns IndexSizeError if utf16_start > utf16 length of data.\n/// Clamps utf16_end to the actual string length if it exceeds it.\nfn utf16RangeToUtf8(data: []const u8, utf16_start: usize, utf16_end: usize) !struct { start: usize, end: usize } {\n    var i: usize = 0;\n    var utf16_pos: usize = 0;\n    var byte_start: ?usize = null;\n\n    while (i < data.len) {\n        // Record start offset when we reach it\n        if (utf16_pos == utf16_start) {\n            byte_start = i;\n        }\n        // If we've found start and reached end, return both\n        if (utf16_pos == utf16_end and byte_start != null) {\n            return .{ .start = byte_start.?, .end = i };\n        }\n\n        const byte = data[i];\n        const seq_len = std.unicode.utf8ByteSequenceLength(byte) catch {\n            i += 1;\n            utf16_pos += 1;\n            continue;\n        };\n        if (i + seq_len > data.len) {\n            utf16_pos += 1;\n            i += 1;\n            continue;\n        }\n        utf16_pos += if (seq_len == 4) 2 else 1;\n        i += seq_len;\n    }\n\n    // At end of string\n    if (utf16_pos == utf16_start) {\n        byte_start = i;\n    }\n    const start = byte_start orelse return error.IndexSizeError;\n    // End is either exactly at utf16_end or clamped to string end\n    return .{ .start = start, .end = i };\n}\n\npub const Type = union(enum) {\n    text: Text,\n    comment: Comment,\n    // This should be under Text, but that would require storing a _type union\n    // in text, which would add 8 bytes to every text node.\n    cdata_section: CDATASection,\n    processing_instruction: *ProcessingInstruction,\n};\n\npub fn asNode(self: *CData) *Node {\n    return self._proto;\n}\n\npub fn is(self: *CData, comptime T: type) ?*T {\n    inline for (@typeInfo(Type).@\"union\".fields) |f| {\n        if (@field(Type, f.name) == self._type) {\n            if (f.type == T) {\n                return &@field(self._type, f.name);\n            }\n            if (f.type == *T) {\n                return @field(self._type, f.name);\n            }\n        }\n    }\n    return null;\n}\n\npub fn getData(self: *const CData) String {\n    return self._data;\n}\n\npub const RenderOpts = struct {\n    trim_left: bool = true,\n    trim_right: bool = true,\n};\n// Replace successives whitespaces with one withespace.\n// Trims left and right according to the options.\n// Returns true if the string ends with a trimmed whitespace.\npub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !bool {\n    var start: usize = 0;\n    var prev_w: ?bool = null;\n    var is_w: bool = undefined;\n    const s = self._data.str();\n\n    for (s, 0..) |c, i| {\n        is_w = std.ascii.isWhitespace(c);\n\n        // Detect the first char type.\n        if (prev_w == null) {\n            prev_w = is_w;\n        }\n        // The current char is the same kind of char, the chunk continues.\n        if (prev_w.? == is_w) {\n            continue;\n        }\n\n        // Starting here, the chunk changed.\n        if (is_w) {\n            // We have a chunk of non-whitespaces, we write it as it.\n            try writer.writeAll(s[start..i]);\n        } else {\n            // We have a chunk of whitespaces, replace with one space,\n            // depending the position.\n            if (start > 0 or !opts.trim_left) {\n                try writer.writeByte(' ');\n            }\n        }\n        // Start the new chunk.\n        prev_w = is_w;\n        start = i;\n    }\n    // Write the reminder chunk.\n    if (is_w) {\n        // Last chunk is whitespaces.\n        // If the string contains only whitespaces, don't write it.\n        if (start > 0 and opts.trim_right == false) {\n            try writer.writeByte(' ');\n        } else {\n            return true;\n        }\n    } else {\n        // last chunk is non whitespaces.\n        try writer.writeAll(s[start..]);\n    }\n\n    return false;\n}\n\npub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {\n    const old_value = self._data;\n\n    if (value) |v| {\n        self._data = try page.dupeSSO(v);\n    } else {\n        self._data = .empty;\n    }\n\n    page.characterDataChange(self.asNode(), old_value);\n}\n\n/// JS bridge wrapper for `data` setter.\n/// Per spec, setting .data runs replaceData(0, this.length, value),\n/// which includes live range updates.\n/// Handles [LegacyNullToEmptyString]: null → \"\" per spec.\npub fn _setData(self: *CData, value: js.Value, page: *Page) !void {\n    const new_value: []const u8 = if (value.isNull()) \"\" else try value.toZig([]const u8);\n    const length = self.getLength();\n    try self.replaceData(0, length, new_value, page);\n}\n\npub fn format(self: *const CData, writer: *std.io.Writer) !void {\n    return switch (self._type) {\n        .text => writer.print(\"<text>{f}</text>\", .{self._data}),\n        .comment => writer.print(\"<!-- {f} -->\", .{self._data}),\n        .cdata_section => writer.print(\"<![CDATA[{f}]]>\", .{self._data}),\n        .processing_instruction => |pi| writer.print(\"<?{s} {f}?>\", .{ pi._target, self._data }),\n    };\n}\n\npub fn getLength(self: *const CData) usize {\n    return utf16Len(self._data.str());\n}\n\npub fn isEqualNode(self: *const CData, other: *const CData) bool {\n    if (std.meta.activeTag(self._type) != std.meta.activeTag(other._type)) {\n        return false;\n    }\n\n    if (self._type == .processing_instruction) {\n        @branchHint(.unlikely);\n        if (std.mem.eql(u8, self._type.processing_instruction._target, other._type.processing_instruction._target) == false) {\n            return false;\n        }\n        // if the _targets are equal, we still want to compare the data\n    }\n\n    return self._data.eql(other._data);\n}\n\npub fn appendData(self: *CData, data: []const u8, page: *Page) !void {\n    // Per DOM spec, appendData(data) is replaceData(length, 0, data).\n    const length = self.getLength();\n    try self.replaceData(length, 0, data, page);\n}\n\npub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void {\n    const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);\n    const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);\n\n    // Update live ranges per DOM spec replaceData steps (deleteData = replaceData with data=\"\")\n    const length = self.getLength();\n    const effective_count: u32 = @intCast(@min(count, length - offset));\n    page.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), effective_count, 0);\n\n    const old_data = self._data;\n    const old_value = old_data.str();\n    if (range.start == 0) {\n        self._data = try page.dupeSSO(old_value[range.end..]);\n    } else if (range.end >= old_value.len) {\n        self._data = try page.dupeSSO(old_value[0..range.start]);\n    } else {\n        // Deleting from middle - concat prefix and suffix\n        self._data = try String.concat(page.arena, &.{\n            old_value[0..range.start],\n            old_value[range.end..],\n        });\n    }\n    page.characterDataChange(self.asNode(), old_data);\n}\n\npub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void {\n    const byte_offset = try utf16OffsetToUtf8(self._data.str(), offset);\n\n    // Update live ranges per DOM spec replaceData steps (insertData = replaceData with count=0)\n    page.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), 0, @intCast(utf16Len(data)));\n\n    const old_value = self._data;\n    const existing = old_value.str();\n    self._data = try String.concat(page.arena, &.{\n        existing[0..byte_offset],\n        data,\n        existing[byte_offset..],\n    });\n    page.characterDataChange(self.asNode(), old_value);\n}\n\npub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void {\n    const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);\n    const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);\n\n    // Update live ranges per DOM spec replaceData steps\n    const length = self.getLength();\n    const effective_count: u32 = @intCast(@min(count, length - offset));\n    page.updateRangesForCharacterDataReplace(self.asNode(), @intCast(offset), effective_count, @intCast(utf16Len(data)));\n\n    const old_value = self._data;\n    const existing = old_value.str();\n    self._data = try String.concat(page.arena, &.{\n        existing[0..range.start],\n        data,\n        existing[range.end..],\n    });\n    page.characterDataChange(self.asNode(), old_value);\n}\n\npub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {\n    const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);\n    const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);\n    return self._data.str()[range.start..range.end];\n}\n\npub fn remove(self: *CData, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node.parentNode() orelse return;\n    _ = try parent.removeChild(node, page);\n}\n\npub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node.parentNode() orelse return;\n\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        _ = try parent.insertBefore(child, node, page);\n    }\n}\n\npub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node.parentNode() orelse return;\n    const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);\n\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        _ = try parent.insertBefore(child, viable_next, page);\n    }\n}\n\npub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const ref_node = self.asNode();\n    const parent = ref_node.parentNode() orelse return;\n\n    var rm_ref_node = true;\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        if (child == ref_node) {\n            rm_ref_node = false;\n            continue;\n        }\n        _ = try parent.insertBefore(child, ref_node, page);\n    }\n\n    if (rm_ref_node) {\n        _ = try parent.removeChild(ref_node, page);\n    }\n}\n\npub fn nextElementSibling(self: *CData) ?*Node.Element {\n    var maybe_sibling = self.asNode().nextSibling();\n    while (maybe_sibling) |sibling| {\n        if (sibling.is(Node.Element)) |el| return el;\n        maybe_sibling = sibling.nextSibling();\n    }\n    return null;\n}\n\npub fn previousElementSibling(self: *CData) ?*Node.Element {\n    var maybe_sibling = self.asNode().previousSibling();\n    while (maybe_sibling) |sibling| {\n        if (sibling.is(Node.Element)) |el| return el;\n        maybe_sibling = sibling.previousSibling();\n    }\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CData);\n\n    pub const Meta = struct {\n        pub const name = \"CharacterData\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const data = bridge.accessor(CData.getData, CData._setData, .{});\n    pub const length = bridge.accessor(CData.getLength, null, .{});\n\n    pub const appendData = bridge.function(CData.appendData, .{});\n    pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true });\n    pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true });\n    pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true });\n    pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true });\n\n    pub const remove = bridge.function(CData.remove, .{});\n    pub const before = bridge.function(CData.before, .{});\n    pub const after = bridge.function(CData.after, .{});\n    pub const replaceWith = bridge.function(CData.replaceWith, .{});\n\n    pub const nextElementSibling = bridge.accessor(CData.nextElementSibling, null, .{});\n    pub const previousElementSibling = bridge.accessor(CData.previousElementSibling, null, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: CData\" {\n    try testing.htmlRunner(\"cdata\", .{});\n}\n\ntest \"WebApi: CData.render\" {\n    const allocator = std.testing.allocator;\n\n    const TestCase = struct {\n        value: []const u8,\n        expected: []const u8,\n        result: bool = false,\n        opts: RenderOpts = .{},\n    };\n\n    const test_cases = [_]TestCase{\n        .{ .value = \"   \", .expected = \"\", .result = true },\n        .{ .value = \"   \", .expected = \"\", .opts = .{ .trim_left = false, .trim_right = false }, .result = true },\n        .{ .value = \"foo bar\", .expected = \"foo bar\" },\n        .{ .value = \"foo  bar\", .expected = \"foo bar\" },\n        .{ .value = \"  foo bar\", .expected = \"foo bar\" },\n        .{ .value = \"foo bar  \", .expected = \"foo bar\", .result = true },\n        .{ .value = \"  foo  bar  \", .expected = \"foo bar\", .result = true },\n        .{ .value = \"foo\\n\\tbar\", .expected = \"foo bar\" },\n        .{ .value = \"\\tfoo bar   baz   \\t\\n yeah\\r\\n\", .expected = \"foo bar baz yeah\", .result = true },\n        .{ .value = \"  foo bar\", .expected = \" foo bar\", .opts = .{ .trim_left = false } },\n        .{ .value = \"foo bar  \", .expected = \"foo bar \", .opts = .{ .trim_right = false } },\n        .{ .value = \"  foo bar  \", .expected = \" foo bar \", .opts = .{ .trim_left = false, .trim_right = false } },\n    };\n\n    var buffer = std.io.Writer.Allocating.init(allocator);\n    defer buffer.deinit();\n    for (test_cases) |test_case| {\n        buffer.clearRetainingCapacity();\n\n        const cdata = CData{\n            ._type = .{ .text = undefined },\n            ._proto = undefined,\n            ._data = .wrap(test_case.value),\n        };\n\n        const result = try cdata.render(&buffer.writer, test_case.opts);\n\n        try std.testing.expectEqualStrings(test_case.expected, buffer.written());\n        try std.testing.expect(result == test_case.result);\n    }\n}\n\ntest \"utf16Len\" {\n    // ASCII: 1 byte = 1 code unit each\n    try std.testing.expectEqual(@as(usize, 0), utf16Len(\"\"));\n    try std.testing.expectEqual(@as(usize, 5), utf16Len(\"hello\"));\n    // CJK: 3 bytes UTF-8 = 1 UTF-16 code unit each\n    try std.testing.expectEqual(@as(usize, 2), utf16Len(\"資料\")); // 6 bytes, 2 code units\n    // Emoji U+1F320: 4 bytes UTF-8 = 2 UTF-16 code units (surrogate pair)\n    try std.testing.expectEqual(@as(usize, 2), utf16Len(\"🌠\")); // 4 bytes, 2 code units\n    // Mixed: 🌠(2) + \" test \"(6) + 🌠(2) + \" TEST\"(5) = 15\n    try std.testing.expectEqual(@as(usize, 15), utf16Len(\"🌠 test 🌠 TEST\"));\n    // 2-byte UTF-8 (e.g. é U+00E9): 1 UTF-16 code unit\n    try std.testing.expectEqual(@as(usize, 4), utf16Len(\"café\")); // c(1) + a(1) + f(1) + é(1)\n}\n\ntest \"utf16OffsetToUtf8\" {\n    // ASCII: offsets map 1:1\n    try std.testing.expectEqual(@as(usize, 0), try utf16OffsetToUtf8(\"hello\", 0));\n    try std.testing.expectEqual(@as(usize, 3), try utf16OffsetToUtf8(\"hello\", 3));\n    try std.testing.expectEqual(@as(usize, 5), try utf16OffsetToUtf8(\"hello\", 5)); // end\n    try std.testing.expectError(error.IndexSizeError, utf16OffsetToUtf8(\"hello\", 6)); // past end\n\n    // CJK \"資料\" (6 bytes, 2 UTF-16 code units)\n    try std.testing.expectEqual(@as(usize, 0), try utf16OffsetToUtf8(\"資料\", 0)); // before 資\n    try std.testing.expectEqual(@as(usize, 3), try utf16OffsetToUtf8(\"資料\", 1)); // before 料\n    try std.testing.expectEqual(@as(usize, 6), try utf16OffsetToUtf8(\"資料\", 2)); // end\n    try std.testing.expectError(error.IndexSizeError, utf16OffsetToUtf8(\"資料\", 3));\n\n    // Emoji \"🌠AB\" (4+1+1 = 6 bytes; 2+1+1 = 4 UTF-16 code units)\n    try std.testing.expectEqual(@as(usize, 0), try utf16OffsetToUtf8(\"🌠AB\", 0)); // before 🌠\n    // offset 1 lands inside the surrogate pair — still valid UTF-16 offset\n    try std.testing.expectEqual(@as(usize, 4), try utf16OffsetToUtf8(\"🌠AB\", 2)); // before A\n    try std.testing.expectEqual(@as(usize, 5), try utf16OffsetToUtf8(\"🌠AB\", 3)); // before B\n    try std.testing.expectEqual(@as(usize, 6), try utf16OffsetToUtf8(\"🌠AB\", 4)); // end\n\n    // Empty string: only offset 0 is valid\n    try std.testing.expectEqual(@as(usize, 0), try utf16OffsetToUtf8(\"\", 0));\n    try std.testing.expectError(error.IndexSizeError, utf16OffsetToUtf8(\"\", 1));\n}\n\ntest \"utf16RangeToUtf8\" {\n    // ASCII: basic range\n    {\n        const result = try utf16RangeToUtf8(\"hello\", 1, 4);\n        try std.testing.expectEqual(@as(usize, 1), result.start);\n        try std.testing.expectEqual(@as(usize, 4), result.end);\n    }\n\n    // ASCII: range to end\n    {\n        const result = try utf16RangeToUtf8(\"hello\", 2, 5);\n        try std.testing.expectEqual(@as(usize, 2), result.start);\n        try std.testing.expectEqual(@as(usize, 5), result.end);\n    }\n\n    // ASCII: range past end (should clamp)\n    {\n        const result = try utf16RangeToUtf8(\"hello\", 2, 100);\n        try std.testing.expectEqual(@as(usize, 2), result.start);\n        try std.testing.expectEqual(@as(usize, 5), result.end); // clamped\n    }\n\n    // ASCII: full range\n    {\n        const result = try utf16RangeToUtf8(\"hello\", 0, 5);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 5), result.end);\n    }\n\n    // ASCII: start past end\n    try std.testing.expectError(error.IndexSizeError, utf16RangeToUtf8(\"hello\", 6, 10));\n\n    // CJK \"資料\" (6 bytes, 2 UTF-16 code units)\n    {\n        const result = try utf16RangeToUtf8(\"資料\", 0, 1);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 3), result.end); // after 資\n    }\n\n    {\n        const result = try utf16RangeToUtf8(\"資料\", 1, 2);\n        try std.testing.expectEqual(@as(usize, 3), result.start); // before 料\n        try std.testing.expectEqual(@as(usize, 6), result.end); // end\n    }\n\n    {\n        const result = try utf16RangeToUtf8(\"資料\", 0, 2);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 6), result.end);\n    }\n\n    // Emoji \"🌠AB\" (4+1+1 = 6 bytes; 2+1+1 = 4 UTF-16 code units)\n    {\n        const result = try utf16RangeToUtf8(\"🌠AB\", 0, 2);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 4), result.end); // after 🌠\n    }\n\n    {\n        const result = try utf16RangeToUtf8(\"🌠AB\", 2, 3);\n        try std.testing.expectEqual(@as(usize, 4), result.start); // before A\n        try std.testing.expectEqual(@as(usize, 5), result.end); // before B\n    }\n\n    {\n        const result = try utf16RangeToUtf8(\"🌠AB\", 0, 4);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 6), result.end);\n    }\n\n    // Empty string\n    {\n        const result = try utf16RangeToUtf8(\"\", 0, 0);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 0), result.end);\n    }\n\n    {\n        const result = try utf16RangeToUtf8(\"\", 0, 100);\n        try std.testing.expectEqual(@as(usize, 0), result.start);\n        try std.testing.expectEqual(@as(usize, 0), result.end); // clamped\n    }\n\n    // Mixed \"🌠 test 🌠\" (4+1+4+1+4 = 14 bytes; 2+1+4+1+2 = 10 UTF-16 code units)\n    {\n        const result = try utf16RangeToUtf8(\"🌠 test 🌠\", 3, 7);\n        try std.testing.expectEqual(@as(usize, 5), result.start); // before 'test'\n        try std.testing.expectEqual(@as(usize, 9), result.end); // after 'test', before second space\n    }\n}\n"
  },
  {
    "path": "src/browser/webapi/CSS.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst CSS = @This();\n_pad: bool = false,\n\npub const init: CSS = .{};\n\npub fn parseDimension(value: []const u8) ?f64 {\n    if (value.len == 0) {\n        return null;\n    }\n\n    var num_str = value;\n    if (std.mem.endsWith(u8, value, \"px\")) {\n        num_str = value[0 .. value.len - 2];\n    }\n\n    return std.fmt.parseFloat(f64, num_str) catch null;\n}\n\n/// Escapes a CSS identifier string\n/// https://drafts.csswg.org/cssom/#the-css.escape()-method\npub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {\n    if (value.len == 0) {\n        return \"\";\n    }\n\n    const first = value[0];\n    if (first == '-' and value.len == 1) {\n        return \"\\\\-\";\n    }\n\n    // Count how many characters we need for the output\n    var out_len: usize = escapeLen(true, first);\n    for (value[1..], 0..) |c, i| {\n        // Second char (i==0) is a digit and first is '-', needs hex escape\n        if (i == 0 and first == '-' and c >= '0' and c <= '9') {\n            out_len += 2 + hexDigitsNeeded(c);\n        } else {\n            out_len += escapeLen(false, c);\n        }\n    }\n\n    if (out_len == value.len) {\n        return value;\n    }\n\n    const result = try page.call_arena.alloc(u8, out_len);\n    var pos: usize = 0;\n\n    if (needsEscape(true, first)) {\n        pos = writeEscape(true, result, first);\n    } else {\n        result[0] = first;\n        pos = 1;\n    }\n\n    for (value[1..], 0..) |c, i| {\n        // Second char (i==0) is a digit and first is '-', needs hex escape\n        if (i == 0 and first == '-' and c >= '0' and c <= '9') {\n            result[pos] = '\\\\';\n            const hex_str = std.fmt.bufPrint(result[pos + 1 ..], \"{x} \", .{c}) catch unreachable;\n            pos += 1 + hex_str.len;\n        } else if (!needsEscape(false, c)) {\n            result[pos] = c;\n            pos += 1;\n        } else {\n            pos += writeEscape(false, result[pos..], c);\n        }\n    }\n\n    return result;\n}\n\npub fn supports(_: *const CSS, property_or_condition: []const u8, value: ?[]const u8) bool {\n    _ = property_or_condition;\n    _ = value;\n    return true;\n}\n\nfn escapeLen(comptime is_first: bool, c: u8) usize {\n    if (needsEscape(is_first, c) == false) {\n        return 1;\n    }\n    if (c == 0) {\n        return \"\\u{FFFD}\".len;\n    }\n    if (isHexEscape(c) or ((comptime is_first) and c >= '0' and c <= '9')) {\n        // Will be escaped as \\XX (backslash + 1-6 hex digits + space)\n        return 2 + hexDigitsNeeded(c);\n    }\n    // Escaped as \\C (backslash + character)\n    return 2;\n}\n\nfn needsEscape(comptime is_first: bool, c: u8) bool {\n    if (comptime is_first) {\n        if (c >= '0' and c <= '9') {\n            return true;\n        }\n    }\n\n    // Characters that need escaping\n    return switch (c) {\n        0...0x1F, 0x7F => true,\n        '!', '\"', '#', '$', '%', '&', '\\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\\\', ']', '^', '`', '{', '|', '}', '~' => true,\n        ' ' => true,\n        else => false,\n    };\n}\n\nfn isHexEscape(c: u8) bool {\n    return (c >= 0x00 and c <= 0x1F) or c == 0x7F;\n}\n\nfn hexDigitsNeeded(c: u8) usize {\n    if (c < 0x10) {\n        return 1;\n    }\n    return 2;\n}\n\nfn writeEscape(comptime is_first: bool, buf: []u8, c: u8) usize {\n    if (c == 0) {\n        // NULL character becomes replacement character (no backslash)\n        const replacement = \"\\u{FFFD}\";\n        @memcpy(buf[0..replacement.len], replacement);\n        return replacement.len;\n    }\n\n    buf[0] = '\\\\';\n    var data = buf[1..];\n\n    if (isHexEscape(c) or ((comptime is_first) and c >= '0' and c <= '9')) {\n        const hex_str = std.fmt.bufPrint(data, \"{x} \", .{c}) catch unreachable;\n        return 1 + hex_str.len;\n    }\n\n    data[0] = c;\n    return 2;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSS);\n\n    pub const Meta = struct {\n        pub const name = \"Css\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const escape = bridge.function(CSS.escape, .{});\n    pub const supports = bridge.function(CSS.supports, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: CSS\" {\n    try testing.htmlRunner(\"css.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/Console.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst logger = @import(\"../../log.zig\");\n\nconst Console = @This();\n\n_timers: std.StringHashMapUnmanaged(u64) = .{},\n_counts: std.StringHashMapUnmanaged(u64) = .{},\n\npub const init: Console = .{};\n\npub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {\n    logger.debug(.js, \"console.trace\", .{\n        .stack = page.js.local.?.stackTrace() catch \"???\",\n        .args = ValueWriter{ .page = page, .values = values },\n    });\n}\n\npub fn debug(_: *const Console, values: []js.Value, page: *Page) void {\n    logger.debug(.js, \"console.debug\", .{ValueWriter{ .page = page, .values = values }});\n}\n\npub fn info(_: *const Console, values: []js.Value, page: *Page) void {\n    logger.info(.js, \"console.info\", .{ValueWriter{ .page = page, .values = values }});\n}\n\npub fn log(_: *const Console, values: []js.Value, page: *Page) void {\n    logger.info(.js, \"console.log\", .{ValueWriter{ .page = page, .values = values }});\n}\n\npub fn warn(_: *const Console, values: []js.Value, page: *Page) void {\n    logger.warn(.js, \"console.warn\", .{ValueWriter{ .page = page, .values = values }});\n}\n\npub fn clear(_: *const Console) void {}\n\npub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, page: *Page) void {\n    if (assertion.toBool()) {\n        return;\n    }\n    logger.warn(.js, \"console.assert\", .{ValueWriter{ .page = page, .values = values }});\n}\n\npub fn @\"error\"(_: *const Console, values: []js.Value, page: *Page) void {\n    logger.warn(.js, \"console.error\", .{ValueWriter{ .page = page, .values = values, .include_stack = true }});\n}\n\npub fn table(_: *const Console, data: js.Value, columns: ?js.Value) void {\n    logger.info(.js, \"console.table\", .{ .data = data, .columns = columns });\n}\n\npub fn count(self: *Console, label_: ?[]const u8, page: *Page) !void {\n    const label = label_ orelse \"default\";\n    const gop = try self._counts.getOrPut(page.arena, label);\n\n    var current: u64 = 0;\n    if (gop.found_existing) {\n        current = gop.value_ptr.*;\n    } else {\n        gop.key_ptr.* = try page.dupeString(label);\n    }\n\n    const c = current + 1;\n    gop.value_ptr.* = c;\n\n    logger.info(.js, \"console.count\", .{ .label = label, .count = c });\n}\n\npub fn countReset(self: *Console, label_: ?[]const u8) !void {\n    const label = label_ orelse \"default\";\n    const kv = self._counts.fetchRemove(label) orelse {\n        logger.info(.js, \"console.countReset\", .{ .label = label, .err = \"invalid label\" });\n        return;\n    };\n    logger.info(.js, \"console.countReset\", .{ .label = label, .count = kv.value });\n}\n\npub fn time(self: *Console, label_: ?[]const u8, page: *Page) !void {\n    const label = label_ orelse \"default\";\n    const gop = try self._timers.getOrPut(page.arena, label);\n\n    if (gop.found_existing) {\n        logger.info(.js, \"console.time\", .{ .label = label, .err = \"duplicate timer\" });\n        return;\n    }\n    gop.key_ptr.* = try page.dupeString(label);\n    gop.value_ptr.* = timestamp();\n}\n\npub fn timeLog(self: *Console, label_: ?[]const u8) void {\n    const elapsed = timestamp();\n    const label = label_ orelse \"default\";\n    const start = self._timers.get(label) orelse {\n        logger.info(.js, \"console.timeLog\", .{ .label = label, .err = \"invalid timer\" });\n        return;\n    };\n    logger.info(.js, \"console.timeLog\", .{ .label = label, .elapsed = elapsed - start });\n}\n\npub fn timeEnd(self: *Console, label_: ?[]const u8) void {\n    const elapsed = timestamp();\n    const label = label_ orelse \"default\";\n    const kv = self._timers.fetchRemove(label) orelse {\n        logger.info(.js, \"console.timeEnd\", .{ .label = label, .err = \"invalid timer\" });\n        return;\n    };\n\n    logger.info(.js, \"console.timeEnd\", .{ .label = label, .elapsed = elapsed - kv.value });\n}\n\nfn timestamp() u64 {\n    return @import(\"../../datetime.zig\").timestamp(.monotonic);\n}\n\nconst ValueWriter = struct {\n    page: *Page,\n    values: []js.Value,\n    include_stack: bool = false,\n\n    pub fn format(self: ValueWriter, writer: *std.io.Writer) !void {\n        for (self.values, 1..) |value, i| {\n            try writer.print(\"\\n  arg({d}): {f}\", .{ i, value });\n        }\n        if (self.include_stack) {\n            try writer.print(\"\\n stack: {s}\", .{self.page.js.local.?.stackTrace() catch |err| @errorName(err) orelse \"???\"});\n        }\n    }\n\n    pub fn logFmt(self: ValueWriter, _: []const u8, writer: anytype) !void {\n        var buf: [32]u8 = undefined;\n        for (self.values, 0..) |value, i| {\n            const name = try std.fmt.bufPrint(&buf, \"param.{d}\", .{i});\n            try writer.write(name, value);\n        }\n    }\n\n    pub fn jsonStringify(self: ValueWriter, writer: *std.json.Stringify) !void {\n        try writer.beginArray();\n        for (self.values) |value| {\n            try writer.write(value);\n        }\n        return writer.endArray();\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Console);\n\n    pub const Meta = struct {\n        pub const name = \"Console\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const trace = bridge.function(Console.trace, .{});\n    pub const debug = bridge.function(Console.debug, .{});\n    pub const info = bridge.function(Console.info, .{});\n    pub const log = bridge.function(Console.log, .{});\n    pub const warn = bridge.function(Console.warn, .{});\n    pub const clear = bridge.function(Console.clear, .{ .noop = true });\n    pub const assert = bridge.function(Console.assert, .{});\n    pub const @\"error\" = bridge.function(Console.@\"error\", .{});\n    pub const exception = bridge.function(Console.@\"error\", .{});\n    pub const table = bridge.function(Console.table, .{});\n    pub const count = bridge.function(Console.count, .{});\n    pub const countReset = bridge.function(Console.countReset, .{});\n    pub const time = bridge.function(Console.time, .{});\n    pub const timeLog = bridge.function(Console.timeLog, .{});\n    pub const timeEnd = bridge.function(Console.timeEnd, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Console\" {\n    try testing.htmlRunner(\"console\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/Crypto.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst SubtleCrypto = @import(\"SubtleCrypto.zig\");\n\nconst Crypto = @This();\n_subtle: SubtleCrypto = .{},\n\npub const init: Crypto = .{};\n\n// We take a js.Value, because we want to return the same instance, not a new\n// TypedArray\npub fn getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {\n    var into = try js_obj.toZig(RandomValues);\n    const buf = into.asBuffer();\n    if (buf.len > 65_536) {\n        return error.QuotaExceeded;\n    }\n    std.crypto.random.bytes(buf);\n    return js_obj;\n}\n\npub fn randomUUID(_: *const Crypto) ![36]u8 {\n    var hex: [36]u8 = undefined;\n    @import(\"../../id.zig\").uuidv4(&hex);\n    return hex;\n}\n\npub fn getSubtle(self: *Crypto) *SubtleCrypto {\n    return &self._subtle;\n}\n\nconst RandomValues = union(enum) {\n    int8: []i8,\n    uint8: []u8,\n    int16: []i16,\n    uint16: []u16,\n    int32: []i32,\n    uint32: []u32,\n    int64: []i64,\n    uint64: []u64,\n\n    fn asBuffer(self: RandomValues) []u8 {\n        return switch (self) {\n            .int8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],\n            .uint8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],\n            .int16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],\n            .uint16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],\n            .int32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],\n            .uint32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],\n            .int64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],\n            .uint64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],\n        };\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Crypto);\n\n    pub const Meta = struct {\n        pub const name = \"Crypto\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{ .dom_exception = true });\n    pub const randomUUID = bridge.function(Crypto.randomUUID, .{});\n    pub const subtle = bridge.accessor(Crypto.getSubtle, null, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Crypto\" {\n    try testing.htmlRunner(\"crypto.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/CustomElementDefinition.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Element = @import(\"Element.zig\");\n\nconst CustomElementDefinition = @This();\n\nname: []const u8,\nconstructor: js.Function.Global,\n\n// TODO: Make this a Map<String>\nobserved_attributes: std.StringHashMapUnmanaged(void) = .{},\n\n// For customized built-in elements, this is the element tag they extend (e.g., .button)\n// For autonomous custom elements, this is null\nextends: ?Element.Tag = null,\n\npub fn isAttributeObserved(self: *const CustomElementDefinition, name: String) bool {\n    return self.observed_attributes.contains(name.str());\n}\n\npub fn isAutonomous(self: *const CustomElementDefinition) bool {\n    return self.extends == null;\n}\n\npub fn isCustomizedBuiltIn(self: *const CustomElementDefinition) bool {\n    return self.extends != null;\n}\n"
  },
  {
    "path": "src/browser/webapi/CustomElementRegistry.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../log.zig\");\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst Element = @import(\"Element.zig\");\nconst DOMException = @import(\"DOMException.zig\");\nconst Custom = @import(\"element/html/Custom.zig\");\nconst CustomElementDefinition = @import(\"CustomElementDefinition.zig\");\n\nconst CustomElementRegistry = @This();\n\n_definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{},\n_when_defined: std.StringHashMapUnmanaged(js.PromiseResolver.Global) = .{},\n\nconst DefineOptions = struct {\n    extends: ?[]const u8 = null,\n};\n\npub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Function, options_: ?DefineOptions, page: *Page) !void {\n    const options = options_ orelse DefineOptions{};\n\n    try validateName(name);\n\n    // Parse and validate extends option\n    const extends_tag: ?Element.Tag = if (options.extends) |extends_name| blk: {\n        const tag = std.meta.stringToEnum(Element.Tag, extends_name) orelse return error.NotSupported;\n\n        // Can't extend custom elements\n        if (tag == .custom) {\n            return error.NotSupported;\n        }\n\n        break :blk tag;\n    } else null;\n\n    const gop = try self._definitions.getOrPut(page.arena, name);\n    if (gop.found_existing) {\n        // Yes, this is the correct error to return when trying to redefine a name\n        return error.NotSupported;\n    }\n\n    const owned_name = try page.dupeString(name);\n\n    const definition = try page._factory.create(CustomElementDefinition{\n        .name = owned_name,\n        .constructor = try constructor.persist(),\n        .extends = extends_tag,\n    });\n\n    // Read observedAttributes static property from constructor\n    if (constructor.getPropertyValue(\"observedAttributes\") catch null) |observed_attrs| {\n        if (observed_attrs.isArray()) {\n            var js_arr = observed_attrs.toArray();\n            for (0..js_arr.len()) |i| {\n                const attr_val = js_arr.get(@intCast(i)) catch continue;\n                const attr_name = attr_val.toStringSliceWithAlloc(page.arena) catch continue;\n                definition.observed_attributes.put(page.arena, attr_name, {}) catch continue;\n            }\n        }\n    }\n\n    gop.key_ptr.* = owned_name;\n    gop.value_ptr.* = definition;\n\n    // Upgrade any undefined custom elements with this name\n    var idx: usize = 0;\n    while (idx < page._undefined_custom_elements.items.len) {\n        const custom = page._undefined_custom_elements.items[idx];\n        if (!custom._tag_name.eqlSlice(name)) {\n            idx += 1;\n            continue;\n        }\n\n        if (!custom.asElement().asNode().isConnected()) {\n            idx += 1;\n            continue;\n        }\n\n        upgradeCustomElement(custom, definition, page) catch {\n            _ = page._undefined_custom_elements.swapRemove(idx);\n            continue;\n        };\n\n        _ = page._undefined_custom_elements.swapRemove(idx);\n    }\n\n    if (self._when_defined.fetchRemove(name)) |entry| {\n        page.js.toLocal(entry.value).resolve(\"whenDefined\", constructor);\n    }\n}\n\npub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function.Global {\n    const definition = self._definitions.get(name) orelse return null;\n    return definition.constructor;\n}\n\npub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {\n    try upgradeNode(self, root, page);\n}\n\npub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {\n    const local = page.js.local.?;\n    if (self._definitions.get(name)) |definition| {\n        return local.resolvePromise(definition.constructor);\n    }\n\n    validateName(name) catch |err| {\n        return local.rejectPromise(DOMException.fromError(err) orelse unreachable);\n    };\n\n    const gop = try self._when_defined.getOrPut(page.arena, name);\n    if (gop.found_existing) {\n        return local.toLocal(gop.value_ptr.*).promise();\n    }\n    errdefer _ = self._when_defined.remove(name);\n    const owned_name = try page.dupeString(name);\n\n    const resolver = local.createPromiseResolver();\n    gop.key_ptr.* = owned_name;\n    gop.value_ptr.* = try resolver.persist();\n\n    return resolver.promise();\n}\n\nfn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {\n    if (node.is(Element)) |element| {\n        try upgradeElement(self, element, page);\n    }\n\n    var it = node.childrenIterator();\n    while (it.next()) |child| {\n        try upgradeNode(self, child, page);\n    }\n}\n\nfn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) !void {\n    const custom = element.is(Custom) orelse {\n        return Custom.checkAndAttachBuiltIn(element, page);\n    };\n\n    if (custom._definition != null) return;\n\n    const name = custom._tag_name.str();\n    const definition = self._definitions.get(name) orelse return;\n\n    try upgradeCustomElement(custom, definition, page);\n}\n\npub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void {\n    custom._definition = definition;\n\n    // Reset callback flags since this is a fresh upgrade\n    custom._connected_callback_invoked = false;\n    custom._disconnected_callback_invoked = false;\n\n    const node = custom.asNode();\n    const prev_upgrading = page._upgrading_element;\n    page._upgrading_element = node;\n    defer page._upgrading_element = prev_upgrading;\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var caught: js.TryCatch.Caught = undefined;\n    _ = ls.toLocal(definition.constructor).newInstance(&caught) catch |err| {\n        log.warn(.js, \"custom element upgrade\", .{ .name = definition.name, .err = err, .caught = caught });\n        return error.CustomElementUpgradeFailed;\n    };\n\n    // Invoke attributeChangedCallback for existing observed attributes\n    var attr_it = custom.asElement().attributeIterator();\n    while (attr_it.next()) |attr| {\n        const name = attr._name;\n        if (definition.isAttributeObserved(name)) {\n            custom.invokeAttributeChangedCallback(name, null, attr._value, page);\n        }\n    }\n\n    if (node.isConnected()) {\n        custom.invokeConnectedCallback(page);\n    }\n}\n\nfn validateName(name: []const u8) !void {\n    if (name.len == 0) {\n        return error.SyntaxError;\n    }\n\n    if (std.mem.indexOf(u8, name, \"-\") == null) {\n        return error.SyntaxError;\n    }\n\n    if (name[0] < 'a' or name[0] > 'z') {\n        return error.SyntaxError;\n    }\n\n    const reserved_names = [_][]const u8{\n        \"annotation-xml\",\n        \"color-profile\",\n        \"font-face\",\n        \"font-face-src\",\n        \"font-face-uri\",\n        \"font-face-format\",\n        \"font-face-name\",\n        \"missing-glyph\",\n    };\n\n    for (reserved_names) |reserved| {\n        if (std.mem.eql(u8, name, reserved)) {\n            return error.SyntaxError;\n        }\n    }\n\n    for (name) |c| {\n        if (c >= 'A' and c <= 'Z') {\n            return error.SyntaxError;\n        }\n\n        // Reject control characters and specific invalid characters\n        // per elementLocalNameRegex: [^\\0\\t\\n\\f\\r\\u0020/>]*\n        switch (c) {\n            0, '\\t', '\\n', '\\r', 0x0C, ' ', '/', '>' => return error.SyntaxError,\n            else => {},\n        }\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CustomElementRegistry);\n\n    pub const Meta = struct {\n        pub const name = \"CustomElementRegistry\";\n        pub var class_id: bridge.ClassId = undefined;\n        pub const prototype_chain = bridge.prototypeChain();\n    };\n\n    pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });\n    pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });\n    pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{});\n    pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{ .dom_exception = true });\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: CustomElementRegistry\" {\n    try testing.htmlRunner(\"custom_elements\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/DOMException.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst DOMException = @This();\n\n_code: Code = .none,\n_custom_name: ?[]const u8 = null,\n_custom_message: ?[]const u8 = null,\n\npub fn init(message: ?[]const u8, name: ?[]const u8) DOMException {\n    // If name is provided, try to map it to a legacy code\n    const code = if (name) |n| Code.fromName(n) else .none;\n    return .{\n        ._code = code,\n        ._custom_name = name,\n        ._custom_message = message,\n    };\n}\n\npub fn fromError(err: anyerror) ?DOMException {\n    return switch (err) {\n        error.SyntaxError => .{ ._code = .syntax_error },\n        error.InvalidCharacterError => .{ ._code = .invalid_character_error },\n        error.NotFound => .{ ._code = .not_found },\n        error.NotSupported => .{ ._code = .not_supported },\n        error.HierarchyError => .{ ._code = .hierarchy_error },\n        error.IndexSizeError => .{ ._code = .index_size_error },\n        error.InvalidStateError => .{ ._code = .invalid_state_error },\n        error.WrongDocument => .{ ._code = .wrong_document_error },\n        error.NoModificationAllowed => .{ ._code = .no_modification_allowed_error },\n        error.InUseAttribute => .{ ._code = .inuse_attribute_error },\n        error.InvalidModification => .{ ._code = .invalid_modification_error },\n        error.NamespaceError => .{ ._code = .namespace_error },\n        error.InvalidAccess => .{ ._code = .invalid_access_error },\n        error.SecurityError => .{ ._code = .security_error },\n        error.NetworkError => .{ ._code = .network_error },\n        error.AbortError => .{ ._code = .abort_error },\n        error.URLMismatch => .{ ._code = .url_mismatch_error },\n        error.QuotaExceeded => .{ ._code = .quota_exceeded_error },\n        error.TimeoutError => .{ ._code = .timeout_error },\n        error.InvalidNodeType => .{ ._code = .invalid_node_type_error },\n        error.DataClone => .{ ._code = .data_clone_error },\n        else => null,\n    };\n}\n\npub fn getCode(self: *const DOMException) u8 {\n    return @intFromEnum(self._code);\n}\n\npub fn getName(self: *const DOMException) []const u8 {\n    if (self._custom_name) |name| {\n        return name;\n    }\n\n    return switch (self._code) {\n        .none => \"Error\",\n        .index_size_error => \"IndexSizeError\",\n        .hierarchy_error => \"HierarchyRequestError\",\n        .wrong_document_error => \"WrongDocumentError\",\n        .invalid_character_error => \"InvalidCharacterError\",\n        .no_modification_allowed_error => \"NoModificationAllowedError\",\n        .not_found => \"NotFoundError\",\n        .not_supported => \"NotSupportedError\",\n        .inuse_attribute_error => \"InUseAttributeError\",\n        .invalid_state_error => \"InvalidStateError\",\n        .syntax_error => \"SyntaxError\",\n        .invalid_modification_error => \"InvalidModificationError\",\n        .namespace_error => \"NamespaceError\",\n        .invalid_access_error => \"InvalidAccessError\",\n        .security_error => \"SecurityError\",\n        .network_error => \"NetworkError\",\n        .abort_error => \"AbortError\",\n        .url_mismatch_error => \"URLMismatchError\",\n        .quota_exceeded_error => \"QuotaExceededError\",\n        .timeout_error => \"TimeoutError\",\n        .invalid_node_type_error => \"InvalidNodeTypeError\",\n        .data_clone_error => \"DataCloneError\",\n    };\n}\n\npub fn getMessage(self: *const DOMException) []const u8 {\n    if (self._custom_message) |msg| {\n        return msg;\n    }\n    return switch (self._code) {\n        .none => \"\",\n        .index_size_error => \"Index or size is negative or greater than the allowed amount\",\n        .hierarchy_error => \"The operation would yield an incorrect node tree\",\n        .wrong_document_error => \"The object is in the wrong document\",\n        .invalid_character_error => \"The string contains invalid characters\",\n        .no_modification_allowed_error => \"The object can not be modified\",\n        .not_found => \"The object can not be found here\",\n        .not_supported => \"The operation is not supported\",\n        .inuse_attribute_error => \"The attribute already in use\",\n        .invalid_state_error => \"The object is in an invalid state\",\n        .syntax_error => \"The string did not match the expected pattern\",\n        .invalid_modification_error => \"The object can not be modified in this way\",\n        .namespace_error => \"The operation is not allowed by Namespaces in XML\",\n        .invalid_access_error => \"The object does not support the operation or argument\",\n        .security_error => \"The operation is insecure\",\n        .network_error => \"A network error occurred\",\n        .abort_error => \"The operation was aborted\",\n        .url_mismatch_error => \"The given URL does not match another URL\",\n        .quota_exceeded_error => \"The quota has been exceeded\",\n        .timeout_error => \"The operation timed out\",\n        .invalid_node_type_error => \"The supplied node is incorrect or has an incorrect ancestor for this operation\",\n        .data_clone_error => \"The object can not be cloned\",\n    };\n}\n\npub fn toString(self: *const DOMException, page: *Page) ![]const u8 {\n    const msg = blk: {\n        if (self._custom_message) |msg| {\n            break :blk msg;\n        }\n        switch (self._code) {\n            .none => return \"Error\",\n            else => break :blk self.getMessage(),\n        }\n    };\n    return std.fmt.bufPrint(&page.buf, \"{s}: {s}\", .{ self.getName(), msg }) catch return msg;\n}\n\nconst Code = enum(u8) {\n    none = 0,\n    index_size_error = 1,\n    hierarchy_error = 3,\n    wrong_document_error = 4,\n    invalid_character_error = 5,\n    no_modification_allowed_error = 7,\n    not_found = 8,\n    not_supported = 9,\n    inuse_attribute_error = 10,\n    invalid_state_error = 11,\n    syntax_error = 12,\n    invalid_modification_error = 13,\n    namespace_error = 14,\n    invalid_access_error = 15,\n    security_error = 18,\n    network_error = 19,\n    abort_error = 20,\n    url_mismatch_error = 21,\n    quota_exceeded_error = 22,\n    timeout_error = 23,\n    invalid_node_type_error = 24,\n    data_clone_error = 25,\n\n    /// Maps a standard error name to its legacy code\n    /// Returns .none (code 0) for non-legacy error names\n    pub fn fromName(name: []const u8) Code {\n        const lookup = std.StaticStringMap(Code).initComptime(.{\n            .{ \"IndexSizeError\", .index_size_error },\n            .{ \"HierarchyRequestError\", .hierarchy_error },\n            .{ \"WrongDocumentError\", .wrong_document_error },\n            .{ \"InvalidCharacterError\", .invalid_character_error },\n            .{ \"NoModificationAllowedError\", .no_modification_allowed_error },\n            .{ \"NotFoundError\", .not_found },\n            .{ \"NotSupportedError\", .not_supported },\n            .{ \"InUseAttributeError\", .inuse_attribute_error },\n            .{ \"InvalidStateError\", .invalid_state_error },\n            .{ \"SyntaxError\", .syntax_error },\n            .{ \"InvalidModificationError\", .invalid_modification_error },\n            .{ \"NamespaceError\", .namespace_error },\n            .{ \"InvalidAccessError\", .invalid_access_error },\n            .{ \"SecurityError\", .security_error },\n            .{ \"NetworkError\", .network_error },\n            .{ \"AbortError\", .abort_error },\n            .{ \"URLMismatchError\", .url_mismatch_error },\n            .{ \"QuotaExceededError\", .quota_exceeded_error },\n            .{ \"TimeoutError\", .timeout_error },\n            .{ \"InvalidNodeTypeError\", .invalid_node_type_error },\n            .{ \"DataCloneError\", .data_clone_error },\n        });\n        return lookup.get(name) orelse .none;\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMException);\n\n    pub const Meta = struct {\n        pub const name = \"DOMException\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(DOMException.init, .{});\n    pub const code = bridge.accessor(DOMException.getCode, null, .{});\n    pub const name = bridge.accessor(DOMException.getName, null, .{});\n    pub const message = bridge.accessor(DOMException.getMessage, null, .{});\n    pub const toString = bridge.function(DOMException.toString, .{});\n\n    // Legacy error code constants (on both prototype and constructor)\n    pub const INDEX_SIZE_ERR = bridge.property(1, .{ .template = true });\n    pub const DOMSTRING_SIZE_ERR = bridge.property(2, .{ .template = true });\n    pub const HIERARCHY_REQUEST_ERR = bridge.property(3, .{ .template = true });\n    pub const WRONG_DOCUMENT_ERR = bridge.property(4, .{ .template = true });\n    pub const INVALID_CHARACTER_ERR = bridge.property(5, .{ .template = true });\n    pub const NO_DATA_ALLOWED_ERR = bridge.property(6, .{ .template = true });\n    pub const NO_MODIFICATION_ALLOWED_ERR = bridge.property(7, .{ .template = true });\n    pub const NOT_FOUND_ERR = bridge.property(8, .{ .template = true });\n    pub const NOT_SUPPORTED_ERR = bridge.property(9, .{ .template = true });\n    pub const INUSE_ATTRIBUTE_ERR = bridge.property(10, .{ .template = true });\n    pub const INVALID_STATE_ERR = bridge.property(11, .{ .template = true });\n    pub const SYNTAX_ERR = bridge.property(12, .{ .template = true });\n    pub const INVALID_MODIFICATION_ERR = bridge.property(13, .{ .template = true });\n    pub const NAMESPACE_ERR = bridge.property(14, .{ .template = true });\n    pub const INVALID_ACCESS_ERR = bridge.property(15, .{ .template = true });\n    pub const VALIDATION_ERR = bridge.property(16, .{ .template = true });\n    pub const TYPE_MISMATCH_ERR = bridge.property(17, .{ .template = true });\n    pub const SECURITY_ERR = bridge.property(18, .{ .template = true });\n    pub const NETWORK_ERR = bridge.property(19, .{ .template = true });\n    pub const ABORT_ERR = bridge.property(20, .{ .template = true });\n    pub const URL_MISMATCH_ERR = bridge.property(21, .{ .template = true });\n    pub const QUOTA_EXCEEDED_ERR = bridge.property(22, .{ .template = true });\n    pub const TIMEOUT_ERR = bridge.property(23, .{ .template = true });\n    pub const INVALID_NODE_TYPE_ERR = bridge.property(24, .{ .template = true });\n    pub const DATA_CLONE_ERR = bridge.property(25, .{ .template = true });\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: DOMException\" {\n    try testing.htmlRunner(\"domexception.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/DOMImplementation.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Node = @import(\"Node.zig\");\nconst Document = @import(\"Document.zig\");\nconst DocumentType = @import(\"DocumentType.zig\");\n\nconst DOMImplementation = @This();\n_pad: bool = false,\n\npub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {\n    return DocumentType.init(qualified_name, public_id, system_id, page);\n}\n\npub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString, page: *Page) !*Document {\n    const document = (try page._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();\n    document._ready_state = .complete;\n    document._url = \"about:blank\";\n\n    {\n        const doctype = try page._factory.node(DocumentType{\n            ._proto = undefined,\n            ._name = \"html\",\n            ._public_id = \"\",\n            ._system_id = \"\",\n        });\n        _ = try document.asNode().appendChild(doctype.asNode(), page);\n    }\n\n    const html_node = try page.createElementNS(.html, \"html\", null);\n    _ = try document.asNode().appendChild(html_node, page);\n\n    const head_node = try page.createElementNS(.html, \"head\", null);\n    _ = try html_node.appendChild(head_node, page);\n\n    if (title) |t| {\n        const title_node = try page.createElementNS(.html, \"title\", null);\n        _ = try head_node.appendChild(title_node, page);\n        const text_node = try page.createTextNode(t.value);\n        _ = try title_node.appendChild(text_node, page);\n    }\n\n    const body_node = try page.createElementNS(.html, \"body\", null);\n    _ = try html_node.appendChild(body_node, page);\n\n    return document;\n}\n\npub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, page: *Page) !*Document {\n    // Create XML Document\n    const document = (try page._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();\n    document._url = \"about:blank\";\n\n    // Append doctype if provided\n    if (doctype) |dt| {\n        _ = try document.asNode().appendChild(dt.asNode(), page);\n    }\n\n    // Create and append root element if qualified_name provided\n    if (qualified_name) |qname| {\n        if (qname.len > 0) {\n            const namespace = if (namespace_) |ns| Node.Element.Namespace.parse(ns) else .xml;\n            const root = try page.createElementNS(namespace, qname, null);\n            _ = try document.asNode().appendChild(root, page);\n        }\n    }\n\n    return document;\n}\n\npub fn hasFeature(_: *const DOMImplementation, _: ?[]const u8, _: ?[]const u8) bool {\n    // Modern DOM spec says this should always return true\n    // This method is deprecated and kept for compatibility only\n    return true;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMImplementation);\n\n    pub const Meta = struct {\n        pub const name = \"DOMImplementation\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n        pub const enumerable = false;\n    };\n\n    pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{ .dom_exception = true });\n    pub const createDocument = bridge.function(DOMImplementation.createDocument, .{});\n    pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{});\n    pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: DOMImplementation\" {\n    try testing.htmlRunner(\"domimplementation.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/DOMNodeIterator.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst NodeFilter = @import(\"NodeFilter.zig\");\npub const FilterOpts = NodeFilter.FilterOpts;\n\nconst DOMNodeIterator = @This();\n\n_root: *Node,\n_what_to_show: u32,\n_filter: NodeFilter,\n_reference_node: *Node,\n_pointer_before_reference_node: bool,\n_active: bool = false,\n\npub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMNodeIterator {\n    const node_filter = try NodeFilter.init(filter);\n    return page._factory.create(DOMNodeIterator{\n        ._root = root,\n        ._filter = node_filter,\n        ._reference_node = root,\n        ._what_to_show = what_to_show,\n        ._pointer_before_reference_node = true,\n    });\n}\n\npub fn getRoot(self: *const DOMNodeIterator) *Node {\n    return self._root;\n}\n\npub fn getReferenceNode(self: *const DOMNodeIterator) *Node {\n    return self._reference_node;\n}\n\npub fn getPointerBeforeReferenceNode(self: *const DOMNodeIterator) bool {\n    return self._pointer_before_reference_node;\n}\n\npub fn getWhatToShow(self: *const DOMNodeIterator) u32 {\n    return self._what_to_show;\n}\n\npub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts {\n    return self._filter._original_filter;\n}\n\npub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {\n    if (self._active) {\n        return error.InvalidStateError;\n    }\n\n    self._active = true;\n    defer self._active = false;\n\n    var node = self._reference_node;\n    var before_node = self._pointer_before_reference_node;\n\n    while (true) {\n        if (before_node) {\n            before_node = false;\n            const result = try self.filterNode(node, page);\n            if (result == NodeFilter.FILTER_ACCEPT) {\n                self._reference_node = node;\n                self._pointer_before_reference_node = false;\n                return node;\n            }\n        } else {\n            // Move to next node in tree order\n            const next = self.getNextInTree(node);\n            if (next == null) {\n                return null;\n            }\n            node = next.?;\n\n            const result = try self.filterNode(node, page);\n            if (result == NodeFilter.FILTER_ACCEPT) {\n                self._reference_node = node;\n                self._pointer_before_reference_node = false;\n                return node;\n            }\n        }\n    }\n}\n\npub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node {\n    if (self._active) {\n        return error.InvalidStateError;\n    }\n\n    self._active = true;\n    defer self._active = false;\n\n    var node = self._reference_node;\n    var before_node = self._pointer_before_reference_node;\n\n    while (true) {\n        if (!before_node) {\n            const result = try self.filterNode(node, page);\n            if (result == NodeFilter.FILTER_ACCEPT) {\n                self._reference_node = node;\n                self._pointer_before_reference_node = true;\n                return node;\n            }\n            before_node = true;\n        }\n\n        // Move to previous node in tree order\n        const prev = self.getPreviousInTree(node);\n        if (prev == null) {\n            return null;\n        }\n        node = prev.?;\n        before_node = false;\n    }\n}\n\npub fn detach(_: *const DOMNodeIterator) void {\n    // no-op legacy\n}\n\nfn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 {\n    // First check whatToShow\n    if (!NodeFilter.shouldShow(node, self._what_to_show)) {\n        return NodeFilter.FILTER_SKIP;\n    }\n\n    // Then check the filter callback\n    // For NodeIterator, REJECT and SKIP are equivalent - both skip the node\n    // but continue with its descendants\n    const result = try self._filter.acceptNode(node, page.js.local.?);\n    return result;\n}\n\nfn getNextInTree(self: *const DOMNodeIterator, node: *Node) ?*Node {\n    // Depth-first traversal within the root subtree\n    if (node._children) |children| {\n        return children.first();\n    }\n\n    var current = node;\n    while (current != self._root) {\n        if (current.nextSibling()) |sibling| {\n            return sibling;\n        }\n        current = current._parent orelse return null;\n    }\n\n    return null;\n}\n\nfn getPreviousInTree(self: *const DOMNodeIterator, node: *Node) ?*Node {\n    if (node == self._root) {\n        return null;\n    }\n\n    if (node.previousSibling()) |sibling| {\n        // Go to the last descendant of the sibling\n        var last = sibling;\n        while (last.lastChild()) |child| {\n            last = child;\n        }\n        return last;\n    }\n\n    return node._parent;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMNodeIterator);\n\n    pub const Meta = struct {\n        pub const name = \"NodeIterator\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const root = bridge.accessor(DOMNodeIterator.getRoot, null, .{});\n    pub const referenceNode = bridge.accessor(DOMNodeIterator.getReferenceNode, null, .{});\n    pub const pointerBeforeReferenceNode = bridge.accessor(DOMNodeIterator.getPointerBeforeReferenceNode, null, .{});\n    pub const whatToShow = bridge.accessor(DOMNodeIterator.getWhatToShow, null, .{});\n    pub const filter = bridge.accessor(DOMNodeIterator.getFilter, null, .{});\n\n    pub const nextNode = bridge.function(DOMNodeIterator.nextNode, .{ .dom_exception = true });\n    pub const previousNode = bridge.function(DOMNodeIterator.previousNode, .{ .dom_exception = true });\n    pub const detach = bridge.function(DOMNodeIterator.detach, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/DOMParser.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Parser = @import(\"../parser/Parser.zig\");\n\nconst HTMLDocument = @import(\"HTMLDocument.zig\");\nconst XMLDocument = @import(\"XMLDocument.zig\");\nconst Document = @import(\"Document.zig\");\n\nconst DOMParser = @This();\n\n// Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n_pad: bool = false,\n\npub fn init() DOMParser {\n    return .{};\n}\n\npub fn parseFromString(\n    _: *const DOMParser,\n    html: []const u8,\n    mime_type: []const u8,\n    page: *Page,\n) !*Document {\n    const target_mime = std.meta.stringToEnum(enum {\n        @\"text/html\",\n        @\"text/xml\",\n        @\"application/xml\",\n        @\"application/xhtml+xml\",\n        @\"image/svg+xml\",\n    }, mime_type) orelse return error.NotSupported;\n\n    const arena = try page.getArena(.{ .debug = \"DOMParser.parseFromString\" });\n    defer page.releaseArena(arena);\n\n    return switch (target_mime) {\n        .@\"text/html\" => {\n            // Create a new HTMLDocument\n            const doc = try page._factory.document(HTMLDocument{\n                ._proto = undefined,\n            });\n\n            var normalized = std.mem.trim(u8, html, &std.ascii.whitespace);\n            if (normalized.len == 0) {\n                normalized = \"<html></html>\";\n            }\n\n            // Parse HTML into the document\n            var parser = Parser.init(arena, doc.asNode(), page);\n            parser.parse(normalized);\n\n            if (parser.err) |pe| {\n                return pe.err;\n            }\n\n            return doc.asDocument();\n        },\n        else => {\n            // Create a new XMLDocument.\n            const doc = try page._factory.document(XMLDocument{\n                ._proto = undefined,\n            });\n\n            // Parse XML into XMLDocument.\n            const doc_node = doc.asNode();\n            var parser = Parser.init(arena, doc_node, page);\n            parser.parseXML(html);\n\n            if (parser.err != null or doc_node.firstChild() == null) {\n                // Return a document with a <parsererror> element per spec.\n                const err_doc = try page._factory.document(XMLDocument{ ._proto = undefined });\n                var err_parser = Parser.init(arena, err_doc.asNode(), page);\n                err_parser.parseXML(\"<parsererror xmlns=\\\"http://www.mozilla.org/newlayout/xml/parsererror.xml\\\">error</parsererror>\");\n                return err_doc.asDocument();\n            }\n\n            const first_child = doc_node.firstChild().?;\n\n            // If first node is a `ProcessingInstruction`, skip it.\n            if (first_child.getNodeType() == 7) {\n                // We're sure that firstChild exist, this cannot fail.\n                _ = try doc_node.removeChild(first_child, page);\n            }\n\n            return doc.asDocument();\n        },\n    };\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMParser);\n\n    pub const Meta = struct {\n        pub const name = \"DOMParser\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const constructor = bridge.constructor(DOMParser.init, .{});\n    pub const parseFromString = bridge.function(DOMParser.parseFromString, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: DOMParser\" {\n    try testing.htmlRunner(\"domparser.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/DOMRect.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst DOMRect = @This();\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\n_x: f64,\n_y: f64,\n_width: f64,\n_height: f64,\n\npub fn init(x: f64, y: f64, width: f64, height: f64, page: *Page) !*DOMRect {\n    return page._factory.create(DOMRect{\n        ._x = x,\n        ._y = y,\n        ._width = width,\n        ._height = height,\n    });\n}\n\npub fn getX(self: *const DOMRect) f64 {\n    return self._x;\n}\n\npub fn getY(self: *const DOMRect) f64 {\n    return self._y;\n}\n\npub fn getWidth(self: *const DOMRect) f64 {\n    return self._width;\n}\n\npub fn getHeight(self: *const DOMRect) f64 {\n    return self._height;\n}\n\npub fn getTop(self: *const DOMRect) f64 {\n    return @min(self._y, self._y + self._height);\n}\n\npub fn getRight(self: *const DOMRect) f64 {\n    return @max(self._x, self._x + self._width);\n}\n\npub fn getBottom(self: *const DOMRect) f64 {\n    return @max(self._y, self._y + self._height);\n}\n\npub fn getLeft(self: *const DOMRect) f64 {\n    return @min(self._x, self._x + self._width);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMRect);\n\n    pub const Meta = struct {\n        pub const name = \"DOMRect\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(DOMRect.init, .{});\n    pub const x = bridge.accessor(DOMRect.getX, null, .{});\n    pub const y = bridge.accessor(DOMRect.getY, null, .{});\n    pub const width = bridge.accessor(DOMRect.getWidth, null, .{});\n    pub const height = bridge.accessor(DOMRect.getHeight, null, .{});\n    pub const top = bridge.accessor(DOMRect.getTop, null, .{});\n    pub const right = bridge.accessor(DOMRect.getRight, null, .{});\n    pub const bottom = bridge.accessor(DOMRect.getBottom, null, .{});\n    pub const left = bridge.accessor(DOMRect.getLeft, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/DOMTreeWalker.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst NodeFilter = @import(\"NodeFilter.zig\");\npub const FilterOpts = NodeFilter.FilterOpts;\n\nconst DOMTreeWalker = @This();\n\n_root: *Node,\n_what_to_show: u32,\n_filter: NodeFilter,\n_current: *Node,\n\npub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, page: *Page) !*DOMTreeWalker {\n    const node_filter = try NodeFilter.init(filter);\n    return page._factory.create(DOMTreeWalker{\n        ._root = root,\n        ._current = root,\n        ._filter = node_filter,\n        ._what_to_show = what_to_show,\n    });\n}\n\npub fn getRoot(self: *const DOMTreeWalker) *Node {\n    return self._root;\n}\n\npub fn getWhatToShow(self: *const DOMTreeWalker) u32 {\n    return self._what_to_show;\n}\n\npub fn getFilter(self: *const DOMTreeWalker) ?FilterOpts {\n    return self._filter._original_filter;\n}\n\npub fn getCurrentNode(self: *const DOMTreeWalker) *Node {\n    return self._current;\n}\n\npub fn setCurrentNode(self: *DOMTreeWalker, node: *Node) void {\n    self._current = node;\n}\n\n// Navigation methods\npub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self._current._parent;\n    while (node) |n| {\n        if (n == self._root._parent) {\n            return null;\n        }\n        if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {\n            self._current = n;\n            return n;\n        }\n        node = n._parent;\n    }\n    return null;\n}\n\npub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self._current.firstChild();\n\n    while (node) |n| {\n        const filter_result = try self.acceptNode(n, page);\n\n        if (filter_result == NodeFilter.FILTER_ACCEPT) {\n            self._current = n;\n            return n;\n        }\n\n        if (filter_result == NodeFilter.FILTER_SKIP) {\n            // Descend into children of this skipped node\n            if (n.firstChild()) |child| {\n                node = child;\n                continue;\n            }\n        }\n\n        // REJECT or SKIP with no children - find next sibling, walking up if necessary\n        var current_node = n;\n        while (true) {\n            if (current_node.nextSibling()) |sibling| {\n                node = sibling;\n                break;\n            }\n\n            // No sibling, go up to parent\n            const parent = current_node._parent orelse return null;\n            if (parent == self._current) {\n                // We've exhausted all children of self._current\n                return null;\n            }\n            current_node = parent;\n        }\n    }\n\n    return null;\n}\n\npub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self._current.lastChild();\n\n    while (node) |n| {\n        const filter_result = try self.acceptNode(n, page);\n\n        if (filter_result == NodeFilter.FILTER_ACCEPT) {\n            self._current = n;\n            return n;\n        }\n\n        if (filter_result == NodeFilter.FILTER_SKIP) {\n            // Descend into children of this skipped node\n            if (n.lastChild()) |child| {\n                node = child;\n                continue;\n            }\n        }\n\n        // REJECT or SKIP with no children - find previous sibling, walking up if necessary\n        var current_node = n;\n        while (true) {\n            if (current_node.previousSibling()) |sibling| {\n                node = sibling;\n                break;\n            }\n\n            // No sibling, go up to parent\n            const parent = current_node._parent orelse return null;\n            if (parent == self._current) {\n                // We've exhausted all children of self._current\n                return null;\n            }\n            current_node = parent;\n        }\n    }\n\n    return null;\n}\n\npub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self.previousSiblingOrNull(self._current);\n    while (node) |n| {\n        if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {\n            self._current = n;\n            return n;\n        }\n        node = self.previousSiblingOrNull(n);\n    }\n    return null;\n}\n\npub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self.nextSiblingOrNull(self._current);\n    while (node) |n| {\n        if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {\n            self._current = n;\n            return n;\n        }\n        node = self.nextSiblingOrNull(n);\n    }\n    return null;\n}\n\npub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self._current;\n    while (node != self._root) {\n        var sibling = self.previousSiblingOrNull(node);\n        while (sibling) |sib| {\n            node = sib;\n\n            // Check if this sibling is rejected before descending into it\n            const sib_result = try self.acceptNode(node, page);\n            if (sib_result == NodeFilter.FILTER_REJECT) {\n                // Skip this sibling and its descendants entirely\n                sibling = self.previousSiblingOrNull(node);\n                continue;\n            }\n\n            // Descend to the deepest last child, but respect FILTER_REJECT\n            while (true) {\n                var child = self.lastChildOrNull(node);\n\n                // Find the rightmost non-rejected child\n                while (child) |c| {\n                    if (!self.isInSubtree(c)) break;\n\n                    const filter_result = try self.acceptNode(c, page);\n                    if (filter_result == NodeFilter.FILTER_REJECT) {\n                        // Skip this child and try its previous sibling\n                        child = self.previousSiblingOrNull(c);\n                    } else {\n                        // ACCEPT or SKIP - use this child\n                        break;\n                    }\n                }\n\n                if (child == null) break; // No acceptable children\n\n                // Descend into this child\n                node = child.?;\n            }\n\n            if (try self.acceptNode(node, page) == NodeFilter.FILTER_ACCEPT) {\n                self._current = node;\n                return node;\n            }\n            sibling = self.previousSiblingOrNull(node);\n        }\n\n        if (node == self._root) {\n            return null;\n        }\n\n        const parent = node._parent orelse return null;\n        if (try self.acceptNode(parent, page) == NodeFilter.FILTER_ACCEPT) {\n            self._current = parent;\n            return parent;\n        }\n        node = parent;\n    }\n    return null;\n}\n\npub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {\n    var node = self._current;\n\n    while (true) {\n        // Try children first (depth-first)\n        if (node.firstChild()) |child| {\n            node = child;\n            const filter_result = try self.acceptNode(node, page);\n            if (filter_result == NodeFilter.FILTER_ACCEPT) {\n                self._current = node;\n                return node;\n            }\n            // If REJECT, skip this entire subtree; if SKIP, try children\n            if (filter_result == NodeFilter.FILTER_REJECT) {\n                // Skip this node and its children - continue with siblings\n                // Don't update node, will try siblings below\n            } else {\n                // SKIP - already moved to child, will try its children on next iteration\n                continue;\n            }\n        }\n\n        // No (more) children, try siblings\n        while (true) {\n            if (node == self._root) {\n                return null;\n            }\n\n            if (node.nextSibling()) |sibling| {\n                node = sibling;\n                const filter_result = try self.acceptNode(node, page);\n                if (filter_result == NodeFilter.FILTER_ACCEPT) {\n                    self._current = node;\n                    return node;\n                }\n                // If REJECT, skip subtree; if SKIP, try children\n                if (filter_result == NodeFilter.FILTER_REJECT) {\n                    // Continue sibling loop to get next sibling\n                    continue;\n                } else {\n                    // SKIP - try this node's children\n                    break;\n                }\n            }\n\n            // No sibling, go up to parent\n            node = node._parent orelse return null;\n        }\n    }\n}\n\n// Helper methods\nfn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 {\n    // First check whatToShow\n    if (!NodeFilter.shouldShow(node, self._what_to_show)) {\n        return NodeFilter.FILTER_SKIP;\n    }\n\n    // Then check the filter callback\n    // For TreeWalker, REJECT means reject node and its descendants\n    // SKIP means skip node but check its descendants\n    // ACCEPT means accept the node\n    return try self._filter.acceptNode(node, page.js.local.?);\n}\n\nfn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool {\n    var current = node;\n    while (current._parent) |parent| {\n        if (parent == self._root) {\n            return true;\n        }\n        current = parent;\n    }\n    return current == self._root;\n}\n\nfn firstChildOrNull(self: *const DOMTreeWalker, node: *Node) ?*Node {\n    _ = self;\n    return node.firstChild();\n}\n\nfn lastChildOrNull(self: *const DOMTreeWalker, node: *Node) ?*Node {\n    _ = self;\n    return node.lastChild();\n}\n\nfn nextSiblingOrNull(self: *const DOMTreeWalker, node: *Node) ?*Node {\n    _ = self;\n    return node.nextSibling();\n}\n\nfn previousSiblingOrNull(self: *const DOMTreeWalker, node: *Node) ?*Node {\n    _ = self;\n    return node.previousSibling();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMTreeWalker);\n\n    pub const Meta = struct {\n        pub const name = \"TreeWalker\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const root = bridge.accessor(DOMTreeWalker.getRoot, null, .{});\n    pub const whatToShow = bridge.accessor(DOMTreeWalker.getWhatToShow, null, .{});\n    pub const filter = bridge.accessor(DOMTreeWalker.getFilter, null, .{});\n    pub const currentNode = bridge.accessor(DOMTreeWalker.getCurrentNode, DOMTreeWalker.setCurrentNode, .{});\n\n    pub const parentNode = bridge.function(DOMTreeWalker.parentNode, .{});\n    pub const firstChild = bridge.function(DOMTreeWalker.firstChild, .{});\n    pub const lastChild = bridge.function(DOMTreeWalker.lastChild, .{});\n    pub const previousSibling = bridge.function(DOMTreeWalker.previousSibling, .{});\n    pub const nextSibling = bridge.function(DOMTreeWalker.nextSibling, .{});\n    pub const previousNode = bridge.function(DOMTreeWalker.previousNode, .{});\n    pub const nextNode = bridge.function(DOMTreeWalker.nextNode, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/Document.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../log.zig\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst URL = @import(\"../URL.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst Element = @import(\"Element.zig\");\nconst Location = @import(\"Location.zig\");\nconst Parser = @import(\"../parser/Parser.zig\");\nconst collections = @import(\"collections.zig\");\nconst Selector = @import(\"selector/Selector.zig\");\nconst DOMTreeWalker = @import(\"DOMTreeWalker.zig\");\nconst DOMNodeIterator = @import(\"DOMNodeIterator.zig\");\nconst DOMImplementation = @import(\"DOMImplementation.zig\");\nconst StyleSheetList = @import(\"css/StyleSheetList.zig\");\nconst FontFaceSet = @import(\"css/FontFaceSet.zig\");\nconst Selection = @import(\"Selection.zig\");\n\npub const XMLDocument = @import(\"XMLDocument.zig\");\npub const HTMLDocument = @import(\"HTMLDocument.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Document = @This();\n\n_type: Type,\n_proto: *Node,\n_page: ?*Page = null,\n_location: ?*Location = null,\n_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)\n_ready_state: ReadyState = .loading,\n_current_script: ?*Element.Html.Script = null,\n_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,\n// Track IDs that were removed from the map - they might have duplicates in the tree\n_removed_ids: std.StringHashMapUnmanaged(void) = .empty,\n_active_element: ?*Element = null,\n_style_sheets: ?*StyleSheetList = null,\n_implementation: ?*DOMImplementation = null,\n_fonts: ?*FontFaceSet = null,\n_write_insertion_point: ?*Node = null,\n_script_created_parser: ?Parser.Streaming = null,\n_adopted_style_sheets: ?js.Object.Global = null,\n_selection: Selection = .init,\n\n// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter\n// Incremented during custom element reactions when parsing. When > 0,\n// document.open/close/write/writeln must throw InvalidStateError.\n_throw_on_dynamic_markup_insertion_counter: u32 = 0,\n\n_on_selectionchange: ?js.Function.Global = null,\n\npub fn getOnSelectionChange(self: *Document) ?js.Function.Global {\n    return self._on_selectionchange;\n}\n\npub fn setOnSelectionChange(self: *Document, listener: ?js.Function) !void {\n    if (listener) |listen| {\n        self._on_selectionchange = try listen.persistWithThis(self);\n    } else {\n        self._on_selectionchange = null;\n    }\n}\n\npub const Type = union(enum) {\n    generic,\n    html: *HTMLDocument,\n    xml: *XMLDocument,\n};\n\npub fn is(self: *Document, comptime T: type) ?*T {\n    switch (self._type) {\n        .html => |html| {\n            if (T == HTMLDocument) {\n                return html;\n            }\n        },\n        .xml => |xml| {\n            if (T == XMLDocument) {\n                return xml;\n            }\n        },\n        .generic => {},\n    }\n    return null;\n}\n\npub fn as(self: *Document, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn asNode(self: *Document) *Node {\n    return self._proto;\n}\n\npub fn asEventTarget(self: *Document) *@import(\"EventTarget.zig\") {\n    return self._proto.asEventTarget();\n}\n\npub fn getURL(self: *const Document, page: *const Page) [:0]const u8 {\n    return self._url orelse page.url;\n}\n\npub fn getContentType(self: *const Document) []const u8 {\n    return switch (self._type) {\n        .html => \"text/html\",\n        .xml => \"application/xml\",\n        .generic => \"application/xml\",\n    };\n}\n\npub fn getDomain(_: *const Document, page: *const Page) []const u8 {\n    return URL.getHostname(page.url);\n}\n\nconst CreateElementOptions = struct {\n    is: ?[]const u8 = null,\n};\n\npub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {\n    try validateElementName(name);\n    const ns: Element.Namespace, const normalized_name = blk: {\n        if (self._type == .html) {\n            break :blk .{ .html, std.ascii.lowerString(&page.buf, name) };\n        }\n        // Generic and XML documents create elements with null namespace\n        break :blk .{ .null, name };\n    };\n    // HTML documents are case-insensitive - lowercase the tag name\n\n    const node = try page.createElementNS(ns, normalized_name, null);\n    const element = node.as(Element);\n\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(node, self);\n    }\n\n    const options = options_ orelse return element;\n    if (options.is) |is_value| {\n        try element.setAttribute(comptime .wrap(\"is\"), .wrap(is_value), page);\n        try Element.Html.Custom.checkAndAttachBuiltIn(element, page);\n    }\n\n    return element;\n}\n\npub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {\n    try validateElementName(name);\n    const ns = Element.Namespace.parse(namespace);\n    // Per spec, createElementNS does NOT lowercase (unlike createElement).\n    const node = try page.createElementNS(ns, name, null);\n\n    // Store original URI for unknown namespaces so lookupNamespaceURI can return it\n    if (ns == .unknown) {\n        if (namespace) |uri| {\n            const duped = try page.dupeString(uri);\n            try page._element_namespace_uris.put(page.arena, node.as(Element), duped);\n        }\n    }\n\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(node, self);\n    }\n    return node.as(Element);\n}\n\npub fn createAttribute(_: *const Document, name: String.Global, page: *Page) !?*Element.Attribute {\n    try Element.Attribute.validateAttributeName(name.str);\n    return page._factory.node(Element.Attribute{\n        ._proto = undefined,\n        ._name = name.str,\n        ._value = String.empty,\n        ._element = null,\n    });\n}\n\npub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String.Global, page: *Page) !?*Element.Attribute {\n    if (std.mem.eql(u8, namespace, \"http://www.w3.org/1999/xhtml\") == false) {\n        log.warn(.not_implemented, \"document.createAttributeNS\", .{ .namespace = namespace });\n    }\n\n    try Element.Attribute.validateAttributeName(name.str);\n    return page._factory.node(Element.Attribute{\n        ._proto = undefined,\n        ._name = name.str,\n        ._value = String.empty,\n        ._element = null,\n    });\n}\n\npub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {\n    if (id.len == 0) {\n        return null;\n    }\n\n    if (self._elements_by_id.get(id)) |element| {\n        return element;\n    }\n\n    //ID was removed but might have duplicates\n    if (self._removed_ids.remove(id)) {\n        var tw = @import(\"TreeWalker.zig\").Full.Elements.init(self.asNode(), .{});\n        while (tw.next()) |el| {\n            const element_id = el.getAttributeSafe(comptime .wrap(\"id\")) orelse continue;\n            if (std.mem.eql(u8, element_id, id)) {\n                // we ignore this error to keep getElementById easy to call\n                // if it really failed, then we're out of memory and nothing's\n                // going to work like it should anyways.\n                const owned_id = page.dupeString(id) catch return null;\n                self._elements_by_id.put(page.arena, owned_id, el) catch return null;\n                return el;\n            }\n        }\n    }\n\n    return null;\n}\n\npub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {\n    return self.asNode().getElementsByTagName(tag_name, page);\n}\n\npub fn getElementsByTagNameNS(self: *Document, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {\n    return self.asNode().getElementsByTagNameNS(namespace, local_name, page);\n}\n\npub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {\n    return self.asNode().getElementsByClassName(class_name, page);\n}\n\npub fn getElementsByName(self: *Document, name: []const u8, page: *Page) !collections.NodeLive(.name) {\n    const arena = page.arena;\n    const filter = try arena.dupe(u8, name);\n    return collections.NodeLive(.name).init(self.asNode(), filter, page);\n}\n\npub fn getChildren(self: *Document, page: *Page) !collections.NodeLive(.child_elements) {\n    return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);\n}\n\npub fn getDocumentElement(self: *Document) ?*Element {\n    var child = self.asNode().firstChild();\n    while (child) |node| {\n        if (node.is(Element)) |el| {\n            return el;\n        }\n        child = node.nextSibling();\n    }\n    return null;\n}\n\npub fn getSelection(self: *Document) *Selection {\n    return &self._selection;\n}\n\npub fn querySelector(self: *Document, input: String, page: *Page) !?*Element {\n    return Selector.querySelector(self.asNode(), input.str(), page);\n}\n\npub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector.List {\n    return Selector.querySelectorAll(self.asNode(), input.str(), page);\n}\n\npub fn getImplementation(self: *Document, page: *Page) !*DOMImplementation {\n    if (self._implementation) |impl| return impl;\n    const impl = try page._factory.create(DOMImplementation{});\n    self._implementation = impl;\n    return impl;\n}\n\npub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {\n    const frag = try Node.DocumentFragment.init(page);\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(frag.asNode(), self);\n    }\n    return frag;\n}\n\npub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {\n    const node = try page.createComment(data);\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(node, self);\n    }\n    return node;\n}\n\npub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {\n    const node = try page.createTextNode(data);\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(node, self);\n    }\n    return node;\n}\n\npub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {\n    const node = switch (self._type) {\n        .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument\n        .xml => try page.createCDATASection(data),\n        .generic => try page.createCDATASection(data),\n    };\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(node, self);\n    }\n    return node;\n}\n\npub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {\n    const node = try page.createProcessingInstruction(target, data);\n    // Track owner document if it's not the main document\n    if (self != page.document) {\n        try page.setNodeOwnerDocument(node, self);\n    }\n    return node;\n}\n\nconst Range = @import(\"Range.zig\");\npub fn createRange(_: *const Document, page: *Page) !*Range {\n    return Range.init(page);\n}\n\npub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import(\"Event.zig\") {\n    const Event = @import(\"Event.zig\");\n    if (event_type.len > 100) {\n        return error.NotSupported;\n    }\n    const normalized = std.ascii.lowerString(&page.buf, event_type);\n\n    if (std.mem.eql(u8, normalized, \"event\") or std.mem.eql(u8, normalized, \"events\") or std.mem.eql(u8, normalized, \"htmlevents\")) {\n        return Event.init(\"\", null, page);\n    }\n\n    if (std.mem.eql(u8, normalized, \"customevent\") or std.mem.eql(u8, normalized, \"customevents\")) {\n        const CustomEvent = @import(\"event/CustomEvent.zig\");\n        return (try CustomEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"keyboardevent\")) {\n        const KeyboardEvent = @import(\"event/KeyboardEvent.zig\");\n        return (try KeyboardEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"inputevent\")) {\n        const InputEvent = @import(\"event/InputEvent.zig\");\n        return (try InputEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"mouseevent\") or std.mem.eql(u8, normalized, \"mouseevents\")) {\n        const MouseEvent = @import(\"event/MouseEvent.zig\");\n        return (try MouseEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"messageevent\")) {\n        const MessageEvent = @import(\"event/MessageEvent.zig\");\n        return (try MessageEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"uievent\") or std.mem.eql(u8, normalized, \"uievents\")) {\n        const UIEvent = @import(\"event/UIEvent.zig\");\n        return (try UIEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"focusevent\") or std.mem.eql(u8, normalized, \"focusevents\")) {\n        const FocusEvent = @import(\"event/FocusEvent.zig\");\n        return (try FocusEvent.init(\"\", null, page)).asEvent();\n    }\n\n    if (std.mem.eql(u8, normalized, \"textevent\") or std.mem.eql(u8, normalized, \"textevents\")) {\n        const TextEvent = @import(\"event/TextEvent.zig\");\n        return (try TextEvent.init(\"\", null, page)).asEvent();\n    }\n\n    return error.NotSupported;\n}\n\npub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {\n    return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);\n}\n\npub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {\n    return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page);\n}\n\nfn whatToShow(value_: ?js.Value) !u32 {\n    const value = value_ orelse return 4294967295; // show all when undefined\n    if (value.isUndefined()) {\n        // undefined explicitly passed\n        return 4294967295;\n    }\n\n    if (value.isNull()) {\n        return 0;\n    }\n\n    return value.toZig(u32);\n}\n\npub fn getReadyState(self: *const Document) []const u8 {\n    return @tagName(self._ready_state);\n}\n\npub fn getActiveElement(self: *Document) ?*Element {\n    if (self._active_element) |el| {\n        return el;\n    }\n\n    // Default to body if it exists\n    if (self.is(HTMLDocument)) |html_doc| {\n        if (html_doc.getBody()) |body| {\n            return body.asElement();\n        }\n    }\n\n    // Fallback to document element\n    return self.getDocumentElement();\n}\n\npub fn getStyleSheets(self: *Document, page: *Page) !*StyleSheetList {\n    if (self._style_sheets) |sheets| {\n        return sheets;\n    }\n    const sheets = try StyleSheetList.init(page);\n    self._style_sheets = sheets;\n    return sheets;\n}\n\npub fn getFonts(self: *Document, page: *Page) !*FontFaceSet {\n    if (self._fonts) |fonts| {\n        return fonts;\n    }\n    const fonts = try FontFaceSet.init(page);\n    self._fonts = fonts;\n    return fonts;\n}\n\npub fn adoptNode(_: *const Document, node: *Node, page: *Page) !*Node {\n    if (node._type == .document) {\n        return error.NotSupported;\n    }\n\n    if (node._parent) |parent| {\n        page.removeNode(parent, node, .{ .will_be_reconnected = false });\n    }\n\n    return node;\n}\n\npub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !*Node {\n    if (node._type == .document) {\n        return error.NotSupported;\n    }\n\n    return node.cloneNode(deep_, page);\n}\n\npub fn append(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {\n    try validateDocumentNodes(self, nodes, false);\n\n    page.domChanged();\n    const parent = self.asNode();\n    const parent_is_connected = parent.isConnected();\n\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n\n        // DocumentFragments are special - append all their children\n        if (child.is(Node.DocumentFragment)) |_| {\n            try page.appendAllChildren(child, parent);\n            continue;\n        }\n\n        var child_connected = false;\n        if (child._parent) |previous_parent| {\n            child_connected = child.isConnected();\n            page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });\n        }\n        try page.appendNode(parent, child, .{ .child_already_connected = child_connected });\n    }\n}\n\npub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {\n    try validateDocumentNodes(self, nodes, false);\n\n    page.domChanged();\n    const parent = self.asNode();\n    const parent_is_connected = parent.isConnected();\n\n    var i = nodes.len;\n    while (i > 0) {\n        i -= 1;\n        const child = try nodes[i].toNode(page);\n\n        // DocumentFragments are special - need to insert all their children\n        if (child.is(Node.DocumentFragment)) |frag| {\n            const first_child = parent.firstChild();\n            var frag_child = frag.asNode().lastChild();\n            while (frag_child) |fc| {\n                const prev = fc.previousSibling();\n                page.removeNode(frag.asNode(), fc, .{ .will_be_reconnected = parent_is_connected });\n                if (first_child) |before| {\n                    try page.insertNodeRelative(parent, fc, .{ .before = before }, .{});\n                } else {\n                    try page.appendNode(parent, fc, .{});\n                }\n                frag_child = prev;\n            }\n            continue;\n        }\n\n        var child_connected = false;\n        if (child._parent) |previous_parent| {\n            child_connected = child.isConnected();\n            page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });\n        }\n\n        const first_child = parent.firstChild();\n        if (first_child) |before| {\n            try page.insertNodeRelative(parent, child, .{ .before = before }, .{ .child_already_connected = child_connected });\n        } else {\n            try page.appendNode(parent, child, .{ .child_already_connected = child_connected });\n        }\n    }\n}\n\npub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {\n    try validateDocumentNodes(self, nodes, true);\n\n    page.domChanged();\n    const parent = self.asNode();\n\n    // Remove all existing children\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        page.removeNode(parent, child, .{ .will_be_reconnected = false });\n    }\n\n    // Append new children\n    const parent_is_connected = parent.isConnected();\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n\n        // DocumentFragments are special - append all their children\n        if (child.is(Node.DocumentFragment)) |_| {\n            try page.appendAllChildren(child, parent);\n            continue;\n        }\n\n        var child_connected = false;\n        if (child._parent) |previous_parent| {\n            child_connected = child.isConnected();\n            page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });\n        }\n        try page.appendNode(parent, child, .{ .child_already_connected = child_connected });\n    }\n}\n\npub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element {\n    // Traverse document in depth-first order to find the topmost (last in document order)\n    // element that contains the point (x, y)\n    var topmost: ?*Element = null;\n\n    const root = self.asNode();\n    var stack: std.ArrayList(*Node) = .empty;\n    try stack.append(page.call_arena, root);\n\n    while (stack.items.len > 0) {\n        const node = stack.pop() orelse break;\n        if (node.is(Element)) |element| {\n            if (element.checkVisibility(page)) {\n                const rect = element.getBoundingClientRectForVisible(page);\n                if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {\n                    topmost = element;\n                }\n            }\n        }\n\n        // Add children to stack in reverse order so we process them in document order\n        var child = node.lastChild();\n        while (child) |c| {\n            try stack.append(page.call_arena, c);\n            child = c.previousSibling();\n        }\n    }\n\n    return topmost;\n}\n\npub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const *Element {\n    // Get topmost element\n    var current: ?*Element = (try self.elementFromPoint(x, y, page)) orelse return &.{};\n    var result: std.ArrayList(*Element) = .empty;\n    while (current) |el| {\n        try result.append(page.call_arena, el);\n        current = el.parentElement();\n    }\n    return result.items;\n}\n\npub fn getDocType(self: *Document) ?*Node {\n    var tw = @import(\"TreeWalker.zig\").Full.init(self.asNode(), .{});\n    while (tw.next()) |node| {\n        if (node._type == .document_type) {\n            return node;\n        }\n    }\n    return null;\n}\n\n// document.write is complicated and works differently based on the state of\n// parsing. But, generally, it's supposed to be additive/streaming. Multiple\n// document.writes are parsed a single unit. Well, that causes issues with\n// html5ever if we're trying to parse 1 document which is really many. So we\n// try to detect \"new\" documents. (This is particularly problematic because we\n// don't have proper frame support, so document.write into a frame can get\n// sent to the main document (instead of the frame document)...and it's completely\n// reasonable for 2 frames to document.write(\"<html>...</html>\") into their own\n// frame.\nfn looksLikeNewDocument(html: []const u8) bool {\n    const trimmed = std.mem.trimLeft(u8, html, &std.ascii.whitespace);\n    return std.ascii.startsWithIgnoreCase(trimmed, \"<!DOCTYPE\") or\n        std.ascii.startsWithIgnoreCase(trimmed, \"<html\");\n}\n\npub fn write(self: *Document, text: []const []const u8, page: *Page) !void {\n    if (self._type == .xml) {\n        return error.InvalidStateError;\n    }\n\n    if (self._throw_on_dynamic_markup_insertion_counter > 0) {\n        return error.InvalidStateError;\n    }\n\n    const html = blk: {\n        var joined: std.ArrayList(u8) = .empty;\n        for (text) |str| {\n            try joined.appendSlice(page.call_arena, str);\n        }\n        break :blk joined.items;\n    };\n\n    if (self._current_script == null or page._load_state != .parsing) {\n        if (self._script_created_parser == null or looksLikeNewDocument(html)) {\n            _ = try self.open(page);\n        }\n\n        if (html.len > 0) {\n            if (self._script_created_parser) |*parser| {\n                parser.read(html) catch |err| {\n                    log.warn(.dom, \"document.write parser error\", .{ .err = err });\n                    // was alrady closed\n                    self._script_created_parser = null;\n                };\n            }\n        }\n        return;\n    }\n\n    // Inline script during parsing\n    const script = self._current_script.?;\n    const parent = script.asNode().parentNode() orelse return;\n\n    // Our implemnetation is hacky. We'll write to a DocumentFragment, then\n    // append its children.\n    const fragment = try Node.DocumentFragment.init(page);\n    const fragment_node = fragment.asNode();\n\n    const previous_parse_mode = page._parse_mode;\n    page._parse_mode = .document_write;\n    defer page._parse_mode = previous_parse_mode;\n\n    const arena = try page.getArena(.{ .debug = \"Document.write\" });\n    defer page.releaseArena(arena);\n\n    var parser = Parser.init(arena, fragment_node, page);\n    parser.parseFragment(html);\n\n    // Extract children from wrapper HTML element (html5ever wraps fragments)\n    // https://github.com/servo/html5ever/issues/583\n    const children = fragment_node._children orelse return;\n    const first = children.first();\n\n    // Collect all children to insert (to avoid iterator invalidation)\n    var children_to_insert: std.ArrayList(*Node) = .empty;\n\n    var it = if (first.is(Element.Html.Html) == null) fragment_node.childrenIterator() else first.childrenIterator();\n    while (it.next()) |child| {\n        try children_to_insert.append(arena, child);\n    }\n\n    if (children_to_insert.items.len == 0) {\n        return;\n    }\n\n    // Determine insertion point:\n    // - If _write_insertion_point is set, continue from there (subsequent write)\n    // - Otherwise, start after the script (first write)\n    var insert_after: ?*Node = self._write_insertion_point orelse script.asNode();\n\n    for (children_to_insert.items) |child| {\n        // Clear parent pointer (child is currently parented to fragment/HTML wrapper)\n        child._parent = null;\n        try page.insertNodeRelative(parent, child, .{ .after = insert_after.? }, .{});\n        insert_after = child;\n    }\n\n    page.domChanged();\n    self._write_insertion_point = children_to_insert.getLast();\n}\n\npub fn open(self: *Document, page: *Page) !*Document {\n    if (self._type == .xml) {\n        return error.InvalidStateError;\n    }\n\n    if (self._throw_on_dynamic_markup_insertion_counter > 0) {\n        return error.InvalidStateError;\n    }\n\n    if (page._load_state == .parsing) {\n        return self;\n    }\n\n    if (self._script_created_parser != null) {\n        return self;\n    }\n\n    // If we aren't parsing, then open clears the document.\n    const doc_node = self.asNode();\n\n    {\n        // Remove all children from document\n        var it = doc_node.childrenIterator();\n        while (it.next()) |child| {\n            page.removeNode(doc_node, child, .{ .will_be_reconnected = false });\n        }\n    }\n\n    // reset the document\n    self._elements_by_id.clearAndFree(page.arena);\n    self._active_element = null;\n    self._style_sheets = null;\n    self._implementation = null;\n    self._ready_state = .loading;\n\n    self._script_created_parser = Parser.Streaming.init(page.arena, doc_node, page);\n    try self._script_created_parser.?.start();\n    page._parse_mode = .document;\n\n    return self;\n}\n\npub fn close(self: *Document, page: *Page) !void {\n    if (self._type == .xml) {\n        return error.InvalidStateError;\n    }\n\n    if (self._throw_on_dynamic_markup_insertion_counter > 0) {\n        return error.InvalidStateError;\n    }\n\n    if (self._script_created_parser == null) {\n        return;\n    }\n\n    // done() calls html5ever_streaming_parser_finish which frees the parser\n    // We must NOT call deinit() after done() as that would be a double-free\n    self._script_created_parser.?.done();\n    // Just null out the handle since done() already freed it\n    self._script_created_parser.?.handle = null;\n    self._script_created_parser = null;\n\n    page.documentIsComplete();\n}\n\npub fn getFirstElementChild(self: *Document) ?*Element {\n    var it = self.asNode().childrenIterator();\n    while (it.next()) |child| {\n        if (child.is(Element)) |el| {\n            return el;\n        }\n    }\n    return null;\n}\n\npub fn getLastElementChild(self: *Document) ?*Element {\n    var maybe_child = self.asNode().lastChild();\n    while (maybe_child) |child| {\n        if (child.is(Element)) |el| {\n            return el;\n        }\n        maybe_child = child.previousSibling();\n    }\n    return null;\n}\n\npub fn getChildElementCount(self: *Document) u32 {\n    var i: u32 = 0;\n    var it = self.asNode().childrenIterator();\n    while (it.next()) |child| {\n        if (child.is(Element) != null) {\n            i += 1;\n        }\n    }\n    return i;\n}\n\npub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {\n    if (self._adopted_style_sheets) |ass| {\n        return ass;\n    }\n    const js_arr = page.js.local.?.newArray(0);\n    const js_obj = js_arr.toObject();\n    self._adopted_style_sheets = try js_obj.persist();\n    return self._adopted_style_sheets.?;\n}\n\npub fn hasFocus(_: *Document) bool {\n    log.debug(.not_implemented, \"Document.hasFocus\", .{});\n    return true;\n}\n\npub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {\n    self._adopted_style_sheets = try sheets.persist();\n}\n\n// Validates that nodes can be inserted into a Document, respecting Document constraints:\n// - At most one Element child\n// - At most one DocumentType child\n// - No Document, Attribute, or Text nodes\n// - Only Element, DocumentType, Comment, and ProcessingInstruction are allowed\n// When replacing=true, existing children are not counted (for replaceChildren)\nfn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, comptime replacing: bool) !void {\n    const parent = self.asNode();\n\n    // Check existing elements and doctypes (unless we're replacing all children)\n    var has_element = false;\n    var has_doctype = false;\n\n    if (!replacing) {\n        var it = parent.childrenIterator();\n        while (it.next()) |child| {\n            if (child._type == .element) {\n                has_element = true;\n            } else if (child._type == .document_type) {\n                has_doctype = true;\n            }\n        }\n    }\n\n    // Validate new nodes\n    for (nodes) |node_or_text| {\n        switch (node_or_text) {\n            .text => {\n                // Text nodes are not allowed as direct children of Document\n                return error.HierarchyError;\n            },\n            .node => |child| {\n                // Check if it's a DocumentFragment - need to validate its children\n                if (child.is(Node.DocumentFragment)) |frag| {\n                    var frag_it = frag.asNode().childrenIterator();\n                    while (frag_it.next()) |frag_child| {\n                        // Document can only contain: Element, DocumentType, Comment, ProcessingInstruction\n                        switch (frag_child._type) {\n                            .element => {\n                                if (has_element) {\n                                    return error.HierarchyError;\n                                }\n                                has_element = true;\n                            },\n                            .document_type => {\n                                if (has_doctype) {\n                                    return error.HierarchyError;\n                                }\n                                has_doctype = true;\n                            },\n                            .cdata => |cd| switch (cd._type) {\n                                .comment, .processing_instruction => {}, // Allowed\n                                .text, .cdata_section => return error.HierarchyError, // Not allowed in Document\n                            },\n                            .document, .attribute, .document_fragment => return error.HierarchyError,\n                        }\n                    }\n                } else {\n                    // Validate node type for direct insertion\n                    switch (child._type) {\n                        .element => {\n                            if (has_element) {\n                                return error.HierarchyError;\n                            }\n                            has_element = true;\n                        },\n                        .document_type => {\n                            if (has_doctype) {\n                                return error.HierarchyError;\n                            }\n                            has_doctype = true;\n                        },\n                        .cdata => |cd| switch (cd._type) {\n                            .comment, .processing_instruction => {}, // Allowed\n                            .text, .cdata_section => return error.HierarchyError, // Not allowed in Document\n                        },\n                        .document, .attribute, .document_fragment => return error.HierarchyError,\n                    }\n                }\n\n                // Check for cycles\n                if (child.contains(parent)) {\n                    return error.HierarchyError;\n                }\n            },\n        }\n    }\n}\n\nfn validateElementName(name: []const u8) !void {\n    if (name.len == 0) {\n        return error.InvalidCharacterError;\n    }\n\n    const first = name[0];\n    // Element names cannot start with: digits, period, hyphen\n    if ((first >= '0' and first <= '9') or first == '.' or first == '-') {\n        return error.InvalidCharacterError;\n    }\n\n    for (name[1..]) |c| {\n        const is_valid = (c >= 'a' and c <= 'z') or\n            (c >= 'A' and c <= 'Z') or\n            (c >= '0' and c <= '9') or\n            c == '_' or c == '-' or c == '.' or c == ':' or\n            c >= 128; // Allow non-ASCII UTF-8\n\n        if (!is_valid) {\n            return error.InvalidCharacterError;\n        }\n    }\n}\n\n// When a page or frame's URL is about:blank, or as soon as a frame is\n// programmatically created, it has this default \"blank\" content\npub fn injectBlank(self: *Document, page: *Page) error{InjectBlankError}!void {\n    self._injectBlank(page) catch |err| {\n        // we wrap _injectBlank like this so that injectBlank can only return an\n        // InjectBlankError. injectBlank is used in when nodes are inserted\n        // as since it inserts node itself, Zig can't infer the error set.\n        log.err(.browser, \"inject blank\", .{ .err = err });\n        return error.InjectBlankError;\n    };\n}\n\nfn _injectBlank(self: *Document, page: *Page) !void {\n    if (comptime IS_DEBUG) {\n        // should only be called on an empty document\n        std.debug.assert(self.asNode()._children == null);\n    }\n\n    const html = try page.createElementNS(.html, \"html\", null);\n    const head = try page.createElementNS(.html, \"head\", null);\n    const body = try page.createElementNS(.html, \"body\", null);\n    try page.appendNode(html, head, .{});\n    try page.appendNode(html, body, .{});\n    try page.appendNode(self.asNode(), html, .{});\n}\n\nconst ReadyState = enum {\n    loading,\n    interactive,\n    complete,\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Document);\n\n    pub const Meta = struct {\n        pub const name = \"Document\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(_constructor, .{});\n    fn _constructor(page: *Page) !*Document {\n        return page._factory.node(Document{\n            ._proto = undefined,\n            ._type = .generic,\n        });\n    }\n\n    pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{});\n    pub const URL = bridge.accessor(Document.getURL, null, .{});\n    pub const documentURI = bridge.accessor(Document.getURL, null, .{});\n    pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});\n    pub const scrollingElement = bridge.accessor(Document.getDocumentElement, null, .{});\n    pub const children = bridge.accessor(Document.getChildren, null, .{});\n    pub const readyState = bridge.accessor(Document.getReadyState, null, .{});\n    pub const implementation = bridge.accessor(Document.getImplementation, null, .{});\n    pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});\n    pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});\n    pub const fonts = bridge.accessor(Document.getFonts, null, .{});\n    pub const contentType = bridge.accessor(Document.getContentType, null, .{});\n    pub const domain = bridge.accessor(Document.getDomain, null, .{});\n    pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });\n    pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });\n    pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});\n    pub const createComment = bridge.function(Document.createComment, .{});\n    pub const createTextNode = bridge.function(Document.createTextNode, .{});\n    pub const createAttribute = bridge.function(Document.createAttribute, .{ .dom_exception = true });\n    pub const createAttributeNS = bridge.function(Document.createAttributeNS, .{ .dom_exception = true });\n    pub const createCDATASection = bridge.function(Document.createCDATASection, .{ .dom_exception = true });\n    pub const createProcessingInstruction = bridge.function(Document.createProcessingInstruction, .{ .dom_exception = true });\n    pub const createRange = bridge.function(Document.createRange, .{});\n    pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });\n    pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});\n    pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});\n    pub const getElementById = bridge.function(_getElementById, .{});\n    fn _getElementById(self: *Document, value_: ?js.Value, page: *Page) !?*Element {\n        const value = value_ orelse return null;\n        if (value.isNull()) {\n            return self.getElementById(\"null\", page);\n        }\n        if (value.isUndefined()) {\n            return self.getElementById(\"undefined\", page);\n        }\n        return self.getElementById(try value.toZig([]const u8), page);\n    }\n    pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });\n    pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });\n    pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});\n    pub const getElementsByTagNameNS = bridge.function(Document.getElementsByTagNameNS, .{});\n    pub const getSelection = bridge.function(Document.getSelection, .{});\n    pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});\n    pub const getElementsByName = bridge.function(Document.getElementsByName, .{});\n    pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });\n    pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });\n    pub const append = bridge.function(Document.append, .{ .dom_exception = true });\n    pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true });\n    pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true });\n    pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});\n    pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});\n    pub const write = bridge.function(Document.write, .{ .dom_exception = true });\n    pub const open = bridge.function(Document.open, .{ .dom_exception = true });\n    pub const close = bridge.function(Document.close, .{ .dom_exception = true });\n    pub const doctype = bridge.accessor(Document.getDocType, null, .{});\n    pub const firstElementChild = bridge.accessor(Document.getFirstElementChild, null, .{});\n    pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});\n    pub const childElementCount = bridge.accessor(Document.getChildElementCount, null, .{});\n    pub const adoptedStyleSheets = bridge.accessor(Document.getAdoptedStyleSheets, Document.setAdoptedStyleSheets, .{});\n    pub const hidden = bridge.property(false, .{ .template = false, .readonly = true });\n    pub const visibilityState = bridge.property(\"visible\", .{ .template = false, .readonly = true });\n    pub const defaultView = bridge.accessor(struct {\n        fn defaultView(_: *const Document, page: *Page) *@import(\"Window.zig\") {\n            return page.window;\n        }\n    }.defaultView, null, .{});\n    pub const hasFocus = bridge.function(Document.hasFocus, .{});\n\n    pub const prerendering = bridge.property(false, .{ .template = false });\n    pub const characterSet = bridge.property(\"UTF-8\", .{ .template = false });\n    pub const charset = bridge.property(\"UTF-8\", .{ .template = false });\n    pub const inputEncoding = bridge.property(\"UTF-8\", .{ .template = false });\n    pub const compatMode = bridge.property(\"CSS1Compat\", .{ .template = false });\n    pub const referrer = bridge.property(\"\", .{ .template = false });\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Document\" {\n    try testing.htmlRunner(\"document\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/DocumentFragment.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Node = @import(\"Node.zig\");\nconst Element = @import(\"Element.zig\");\nconst ShadowRoot = @import(\"ShadowRoot.zig\");\nconst collections = @import(\"collections.zig\");\nconst Selector = @import(\"selector/Selector.zig\");\n\nconst DocumentFragment = @This();\n\n_type: Type,\n_proto: *Node,\n\npub const Type = union(enum) {\n    generic,\n    shadow_root: *ShadowRoot,\n};\n\npub fn is(self: *DocumentFragment, comptime T: type) ?*T {\n    switch (self._type) {\n        .shadow_root => |shadow_root| {\n            if (T == ShadowRoot) {\n                return shadow_root;\n            }\n        },\n        .generic => {},\n    }\n    return null;\n}\n\npub fn as(self: *DocumentFragment, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn init(page: *Page) !*DocumentFragment {\n    return page._factory.node(DocumentFragment{\n        ._type = .generic,\n        ._proto = undefined,\n    });\n}\n\npub fn asNode(self: *DocumentFragment) *Node {\n    return self._proto;\n}\n\npub fn asEventTarget(self: *DocumentFragment) *@import(\"EventTarget.zig\") {\n    return self._proto.asEventTarget();\n}\n\npub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {\n    if (id.len == 0) {\n        return null;\n    }\n\n    var tw = @import(\"TreeWalker.zig\").Full.Elements.init(self.asNode(), .{});\n    while (tw.next()) |el| {\n        if (el.getAttributeSafe(comptime .wrap(\"id\"))) |element_id| {\n            if (std.mem.eql(u8, element_id, id)) {\n                return el;\n            }\n        }\n    }\n    return null;\n}\n\npub fn querySelector(self: *DocumentFragment, selector: []const u8, page: *Page) !?*Element {\n    return Selector.querySelector(self.asNode(), selector, page);\n}\n\npub fn querySelectorAll(self: *DocumentFragment, input: []const u8, page: *Page) !*Selector.List {\n    return Selector.querySelectorAll(self.asNode(), input, page);\n}\n\npub fn getChildren(self: *DocumentFragment, page: *Page) !collections.NodeLive(.child_elements) {\n    return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);\n}\n\npub fn firstElementChild(self: *DocumentFragment) ?*Element {\n    var maybe_child = self.asNode().firstChild();\n    while (maybe_child) |child| {\n        if (child.is(Element)) |el| return el;\n        maybe_child = child.nextSibling();\n    }\n    return null;\n}\n\npub fn lastElementChild(self: *DocumentFragment) ?*Element {\n    var maybe_child = self.asNode().lastChild();\n    while (maybe_child) |child| {\n        if (child.is(Element)) |el| return el;\n        maybe_child = child.previousSibling();\n    }\n    return null;\n}\n\npub fn getChildElementCount(self: *DocumentFragment) usize {\n    var count: usize = 0;\n    var it = self.asNode().childrenIterator();\n    while (it.next()) |node| {\n        if (node.is(Element) != null) {\n            count += 1;\n        }\n    }\n    return count;\n}\n\npub fn append(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const parent = self.asNode();\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        _ = try parent.appendChild(child, page);\n    }\n}\n\npub fn prepend(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const parent = self.asNode();\n    var i = nodes.len;\n    while (i > 0) {\n        i -= 1;\n        const child = try nodes[i].toNode(page);\n        _ = try parent.insertBefore(child, parent.firstChild(), page);\n    }\n}\n\npub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText, page: *Page) !void {\n    page.domChanged();\n    var parent = self.asNode();\n\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        page.removeNode(parent, child, .{ .will_be_reconnected = false });\n    }\n\n    const parent_is_connected = parent.isConnected();\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n\n        // If the new children has already a parent, remove from it.\n        if (child._parent) |p| {\n            page.removeNode(p, child, .{ .will_be_reconnected = true });\n        }\n\n        try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected });\n    }\n}\n\npub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, page: *Page) !void {\n    const dump = @import(\"../dump.zig\");\n    return dump.children(self.asNode(), .{ .shadow = .complete }, writer, page);\n}\n\npub fn setInnerHTML(self: *DocumentFragment, html: []const u8, page: *Page) !void {\n    const parent = self.asNode();\n\n    page.domChanged();\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        page.removeNode(parent, child, .{ .will_be_reconnected = false });\n    }\n\n    if (html.len == 0) {\n        return;\n    }\n\n    try page.parseHtmlAsChildren(parent, html);\n}\n\npub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {\n    const fragment = try DocumentFragment.init(page);\n    const fragment_node = fragment.asNode();\n\n    if (deep) {\n        const node = self.asNode();\n        const self_is_connected = node.isConnected();\n\n        var child_it = node.childrenIterator();\n        while (child_it.next()) |child| {\n            if (try child.cloneNodeForAppending(true, page)) |cloned_child| {\n                try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected });\n            }\n        }\n    }\n\n    return fragment_node;\n}\n\npub fn isEqualNode(self: *DocumentFragment, other: *DocumentFragment) bool {\n    var self_iter = self.asNode().childrenIterator();\n    var other_iter = other.asNode().childrenIterator();\n\n    while (true) {\n        const self_child = self_iter.next();\n        const other_child = other_iter.next();\n\n        if ((self_child == null) != (other_child == null)) {\n            return false;\n        }\n\n        if (self_child == null) {\n            // We've reached the end\n            return true;\n        }\n\n        if (!self_child.?.isEqualNode(other_child.?)) {\n            return false;\n        }\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DocumentFragment);\n\n    pub const Meta = struct {\n        pub const name = \"DocumentFragment\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(DocumentFragment.init, .{});\n\n    pub const getElementById = bridge.function(_getElementById, .{});\n    fn _getElementById(self: *DocumentFragment, value_: ?js.Value) !?*Element {\n        const value = value_ orelse return null;\n        if (value.isNull()) {\n            return self.getElementById(\"null\");\n        }\n        if (value.isUndefined()) {\n            return self.getElementById(\"undefined\");\n        }\n        return self.getElementById(try value.toZig([]const u8));\n    }\n\n    pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true });\n    pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true });\n    pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{});\n    pub const childElementCount = bridge.accessor(DocumentFragment.getChildElementCount, null, .{});\n    pub const firstElementChild = bridge.accessor(DocumentFragment.firstElementChild, null, .{});\n    pub const lastElementChild = bridge.accessor(DocumentFragment.lastElementChild, null, .{});\n    pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true });\n    pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true });\n    pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true });\n    pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{});\n\n    fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.getInnerHTML(&buf.writer, page);\n        return buf.written();\n    }\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: DocumentFragment\" {\n    try testing.htmlRunner(\"document_fragment\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/DocumentType.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Node = @import(\"Node.zig\");\n\nconst DocumentType = @This();\n\n_proto: *Node,\n_name: []const u8,\n_public_id: []const u8,\n_system_id: []const u8,\n\npub fn init(qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {\n    const name = try page.dupeString(qualified_name);\n    // Firefox converts null to the string \"null\", not empty string\n    const pub_id = if (public_id) |p| try page.dupeString(p) else \"null\";\n    const sys_id = if (system_id) |s| try page.dupeString(s) else \"null\";\n\n    return page._factory.node(DocumentType{\n        ._proto = undefined,\n        ._name = name,\n        ._public_id = pub_id,\n        ._system_id = sys_id,\n    });\n}\n\npub fn asNode(self: *DocumentType) *Node {\n    return self._proto;\n}\n\npub fn asEventTarget(self: *DocumentType) *@import(\"EventTarget.zig\") {\n    return self._proto.asEventTarget();\n}\n\npub fn getName(self: *const DocumentType) []const u8 {\n    return self._name;\n}\n\npub fn getPublicId(self: *const DocumentType) []const u8 {\n    return self._public_id;\n}\n\npub fn getSystemId(self: *const DocumentType) []const u8 {\n    return self._system_id;\n}\n\npub fn isEqualNode(self: *const DocumentType, other: *const DocumentType) bool {\n    return std.mem.eql(u8, self._name, other._name) and\n        std.mem.eql(u8, self._public_id, other._public_id) and\n        std.mem.eql(u8, self._system_id, other._system_id);\n}\n\npub fn clone(self: *const DocumentType, page: *Page) !*DocumentType {\n    return .init(self._name, self._public_id, self._system_id, page);\n}\n\npub fn remove(self: *DocumentType, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node.parentNode() orelse return;\n    _ = try parent.removeChild(node, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DocumentType);\n\n    pub const Meta = struct {\n        pub const name = \"DocumentType\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const name = bridge.accessor(DocumentType.getName, null, .{});\n    pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{});\n    pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{});\n    pub const remove = bridge.function(DocumentType.remove, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/Element.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst log = @import(\"../../log.zig\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst reflect = @import(\"../reflect.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst CSS = @import(\"CSS.zig\");\nconst ShadowRoot = @import(\"ShadowRoot.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\nconst collections = @import(\"collections.zig\");\nconst Selector = @import(\"selector/Selector.zig\");\nconst Animation = @import(\"animation/Animation.zig\");\nconst DOMStringMap = @import(\"element/DOMStringMap.zig\");\nconst CSSStyleProperties = @import(\"css/CSSStyleProperties.zig\");\n\npub const DOMRect = @import(\"DOMRect.zig\");\npub const Svg = @import(\"element/Svg.zig\");\npub const Html = @import(\"element/Html.zig\");\npub const Attribute = @import(\"element/Attribute.zig\");\n\nconst Element = @This();\n\npub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);\npub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);\npub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);\npub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);\npub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);\npub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);\npub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);\n\npub const ScrollPosition = struct {\n    x: u32 = 0,\n    y: u32 = 0,\n};\npub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);\n\npub const Namespace = enum(u8) {\n    html,\n    svg,\n    mathml,\n    xml,\n    // We should keep the original value, but don't.  If this becomes important\n    // consider storing it in a page lookup, like `_element_class_lists`, rather\n    // that adding a slice directly here (directly in every element).\n    unknown,\n    null,\n\n    pub fn toUri(self: Namespace) ?[]const u8 {\n        return switch (self) {\n            .html => \"http://www.w3.org/1999/xhtml\",\n            .svg => \"http://www.w3.org/2000/svg\",\n            .mathml => \"http://www.w3.org/1998/Math/MathML\",\n            .xml => \"http://www.w3.org/XML/1998/namespace\",\n            .unknown => \"http://lightpanda.io/unsupported/namespace\",\n            .null => null,\n        };\n    }\n\n    pub fn parse(namespace_: ?[]const u8) Namespace {\n        const namespace = namespace_ orelse return .null;\n        if (namespace.len == \"http://www.w3.org/1999/xhtml\".len) {\n            // Common case, avoid the string comparion. Recklessly\n            @branchHint(.likely);\n            return .html;\n        }\n        if (std.mem.eql(u8, namespace, \"http://www.w3.org/XML/1998/namespace\")) {\n            return .xml;\n        }\n        if (std.mem.eql(u8, namespace, \"http://www.w3.org/2000/svg\")) {\n            return .svg;\n        }\n        if (std.mem.eql(u8, namespace, \"http://www.w3.org/1998/Math/MathML\")) {\n            return .mathml;\n        }\n        return .unknown;\n    }\n};\n\n_type: Type,\n_proto: *Node,\n_namespace: Namespace = .html,\n_attributes: ?*Attribute.List = null,\n\npub const Type = union(enum) {\n    html: *Html,\n    svg: *Svg,\n};\n\npub fn is(self: *Element, comptime T: type) ?*T {\n    const type_name = @typeName(T);\n    switch (self._type) {\n        .html => |el| {\n            if (T == Html) {\n                return el;\n            }\n            if (comptime std.mem.startsWith(u8, type_name, \"browser.webapi.element.html.\")) {\n                return el.is(T);\n            }\n        },\n        .svg => |svg| {\n            if (T == Svg) {\n                return svg;\n            }\n            if (comptime std.mem.startsWith(u8, type_name, \"webapi.element.svg.\")) {\n                return svg.is(T);\n            }\n        },\n    }\n    return null;\n}\n\npub fn as(self: *Element, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn asNode(self: *Element) *Node {\n    return self._proto;\n}\n\npub fn asEventTarget(self: *Element) *EventTarget {\n    return self._proto.asEventTarget();\n}\n\npub fn asConstNode(self: *const Element) *const Node {\n    return self._proto;\n}\n\npub fn attributesEql(self: *const Element, other: *Element) bool {\n    if (self._attributes) |attr_list| {\n        const other_list = other._attributes orelse return false;\n        return attr_list.eql(other_list);\n    }\n    // Make sure no attrs in both sides.\n    return other._attributes == null;\n}\n\n/// TODO: localName and prefix comparison.\npub fn isEqualNode(self: *Element, other: *Element) bool {\n    const self_tag = self.getTagNameDump();\n    const other_tag = other.getTagNameDump();\n    // Compare namespaces and tags.\n    const dirty = self._namespace != other._namespace or !std.mem.eql(u8, self_tag, other_tag);\n    if (dirty) {\n        return false;\n    }\n\n    // Compare attributes.\n    if (!self.attributesEql(other)) {\n        return false;\n    }\n\n    // Compare children.\n    var self_iter = self.asNode().childrenIterator();\n    var other_iter = other.asNode().childrenIterator();\n    var self_count: usize = 0;\n    var other_count: usize = 0;\n    while (self_iter.next()) |self_node| : (self_count += 1) {\n        const other_node = other_iter.next() orelse return false;\n        other_count += 1;\n        if (self_node.isEqualNode(other_node)) {\n            continue;\n        }\n\n        return false;\n    }\n\n    // Make sure both have equal number of children.\n    return self_count == other_count;\n}\n\npub fn getTagNameLower(self: *const Element) []const u8 {\n    switch (self._type) {\n        .html => |he| switch (he._type) {\n            .custom => |ce| {\n                @branchHint(.unlikely);\n                return ce._tag_name.str();\n            },\n            else => return switch (he._type) {\n                .anchor => \"a\",\n                .area => \"area\",\n                .base => \"base\",\n                .body => \"body\",\n                .br => \"br\",\n                .button => \"button\",\n                .canvas => \"canvas\",\n                .custom => |e| e._tag_name.str(),\n                .data => \"data\",\n                .datalist => \"datalist\",\n                .details => \"details\",\n                .dialog => \"dialog\",\n                .directory => \"dir\",\n                .div => \"div\",\n                .dl => \"dl\",\n                .embed => \"embed\",\n                .fieldset => \"fieldset\",\n                .font => \"font\",\n                .form => \"form\",\n                .generic => |e| e._tag_name.str(),\n                .heading => |e| e._tag_name.str(),\n                .head => \"head\",\n                .html => \"html\",\n                .hr => \"hr\",\n                .iframe => \"iframe\",\n                .img => \"img\",\n                .input => \"input\",\n                .label => \"label\",\n                .legend => \"legend\",\n                .li => \"li\",\n                .link => \"link\",\n                .map => \"map\",\n                .media => |m| switch (m._type) {\n                    .audio => \"audio\",\n                    .video => \"video\",\n                    .generic => \"media\",\n                },\n                .meta => \"meta\",\n                .meter => \"meter\",\n                .mod => |e| e._tag_name.str(),\n                .object => \"object\",\n                .ol => \"ol\",\n                .optgroup => \"optgroup\",\n                .option => \"option\",\n                .output => \"output\",\n                .p => \"p\",\n                .picture => \"picture\",\n                .param => \"param\",\n                .pre => \"pre\",\n                .progress => \"progress\",\n                .quote => |e| e._tag_name.str(),\n                .script => \"script\",\n                .select => \"select\",\n                .slot => \"slot\",\n                .source => \"source\",\n                .span => \"span\",\n                .style => \"style\",\n                .table => \"table\",\n                .table_caption => \"caption\",\n                .table_cell => |e| e._tag_name.str(),\n                .table_col => |e| e._tag_name.str(),\n                .table_row => \"tr\",\n                .table_section => |e| e._tag_name.str(),\n                .template => \"template\",\n                .textarea => \"textarea\",\n                .time => \"time\",\n                .title => \"title\",\n                .track => \"track\",\n                .ul => \"ul\",\n                .unknown => |e| e._tag_name.str(),\n            },\n        },\n        .svg => |svg| return svg._tag_name.str(),\n    }\n}\n\npub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {\n    return switch (self._type) {\n        .html => |he| switch (he._type) {\n            .anchor => \"A\",\n            .area => \"AREA\",\n            .base => \"BASE\",\n            .body => \"BODY\",\n            .br => \"BR\",\n            .button => \"BUTTON\",\n            .canvas => \"CANVAS\",\n            .custom => |e| upperTagName(&e._tag_name, buf),\n            .data => \"DATA\",\n            .datalist => \"DATALIST\",\n            .details => \"DETAILS\",\n            .dialog => \"DIALOG\",\n            .directory => \"DIR\",\n            .div => \"DIV\",\n            .dl => \"DL\",\n            .embed => \"EMBED\",\n            .fieldset => \"FIELDSET\",\n            .font => \"FONT\",\n            .form => \"FORM\",\n            .generic => |e| upperTagName(&e._tag_name, buf),\n            .heading => |e| upperTagName(&e._tag_name, buf),\n            .head => \"HEAD\",\n            .html => \"HTML\",\n            .hr => \"HR\",\n            .iframe => \"IFRAME\",\n            .img => \"IMG\",\n            .input => \"INPUT\",\n            .label => \"LABEL\",\n            .legend => \"LEGEND\",\n            .li => \"LI\",\n            .link => \"LINK\",\n            .map => \"MAP\",\n            .meta => \"META\",\n            .media => |m| switch (m._type) {\n                .audio => \"AUDIO\",\n                .video => \"VIDEO\",\n                .generic => \"MEDIA\",\n            },\n            .meter => \"METER\",\n            .mod => |e| upperTagName(&e._tag_name, buf),\n            .object => \"OBJECT\",\n            .ol => \"OL\",\n            .optgroup => \"OPTGROUP\",\n            .option => \"OPTION\",\n            .output => \"OUTPUT\",\n            .p => \"P\",\n            .picture => \"PICTURE\",\n            .param => \"PARAM\",\n            .pre => \"PRE\",\n            .progress => \"PROGRESS\",\n            .quote => |e| upperTagName(&e._tag_name, buf),\n            .script => \"SCRIPT\",\n            .select => \"SELECT\",\n            .slot => \"SLOT\",\n            .source => \"SOURCE\",\n            .span => \"SPAN\",\n            .style => \"STYLE\",\n            .table => \"TABLE\",\n            .table_caption => \"CAPTION\",\n            .table_cell => |e| upperTagName(&e._tag_name, buf),\n            .table_col => |e| upperTagName(&e._tag_name, buf),\n            .table_row => \"TR\",\n            .table_section => |e| upperTagName(&e._tag_name, buf),\n            .template => \"TEMPLATE\",\n            .textarea => \"TEXTAREA\",\n            .time => \"TIME\",\n            .title => \"TITLE\",\n            .track => \"TRACK\",\n            .ul => \"UL\",\n            .unknown => |e| switch (self._namespace) {\n                .html => upperTagName(&e._tag_name, buf),\n                .svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),\n            },\n        },\n        .svg => |svg| svg._tag_name.str(),\n    };\n}\n\npub fn getTagNameDump(self: *const Element) []const u8 {\n    switch (self._type) {\n        .html => return self.getTagNameLower(),\n        .svg => |svg| return svg._tag_name.str(),\n    }\n}\n\npub fn getNamespaceURI(self: *const Element) ?[]const u8 {\n    return self._namespace.toUri();\n}\n\npub fn getNamespaceUri(self: *Element, page: *Page) ?[]const u8 {\n    if (self._namespace != .unknown) return self._namespace.toUri();\n    return page._element_namespace_uris.get(self);\n}\n\npub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, page: *Page) ?[]const u8 {\n    // Hardcoded reserved prefixes\n    if (prefix) |p| {\n        if (std.mem.eql(u8, p, \"xml\")) return \"http://www.w3.org/XML/1998/namespace\";\n        if (std.mem.eql(u8, p, \"xmlns\")) return \"http://www.w3.org/2000/xmlns/\";\n    }\n\n    // Step 1: check element's own namespace/prefix\n    if (self.getNamespaceUri(page)) |ns_uri| {\n        const el_prefix = self._prefix();\n        const match = if (prefix == null and el_prefix == null)\n            true\n        else if (prefix != null and el_prefix != null)\n            std.mem.eql(u8, prefix.?, el_prefix.?)\n        else\n            false;\n        if (match) return ns_uri;\n    }\n\n    // Step 2: search xmlns attributes\n    if (self._attributes) |attrs| {\n        var iter = attrs.iterator();\n        while (iter.next()) |entry| {\n            if (prefix == null) {\n                if (entry._name.eql(comptime .wrap(\"xmlns\"))) {\n                    const val = entry._value.str();\n                    return if (val.len == 0) null else val;\n                }\n            } else {\n                const name = entry._name.str();\n                if (std.mem.startsWith(u8, name, \"xmlns:\")) {\n                    if (std.mem.eql(u8, name[\"xmlns:\".len..], prefix.?)) {\n                        const val = entry._value.str();\n                        return if (val.len == 0) null else val;\n                    }\n                }\n            }\n        }\n    }\n\n    // Step 3: recurse to parent element\n    const parent = self.asNode().parentElement() orelse return null;\n    return parent.lookupNamespaceURIForElement(prefix, page);\n}\n\nfn _prefix(self: *const Element) ?[]const u8 {\n    const name = self.getTagNameLower();\n    if (std.mem.indexOfPos(u8, name, 0, \":\")) |pos| {\n        return name[0..pos];\n    }\n    return null;\n}\n\npub fn getLocalName(self: *Element) []const u8 {\n    const name = self.getTagNameLower();\n    if (std.mem.indexOfPos(u8, name, 0, \":\")) |pos| {\n        return name[pos + 1 ..];\n    }\n\n    return name;\n}\n\n// Wrapper methods that delegate to Html implementations\npub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {\n    const he = self.is(Html) orelse return error.NotHtmlElement;\n    return he.getInnerText(writer);\n}\n\npub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void {\n    const he = self.is(Html) orelse return error.NotHtmlElement;\n    return he.setInnerText(text, page);\n}\n\npub fn insertAdjacentHTML(\n    self: *Element,\n    position: []const u8,\n    html_or_xml: []const u8,\n    page: *Page,\n) !void {\n    const he = self.is(Html) orelse return error.NotHtmlElement;\n    return he.insertAdjacentHTML(position, html_or_xml, page);\n}\n\npub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {\n    const dump = @import(\"../dump.zig\");\n    return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);\n}\n\npub fn setOuterHTML(self: *Element, html: []const u8, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node._parent orelse return;\n\n    page.domChanged();\n    if (html.len > 0) {\n        const fragment = (try Node.DocumentFragment.init(page)).asNode();\n        try page.parseHtmlAsChildren(fragment, html);\n        try page.insertAllChildrenBefore(fragment, parent, node);\n    }\n\n    page.removeNode(parent, node, .{ .will_be_reconnected = false });\n}\n\npub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {\n    const dump = @import(\"../dump.zig\");\n    return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page);\n}\n\npub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {\n    const parent = self.asNode();\n\n    // Remove all existing children\n    page.domChanged();\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        page.removeNode(parent, child, .{ .will_be_reconnected = false });\n    }\n\n    // Fast path: skip parsing if html is empty\n    if (html.len == 0) {\n        return;\n    }\n\n    // Parse and add new children\n    try page.parseHtmlAsChildren(parent, html);\n}\n\npub fn getId(self: *const Element) []const u8 {\n    return self.getAttributeSafe(comptime .wrap(\"id\")) orelse \"\";\n}\n\npub fn setId(self: *Element, value: []const u8, page: *Page) !void {\n    return self.setAttributeSafe(comptime .wrap(\"id\"), .wrap(value), page);\n}\n\npub fn getSlot(self: *const Element) []const u8 {\n    return self.getAttributeSafe(comptime .wrap(\"slot\")) orelse \"\";\n}\n\npub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {\n    return self.setAttributeSafe(comptime .wrap(\"slot\"), .wrap(value), page);\n}\n\npub fn getDir(self: *const Element) []const u8 {\n    return self.getAttributeSafe(comptime .wrap(\"dir\")) orelse \"\";\n}\n\npub fn setDir(self: *Element, value: []const u8, page: *Page) !void {\n    return self.setAttributeSafe(comptime .wrap(\"dir\"), .wrap(value), page);\n}\n\npub fn getClassName(self: *const Element) []const u8 {\n    return self.getAttributeSafe(comptime .wrap(\"class\")) orelse \"\";\n}\n\npub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {\n    return self.setAttributeSafe(comptime .wrap(\"class\"), .wrap(value), page);\n}\n\npub fn attributeIterator(self: *Element) Attribute.InnerIterator {\n    const attributes = self._attributes orelse return .{};\n    return attributes.iterator();\n}\n\npub fn getAttribute(self: *const Element, name: String, page: *Page) !?String {\n    const attributes = self._attributes orelse return null;\n    return attributes.get(name, page);\n}\n\n/// For simplicity, the namespace is currently ignored and only the local name is used.\npub fn getAttributeNS(\n    self: *const Element,\n    maybe_namespace: ?[]const u8,\n    local_name: String,\n    page: *Page,\n) !?String {\n    if (maybe_namespace) |namespace| {\n        if (!std.mem.eql(u8, namespace, \"http://www.w3.org/1999/xhtml\")) {\n            log.warn(.not_implemented, \"Element.getAttributeNS\", .{ .namespace = namespace });\n        }\n    }\n\n    return self.getAttribute(local_name, page);\n}\n\npub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {\n    const attributes = self._attributes orelse return null;\n    return attributes.getSafe(name);\n}\n\npub fn hasAttribute(self: *const Element, name: String, page: *Page) !bool {\n    const attributes = self._attributes orelse return false;\n    const value = try attributes.get(name, page);\n    return value != null;\n}\n\npub fn hasAttributeSafe(self: *const Element, name: String) bool {\n    const attributes = self._attributes orelse return false;\n    return attributes.hasSafe(name);\n}\n\npub fn hasAttributes(self: *const Element) bool {\n    const attributes = self._attributes orelse return false;\n    return attributes.isEmpty() == false;\n}\n\npub fn getAttributeNode(self: *Element, name: String, page: *Page) !?*Attribute {\n    const attributes = self._attributes orelse return null;\n    return attributes.getAttribute(name, self, page);\n}\n\npub fn setAttribute(self: *Element, name: String, value: String, page: *Page) !void {\n    try Attribute.validateAttributeName(name);\n    const attributes = try self.getOrCreateAttributeList(page);\n    _ = try attributes.put(name, value, self, page);\n}\n\npub fn setAttributeNS(\n    self: *Element,\n    maybe_namespace: ?[]const u8,\n    qualified_name: []const u8,\n    value: String,\n    page: *Page,\n) !void {\n    const attr_name = if (maybe_namespace) |namespace| blk: {\n        // For xmlns namespace, store the full qualified name (e.g. \"xmlns:bar\")\n        // so lookupNamespaceURI can find namespace declarations.\n        if (std.mem.eql(u8, namespace, \"http://www.w3.org/2000/xmlns/\")) {\n            break :blk qualified_name;\n        }\n        if (!std.mem.eql(u8, namespace, \"http://www.w3.org/1999/xhtml\")) {\n            log.warn(.not_implemented, \"Element.setAttributeNS\", .{ .namespace = namespace });\n        }\n        break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|\n            qualified_name[idx + 1 ..]\n        else\n            qualified_name;\n    } else blk: {\n        break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|\n            qualified_name[idx + 1 ..]\n        else\n            qualified_name;\n    };\n    return self.setAttribute(.wrap(attr_name), value, page);\n}\n\npub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {\n    const attributes = try self.getOrCreateAttributeList(page);\n    _ = try attributes.putSafe(name, value, self, page);\n}\n\npub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {\n    return self._attributes orelse return self.createAttributeList(page);\n}\n\npub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List {\n    lp.assert(self._attributes == null, \"Element.createAttributeList non-null _attributes\", .{});\n    const a = try page.arena.create(Attribute.List);\n    a.* = .{ .normalize = self._namespace == .html };\n    self._attributes = a;\n    return a;\n}\n\npub fn getShadowRoot(self: *Element, page: *Page) ?*ShadowRoot {\n    const shadow_root = page._element_shadow_roots.get(self) orelse return null;\n    if (shadow_root._mode == .closed) return null;\n    return shadow_root;\n}\n\npub fn getAssignedSlot(self: *Element, page: *Page) ?*Html.Slot {\n    return page._element_assigned_slots.get(self);\n}\n\npub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowRoot {\n    if (page._element_shadow_roots.get(self)) |_| {\n        return error.AlreadyHasShadowRoot;\n    }\n    const mode = try ShadowRoot.Mode.fromString(mode_str);\n    const shadow_root = try ShadowRoot.init(self, mode, page);\n    try page._element_shadow_roots.put(page.arena, self, shadow_root);\n    return shadow_root;\n}\n\npub fn insertAdjacentElement(\n    self: *Element,\n    position: []const u8,\n    element: *Element,\n    page: *Page,\n) !void {\n    const target_node, const prev_node = try self.asNode().findAdjacentNodes(position);\n    _ = try target_node.insertBefore(element.asNode(), prev_node, page);\n}\n\npub fn insertAdjacentText(\n    self: *Element,\n    where: []const u8,\n    data: []const u8,\n    page: *Page,\n) !void {\n    const text_node = try page.createTextNode(data);\n    const target_node, const prev_node = try self.asNode().findAdjacentNodes(where);\n    _ = try target_node.insertBefore(text_node, prev_node, page);\n}\n\npub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute {\n    if (attr._element) |el| {\n        if (el == self) {\n            return attr;\n        }\n        attr._element = null;\n        _ = try el.removeAttributeNode(attr, page);\n    }\n\n    const attributes = try self.getOrCreateAttributeList(page);\n    return attributes.putAttribute(attr, self, page);\n}\n\npub fn removeAttribute(self: *Element, name: String, page: *Page) !void {\n    const attributes = self._attributes orelse return;\n    return attributes.delete(name, self, page);\n}\n\npub fn toggleAttribute(self: *Element, name: String, force: ?bool, page: *Page) !bool {\n    try Attribute.validateAttributeName(name);\n    const has = try self.hasAttribute(name, page);\n\n    const should_add = force orelse !has;\n\n    if (should_add and !has) {\n        try self.setAttribute(name, String.empty, page);\n        return true;\n    } else if (!should_add and has) {\n        try self.removeAttribute(name, page);\n        return false;\n    }\n\n    return should_add;\n}\n\npub fn removeAttributeNode(self: *Element, attr: *Attribute, page: *Page) !*Attribute {\n    if (attr._element == null or attr._element.? != self) {\n        return error.NotFound;\n    }\n    try self.removeAttribute(attr._name, page);\n    attr._element = null;\n    return attr;\n}\n\npub fn getAttributeNames(self: *const Element, page: *Page) ![][]const u8 {\n    const attributes = self._attributes orelse return &.{};\n    return attributes.getNames(page);\n}\n\npub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNodeMap {\n    const gop = try page._attribute_named_node_map_lookup.getOrPut(page.arena, @intFromPtr(self));\n    if (!gop.found_existing) {\n        const attributes = try self.getOrCreateAttributeList(page);\n        const named_node_map = try page._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });\n        gop.value_ptr.* = named_node_map;\n    }\n    return gop.value_ptr.*;\n}\n\npub fn getOrCreateStyle(self: *Element, page: *Page) !*CSSStyleProperties {\n    const gop = try page._element_styles.getOrPut(page.arena, self);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = try CSSStyleProperties.init(self, false, page);\n    }\n    return gop.value_ptr.*;\n}\n\nfn getStyle(self: *Element, page: *Page) ?*CSSStyleProperties {\n    return page._element_styles.get(self);\n}\n\npub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {\n    const gop = try page._element_class_lists.getOrPut(page.arena, self);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{\n            ._element = self,\n            ._attribute_name = comptime .wrap(\"class\"),\n        });\n    }\n    return gop.value_ptr.*;\n}\n\npub fn setClassList(self: *Element, value: String, page: *Page) !void {\n    const class_list = try self.getClassList(page);\n    try class_list.setValue(value, page);\n}\n\npub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {\n    const gop = try page._element_rel_lists.getOrPut(page.arena, self);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{\n            ._element = self,\n            ._attribute_name = comptime .wrap(\"rel\"),\n        });\n    }\n    return gop.value_ptr.*;\n}\n\npub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {\n    const gop = try page._element_datasets.getOrPut(page.arena, self);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = try page._factory.create(DOMStringMap{\n            ._element = self,\n        });\n    }\n    return gop.value_ptr.*;\n}\n\npub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {\n    page.domChanged();\n    var parent = self.asNode();\n\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        page.removeNode(parent, child, .{ .will_be_reconnected = false });\n    }\n\n    const parent_is_connected = parent.isConnected();\n    for (nodes) |node_or_text| {\n        var child_connected = false;\n        const child = try node_or_text.toNode(page);\n        if (child._parent) |previous_parent| {\n            child_connected = child.isConnected();\n            page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });\n        }\n        try page.appendNode(parent, child, .{ .child_already_connected = child_connected });\n    }\n}\n\npub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {\n    page.domChanged();\n\n    const ref_node = self.asNode();\n    const parent = ref_node._parent orelse return;\n\n    const parent_is_connected = parent.isConnected();\n\n    // Detect if the ref_node must be removed (byt default) or kept.\n    // We kept it when ref_node is present into the nodes list.\n    var rm_ref_node = true;\n\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n\n        // If a child is the ref node. We keep it at its own current position.\n        if (child == ref_node) {\n            rm_ref_node = false;\n            continue;\n        }\n\n        if (child._parent) |current_parent| {\n            page.removeNode(current_parent, child, .{ .will_be_reconnected = parent_is_connected });\n        }\n\n        try page.insertNodeRelative(\n            parent,\n            child,\n            .{ .before = ref_node },\n            .{ .child_already_connected = child.isConnected() },\n        );\n    }\n\n    if (rm_ref_node) {\n        page.removeNode(parent, ref_node, .{ .will_be_reconnected = false });\n    }\n}\n\npub fn remove(self: *Element, page: *Page) void {\n    page.domChanged();\n    const node = self.asNode();\n    const parent = node._parent orelse return;\n    page.removeNode(parent, node, .{ .will_be_reconnected = false });\n}\n\npub fn focus(self: *Element, page: *Page) !void {\n    if (self.asNode().isConnected() == false) {\n        // a disconnected node cannot take focus\n        return;\n    }\n\n    const FocusEvent = @import(\"event/FocusEvent.zig\");\n\n    const new_target = self.asEventTarget();\n    const old_active = page.document._active_element;\n    page.document._active_element = self;\n\n    if (old_active) |old| {\n        if (old == self) {\n            return;\n        }\n\n        const old_target = old.asEventTarget();\n\n        // Dispatch blur on old element (no bubble, composed)\n        const blur_event = try FocusEvent.initTrusted(comptime .wrap(\"blur\"), .{ .composed = true, .relatedTarget = new_target }, page);\n        try page._event_manager.dispatch(old_target, blur_event.asEvent());\n\n        // Dispatch focusout on old element (bubbles, composed)\n        const focusout_event = try FocusEvent.initTrusted(comptime .wrap(\"focusout\"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);\n        try page._event_manager.dispatch(old_target, focusout_event.asEvent());\n    }\n\n    const old_related: ?*EventTarget = if (old_active) |old| old.asEventTarget() else null;\n\n    // Dispatch focus on new element (no bubble, composed)\n    const focus_event = try FocusEvent.initTrusted(comptime .wrap(\"focus\"), .{ .composed = true, .relatedTarget = old_related }, page);\n    try page._event_manager.dispatch(new_target, focus_event.asEvent());\n\n    // Dispatch focusin on new element (bubbles, composed)\n    const focusin_event = try FocusEvent.initTrusted(comptime .wrap(\"focusin\"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);\n    try page._event_manager.dispatch(new_target, focusin_event.asEvent());\n}\n\npub fn blur(self: *Element, page: *Page) !void {\n    if (page.document._active_element != self) return;\n\n    page.document._active_element = null;\n\n    const FocusEvent = @import(\"event/FocusEvent.zig\");\n    const old_target = self.asEventTarget();\n\n    // Dispatch blur (no bubble, composed)\n    const blur_event = try FocusEvent.initTrusted(comptime .wrap(\"blur\"), .{ .composed = true }, page);\n    try page._event_manager.dispatch(old_target, blur_event.asEvent());\n\n    // Dispatch focusout (bubbles, composed)\n    const focusout_event = try FocusEvent.initTrusted(comptime .wrap(\"focusout\"), .{ .bubbles = true, .composed = true }, page);\n    try page._event_manager.dispatch(old_target, focusout_event.asEvent());\n}\n\npub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {\n    return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);\n}\n\npub fn append(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const parent = self.asNode();\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        _ = try parent.appendChild(child, page);\n    }\n}\n\npub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const parent = self.asNode();\n    var i = nodes.len;\n    while (i > 0) {\n        i -= 1;\n        const child = try nodes[i].toNode(page);\n        _ = try parent.insertBefore(child, parent.firstChild(), page);\n    }\n}\n\npub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node.parentNode() orelse return;\n\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        _ = try parent.insertBefore(child, node, page);\n    }\n}\n\npub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {\n    const node = self.asNode();\n    const parent = node.parentNode() orelse return;\n    const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);\n\n    for (nodes) |node_or_text| {\n        const child = try node_or_text.toNode(page);\n        _ = try parent.insertBefore(child, viable_next, page);\n    }\n}\n\npub fn firstElementChild(self: *Element) ?*Element {\n    var maybe_child = self.asNode().firstChild();\n    while (maybe_child) |child| {\n        if (child.is(Element)) |el| return el;\n        maybe_child = child.nextSibling();\n    }\n    return null;\n}\n\npub fn lastElementChild(self: *Element) ?*Element {\n    var maybe_child = self.asNode().lastChild();\n    while (maybe_child) |child| {\n        if (child.is(Element)) |el| return el;\n        maybe_child = child.previousSibling();\n    }\n    return null;\n}\n\npub fn nextElementSibling(self: *Element) ?*Element {\n    var maybe_sibling = self.asNode().nextSibling();\n    while (maybe_sibling) |sibling| {\n        if (sibling.is(Element)) |el| return el;\n        maybe_sibling = sibling.nextSibling();\n    }\n    return null;\n}\n\npub fn previousElementSibling(self: *Element) ?*Element {\n    var maybe_sibling = self.asNode().previousSibling();\n    while (maybe_sibling) |sibling| {\n        if (sibling.is(Element)) |el| return el;\n        maybe_sibling = sibling.previousSibling();\n    }\n    return null;\n}\n\npub fn getChildElementCount(self: *Element) usize {\n    var count: usize = 0;\n    var it = self.asNode().childrenIterator();\n    while (it.next()) |node| {\n        if (node.is(Element) != null) {\n            count += 1;\n        }\n    }\n    return count;\n}\n\npub fn matches(self: *Element, selector: []const u8, page: *Page) !bool {\n    return Selector.matches(self, selector, page);\n}\n\npub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element {\n    return Selector.querySelector(self.asNode(), selector, page);\n}\n\npub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Selector.List {\n    return Selector.querySelectorAll(self.asNode(), input, page);\n}\n\npub fn getAnimations(_: *const Element) []*Animation {\n    return &.{};\n}\n\npub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, page: *Page) !*Animation {\n    return Animation.init(page);\n}\n\npub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element {\n    if (selector.len == 0) {\n        return error.SyntaxError;\n    }\n\n    var current: ?*Element = self;\n    while (current) |el| {\n        if (try Selector.matchesWithScope(el, selector, self, page)) {\n            return el;\n        }\n\n        const parent = el._proto._parent orelse break;\n\n        if (parent.is(ShadowRoot) != null) {\n            break;\n        }\n\n        current = parent.is(Element);\n    }\n    return null;\n}\n\npub fn parentElement(self: *Element) ?*Element {\n    return self._proto.parentElement();\n}\n\npub fn checkVisibility(self: *Element, page: *Page) bool {\n    var current: ?*Element = self;\n\n    while (current) |el| {\n        if (el.getStyle(page)) |style| {\n            const display = style.asCSSStyleDeclaration().getPropertyValue(\"display\", page);\n            if (std.mem.eql(u8, display, \"none\")) {\n                return false;\n            }\n        }\n        current = el.parentElement();\n    }\n\n    return true;\n}\n\nfn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {\n    var width: f64 = 5.0;\n    var height: f64 = 5.0;\n\n    if (self.getStyle(page)) |style| {\n        const decl = style.asCSSStyleDeclaration();\n        width = CSS.parseDimension(decl.getPropertyValue(\"width\", page)) orelse 5.0;\n        height = CSS.parseDimension(decl.getPropertyValue(\"height\", page)) orelse 5.0;\n    }\n\n    if (width == 5.0 or height == 5.0) {\n        const tag = self.getTag();\n\n        // Root containers get large default size to contain descendant positions.\n        // With calculateDocumentPosition using linear depth scaling (100px per level),\n        // even very deep trees (100 levels) stay within 10,000px.\n        // 100M pixels is plausible for very long documents.\n        if (tag == .html or tag == .body) {\n            if (width == 5.0) width = 1920.0;\n            if (height == 5.0) height = 100_000_000.0;\n        } else if (tag == .img or tag == .iframe) {\n            if (self.getAttributeSafe(comptime .wrap(\"width\"))) |w| {\n                width = std.fmt.parseFloat(f64, w) catch width;\n            }\n            if (self.getAttributeSafe(comptime .wrap(\"height\"))) |h| {\n                height = std.fmt.parseFloat(f64, h) catch height;\n            }\n        }\n    }\n\n    return .{ .width = width, .height = height };\n}\n\npub fn getClientWidth(self: *Element, page: *Page) f64 {\n    if (!self.checkVisibility(page)) {\n        return 0.0;\n    }\n    const dims = self.getElementDimensions(page);\n    return dims.width;\n}\n\npub fn getClientHeight(self: *Element, page: *Page) f64 {\n    if (!self.checkVisibility(page)) {\n        return 0.0;\n    }\n    const dims = self.getElementDimensions(page);\n    return dims.height;\n}\n\npub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {\n    if (!self.checkVisibility(page)) {\n        return .{\n            ._x = 0.0,\n            ._y = 0.0,\n            ._width = 0.0,\n            ._height = 0.0,\n        };\n    }\n\n    return self.getBoundingClientRectForVisible(page);\n}\n\n// Some cases need a the BoundingClientRect but have already done the\n// visibility check.\npub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect {\n    const y = calculateDocumentPosition(self.asNode());\n    const dims = self.getElementDimensions(page);\n\n    // Use sibling position for x coordinate to ensure siblings have different x values\n    const x = calculateSiblingPosition(self.asNode());\n\n    return .{\n        ._x = x,\n        ._y = y,\n        ._width = dims.width,\n        ._height = dims.height,\n    };\n}\n\npub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {\n    if (!self.checkVisibility(page)) {\n        return &.{};\n    }\n    const rects = try page.call_arena.alloc(DOMRect, 1);\n    rects[0] = self.getBoundingClientRectForVisible(page);\n    return rects;\n}\n\npub fn getScrollTop(self: *Element, page: *Page) u32 {\n    const pos = page._element_scroll_positions.get(self) orelse return 0;\n    return pos.y;\n}\n\npub fn setScrollTop(self: *Element, value: i32, page: *Page) !void {\n    const gop = try page._element_scroll_positions.getOrPut(page.arena, self);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = .{};\n    }\n    gop.value_ptr.y = @intCast(@max(0, value));\n}\n\npub fn getScrollLeft(self: *Element, page: *Page) u32 {\n    const pos = page._element_scroll_positions.get(self) orelse return 0;\n    return pos.x;\n}\n\npub fn setScrollLeft(self: *Element, value: i32, page: *Page) !void {\n    const gop = try page._element_scroll_positions.getOrPut(page.arena, self);\n    if (!gop.found_existing) {\n        gop.value_ptr.* = .{};\n    }\n    gop.value_ptr.x = @intCast(@max(0, value));\n}\n\npub fn getScrollHeight(self: *Element, page: *Page) f64 {\n    // In our dummy layout engine, content doesn't overflow\n    return self.getClientHeight(page);\n}\n\npub fn getScrollWidth(self: *Element, page: *Page) f64 {\n    // In our dummy layout engine, content doesn't overflow\n    return self.getClientWidth(page);\n}\n\npub fn getOffsetHeight(self: *Element, page: *Page) f64 {\n    if (!self.checkVisibility(page)) {\n        return 0.0;\n    }\n    const dims = self.getElementDimensions(page);\n    return dims.height;\n}\n\npub fn getOffsetWidth(self: *Element, page: *Page) f64 {\n    if (!self.checkVisibility(page)) {\n        return 0.0;\n    }\n    const dims = self.getElementDimensions(page);\n    return dims.width;\n}\n\npub fn getOffsetTop(self: *Element, page: *Page) f64 {\n    if (!self.checkVisibility(page)) {\n        return 0.0;\n    }\n    return calculateDocumentPosition(self.asNode());\n}\n\npub fn getOffsetLeft(self: *Element, page: *Page) f64 {\n    if (!self.checkVisibility(page)) {\n        return 0.0;\n    }\n    return calculateSiblingPosition(self.asNode());\n}\n\npub fn getClientTop(_: *Element) f64 {\n    // Border width - in our dummy layout, we don't apply borders to layout\n    return 0.0;\n}\n\npub fn getClientLeft(_: *Element) f64 {\n    // Border width - in our dummy layout, we don't apply borders to layout\n    return 0.0;\n}\n\n// Calculates document position by counting all nodes that appear before this one\n// in tree order, but only traversing the \"left side\" of the tree.\n//\n// This walks up from the target node to the root, and at each level counts:\n// 1. All previous siblings and their descendants\n// 2. The parent itself\n//\n// Example:\n//   <body>              → y=0\n//     <h1>Text</h1>     → y=1    (body=1)\n//     <h2>              → y=2    (body=1 + h1=1)\n//       <a>Link1</a>    → y=3    (body=1 + h1=1 + h2=1)\n//     </h2>\n//     <p>Text</p>       → y=5    (body=1 + h1=1 + h2=2)\n//     <h2>              → y=6    (body=1 + h1=1 + h2=2 + p=1)\n//       <a>Link2</a>    → y=7    (body=1 + h1=1 + h2=2 + p=1 + h2=1)\n//     </h2>\n//   </body>\n//\n// Trade-offs:\n// - O(depth × siblings × subtree_height) - only left-side traversal\n// - Linear scaling: 5px per node\n// - Perfect document order, guaranteed unique positions\n// - Compact coordinates (1000 nodes ≈ 5,000px)\nfn calculateDocumentPosition(node: *Node) f64 {\n    var position: f64 = 0.0;\n    var current = node;\n\n    // Walk up to root, counting preceding nodes\n    while (current.parentNode()) |parent| {\n        // Count all previous siblings and their descendants\n        var sibling = parent.firstChild();\n        while (sibling) |s| {\n            if (s == current) break;\n            position += countSubtreeNodes(s);\n            sibling = s.nextSibling();\n        }\n\n        // Count the parent itself\n        position += 1.0;\n        current = parent;\n    }\n\n    return position * 5.0; // 5px per node\n}\n\n// Counts total nodes in a subtree (node + all descendants)\nfn countSubtreeNodes(node: *Node) f64 {\n    var count: f64 = 1.0; // Count this node\n\n    var child = node.firstChild();\n    while (child) |c| {\n        count += countSubtreeNodes(c);\n        child = c.nextSibling();\n    }\n\n    return count;\n}\n\n// Calculates horizontal position using the same approach as y,\n// just scaled differently for visual distinction\nfn calculateSiblingPosition(node: *Node) f64 {\n    var position: f64 = 0.0;\n    var current = node;\n\n    // Walk up to root, counting preceding nodes (same as y)\n    while (current.parentNode()) |parent| {\n        // Count all previous siblings and their descendants\n        var sibling = parent.firstChild();\n        while (sibling) |s| {\n            if (s == current) break;\n            position += countSubtreeNodes(s);\n            sibling = s.nextSibling();\n        }\n\n        // Count the parent itself\n        position += 1.0;\n        current = parent;\n    }\n\n    return position * 5.0; // 5px per node\n}\n\npub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {\n    return self.asNode().getElementsByTagName(tag_name, page);\n}\n\npub fn getElementsByTagNameNS(self: *Element, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {\n    return self.asNode().getElementsByTagNameNS(namespace, local_name, page);\n}\n\npub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {\n    return self.asNode().getElementsByClassName(class_name, page);\n}\n\npub fn clone(self: *Element, deep: bool, page: *Page) !*Node {\n    const tag_name = self.getTagNameDump();\n    const node = try page.createElementNS(self._namespace, tag_name, self._attributes);\n\n    // Allow element-specific types to copy their runtime state\n    _ = Element.Build.call(node.as(Element), \"cloned\", .{ self, node.as(Element), page }) catch |err| {\n        log.err(.dom, \"element.clone.failed\", .{ .err = err });\n    };\n\n    if (deep) {\n        var child_it = self.asNode().childrenIterator();\n        while (child_it.next()) |child| {\n            if (try child.cloneNodeForAppending(true, page)) |cloned_child| {\n                // We pass `true` to `child_already_connected` as a hacky optimization\n                // We _know_ this child isn't connected (Because the parent isn't connected)\n                // setting this to `true` skips all connection checks.\n                try page.appendNode(node, cloned_child, .{ .child_already_connected = true });\n            }\n        }\n    }\n\n    return node;\n}\n\npub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {\n    _ = center_if_needed;\n}\n\nconst ScrollIntoViewOpts = union {\n    align_to_top: bool,\n    obj: js.Object,\n};\npub fn scrollIntoView(_: *const Element, opts: ?ScrollIntoViewOpts) void {\n    _ = opts;\n}\n\npub fn format(self: *Element, writer: *std.Io.Writer) !void {\n    try writer.writeByte('<');\n    try writer.writeAll(self.getTagNameDump());\n\n    if (self._attributes) |attributes| {\n        var it = attributes.iterator();\n        while (it.next()) |attr| {\n            try writer.print(\" {f}\", .{attr});\n        }\n    }\n    try writer.writeByte('>');\n}\n\nfn upperTagName(tag_name: *String, buf: []u8) []const u8 {\n    if (tag_name.len > buf.len) {\n        log.info(.dom, \"tag.long.name\", .{ .name = tag_name.str() });\n        return tag_name.str();\n    }\n    const tag = tag_name.str();\n    return std.ascii.upperString(buf, tag);\n}\n\npub fn getTag(self: *const Element) Tag {\n    return switch (self._type) {\n        .html => |he| switch (he._type) {\n            .anchor => .anchor,\n            .area => .area,\n            .base => .base,\n            .div => .div,\n            .dl => .dl,\n            .embed => .embed,\n            .form => .form,\n            .p => .p,\n            .custom => .custom,\n            .data => .data,\n            .datalist => .datalist,\n            .details => .details,\n            .dialog => .dialog,\n            .directory => .directory,\n            .iframe => .iframe,\n            .img => .img,\n            .br => .br,\n            .button => .button,\n            .canvas => .canvas,\n            .fieldset => .fieldset,\n            .font => .font,\n            .heading => |h| h._tag,\n            .label => .label,\n            .legend => .legend,\n            .li => .li,\n            .map => .map,\n            .ul => .ul,\n            .ol => .ol,\n            .object => .object,\n            .optgroup => .optgroup,\n            .output => .output,\n            .picture => .picture,\n            .param => .param,\n            .pre => .pre,\n            .generic => |g| g._tag,\n            .media => |m| switch (m._type) {\n                .audio => .audio,\n                .video => .video,\n                .generic => .media,\n            },\n            .meter => .meter,\n            .mod => |m| m._tag,\n            .progress => .progress,\n            .quote => |q| q._tag,\n            .script => .script,\n            .select => .select,\n            .slot => .slot,\n            .source => .source,\n            .span => .span,\n            .option => .option,\n            .table => .table,\n            .table_caption => .caption,\n            .table_cell => |tc| tc._tag,\n            .table_col => |tc| tc._tag,\n            .table_row => .tr,\n            .table_section => |ts| ts._tag,\n            .template => .template,\n            .textarea => .textarea,\n            .time => .time,\n            .track => .track,\n            .input => .input,\n            .link => .link,\n            .meta => .meta,\n            .hr => .hr,\n            .style => .style,\n            .title => .title,\n            .body => .body,\n            .html => .html,\n            .head => .head,\n            .unknown => .unknown,\n        },\n        .svg => |se| switch (se._type) {\n            .svg => .svg,\n            .generic => |g| g._tag,\n        },\n    };\n}\n\npub const Tag = enum {\n    address,\n    anchor,\n    audio,\n    area,\n    aside,\n    article,\n    b,\n    blockquote,\n    body,\n    br,\n    button,\n    base,\n    canvas,\n    caption,\n    circle,\n    code,\n    col,\n    colgroup,\n    custom,\n    data,\n    datalist,\n    dd,\n    details,\n    del,\n    dfn,\n    dialog,\n    div,\n    directory,\n    dl,\n    dt,\n    embed,\n    ellipse,\n    em,\n    fieldset,\n    figure,\n    form,\n    font,\n    footer,\n    g,\n    h1,\n    h2,\n    h3,\n    h4,\n    h5,\n    h6,\n    head,\n    header,\n    heading,\n    hgroup,\n    hr,\n    html,\n    i,\n    iframe,\n    img,\n    input,\n    ins,\n    label,\n    legend,\n    li,\n    line,\n    link,\n    main,\n    map,\n    marquee,\n    media,\n    menu,\n    meta,\n    meter,\n    nav,\n    noscript,\n    object,\n    ol,\n    optgroup,\n    option,\n    output,\n    p,\n    path,\n    param,\n    picture,\n    polygon,\n    polyline,\n    pre,\n    progress,\n    quote,\n    rect,\n    s,\n    script,\n    section,\n    select,\n    slot,\n    source,\n    span,\n    strong,\n    style,\n    sub,\n    summary,\n    sup,\n    svg,\n    table,\n    time,\n    tbody,\n    td,\n    text,\n    template,\n    textarea,\n    tfoot,\n    th,\n    thead,\n    title,\n    tr,\n    track,\n    ul,\n    video,\n    unknown,\n\n    // If the tag is \"unknown\", we can't use the optimized tag matching, but\n    // need to fallback to the actual tag name\n    pub fn parseForMatch(lower: []const u8) ?Tag {\n        const tag = std.meta.stringToEnum(Tag, lower) orelse return null;\n        return switch (tag) {\n            .unknown, .custom => null,\n            else => tag,\n        };\n    }\n\n    pub fn isBlock(self: Tag) bool {\n        // zig fmt: off\n        return switch (self) {\n            // Semantic Layout\n            .article, .aside, .footer, .header, .main, .nav, .section,\n            // Grouping / Containers\n            .address, .div, .fieldset, .figure, .p,\n            // Headings\n            .h1, .h2, .h3, .h4, .h5, .h6,\n            // Lists\n            .dl, .ol, .ul,\n            // Preformatted / Quotes\n            .blockquote, .pre,\n            // Tables\n            .table,\n            // Other\n            .hr,\n            => true,\n            else => false,\n        };\n        // zig fmt: on\n    }\n\n    pub fn isMetadata(self: Tag) bool {\n        return switch (self) {\n            .base, .head, .link, .meta, .noscript, .script, .style, .template, .title => true,\n            else => false,\n        };\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Element);\n\n    pub const Meta = struct {\n        pub const name = \"Element\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const tagName = bridge.accessor(_tagName, null, .{});\n    fn _tagName(self: *Element, page: *Page) []const u8 {\n        return self.getTagNameSpec(&page.buf);\n    }\n    pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});\n\n    pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{});\n    fn _innerText(self: *Element, page: *const Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.getInnerText(&buf.writer);\n        return buf.written();\n    }\n\n    pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{});\n    fn _outerHTML(self: *Element, page: *Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.getOuterHTML(&buf.writer, page);\n        return buf.written();\n    }\n\n    pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});\n    fn _innerHTML(self: *Element, page: *Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.getInnerHTML(&buf.writer, page);\n        return buf.written();\n    }\n\n    pub const prefix = bridge.accessor(Element._prefix, null, .{});\n\n    pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });\n    fn _setAttribute(self: *Element, name: String, value: js.Value, page: *Page) !void {\n        return self.setAttribute(name, .wrap(try value.toStringSlice()), page);\n    }\n\n    pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true });\n    fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, page: *Page) !void {\n        return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), page);\n    }\n\n    pub const localName = bridge.accessor(Element.getLocalName, null, .{});\n    pub const id = bridge.accessor(Element.getId, Element.setId, .{});\n    pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});\n    pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});\n    pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});\n    pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});\n    pub const dataset = bridge.accessor(Element.getDataset, null, .{});\n    pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{});\n    pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});\n    pub const hasAttribute = bridge.function(Element.hasAttribute, .{});\n    pub const hasAttributes = bridge.function(Element.hasAttributes, .{});\n    pub const getAttribute = bridge.function(Element.getAttribute, .{});\n    pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});\n    pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});\n    pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});\n    pub const removeAttribute = bridge.function(Element.removeAttribute, .{});\n    pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });\n    pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});\n    pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });\n    pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});\n    pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});\n    pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });\n    pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true });\n    pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true });\n    pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true });\n\n    const ShadowRootInit = struct {\n        mode: []const u8,\n    };\n    fn _attachShadow(self: *Element, init: ShadowRootInit, page: *Page) !*ShadowRoot {\n        return self.attachShadow(init.mode, page);\n    }\n    pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true });\n    pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true });\n    pub const remove = bridge.function(Element.remove, .{});\n    pub const append = bridge.function(Element.append, .{ .dom_exception = true });\n    pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true });\n    pub const before = bridge.function(Element.before, .{ .dom_exception = true });\n    pub const after = bridge.function(Element.after, .{ .dom_exception = true });\n    pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});\n    pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});\n    pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});\n    pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});\n    pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});\n    pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });\n    pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });\n    pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });\n    pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });\n    pub const getAnimations = bridge.function(Element.getAnimations, .{});\n    pub const animate = bridge.function(Element.animate, .{});\n    pub const checkVisibility = bridge.function(Element.checkVisibility, .{});\n    pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{});\n    pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{});\n    pub const clientTop = bridge.accessor(Element.getClientTop, null, .{});\n    pub const clientLeft = bridge.accessor(Element.getClientLeft, null, .{});\n    pub const scrollTop = bridge.accessor(Element.getScrollTop, Element.setScrollTop, .{});\n    pub const scrollLeft = bridge.accessor(Element.getScrollLeft, Element.setScrollLeft, .{});\n    pub const scrollHeight = bridge.accessor(Element.getScrollHeight, null, .{});\n    pub const scrollWidth = bridge.accessor(Element.getScrollWidth, null, .{});\n    pub const offsetTop = bridge.accessor(Element.getOffsetTop, null, .{});\n    pub const offsetLeft = bridge.accessor(Element.getOffsetLeft, null, .{});\n    pub const offsetWidth = bridge.accessor(Element.getOffsetWidth, null, .{});\n    pub const offsetHeight = bridge.accessor(Element.getOffsetHeight, null, .{});\n    pub const getClientRects = bridge.function(Element.getClientRects, .{});\n    pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});\n    pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});\n    pub const getElementsByTagNameNS = bridge.function(Element.getElementsByTagNameNS, .{});\n    pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});\n    pub const children = bridge.accessor(Element.getChildren, null, .{});\n    pub const focus = bridge.function(Element.focus, .{});\n    pub const blur = bridge.function(Element.blur, .{});\n    pub const scrollIntoView = bridge.function(Element.scrollIntoView, .{});\n    pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});\n};\n\npub const Build = struct {\n    // Calls `func_name` with `args` on the most specific type where it is\n    // implement. This could be on the Element itself.\n    pub fn call(self: *const Element, comptime func_name: []const u8, args: anytype) !bool {\n        inline for (@typeInfo(Element.Type).@\"union\".fields) |f| {\n            if (@field(Element.Type, f.name) == self._type) {\n                // The inner type implements this function. Call it and we're done.\n                const S = reflect.Struct(f.type);\n                if (@hasDecl(S, \"Build\")) {\n                    if (@hasDecl(S.Build, \"call\")) {\n                        const sub = @field(self._type, f.name);\n                        return S.Build.call(sub, func_name, args);\n                    }\n\n                    // The inner type implements this function. Call it and we're done.\n                    if (@hasDecl(f.type, func_name)) {\n                        return @call(.auto, @field(f.type, func_name), args);\n                    }\n                }\n            }\n        }\n\n        if (@hasDecl(Element.Build, func_name)) {\n            // Our last resort - the element implements this function.\n            try @call(.auto, @field(Element.Build, func_name), args);\n            return true;\n        }\n\n        // inform our caller (the Node) that we didn't find anything that implemented\n        // func_name and it should keep searching for a match.\n        return false;\n    }\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Element\" {\n    try testing.htmlRunner(\"element\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/Event.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\nconst Node = @import(\"Node.zig\");\nconst String = @import(\"../../string.zig\").String;\n\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub const Event = @This();\n\npub const _prototype_root = true;\n_type: Type,\n_arena: Allocator,\n_bubbles: bool = false,\n_cancelable: bool = false,\n_composed: bool = false,\n_type_string: String,\n_target: ?*EventTarget = null,\n_current_target: ?*EventTarget = null,\n_dispatch_target: ?*EventTarget = null, // Original target for composedPath()\n_prevent_default: bool = false,\n_stop_propagation: bool = false,\n_stop_immediate_propagation: bool = false,\n_event_phase: EventPhase = .none,\n_time_stamp: u64,\n_needs_retargeting: bool = false,\n_is_trusted: bool = false,\n\n// There's a period of time between creating an event and handing it off to v8\n// where things can fail. If it does fail, we need to deinit the event. The timing\n// window can be difficult to capture, so we use a reference count.\n// should be 0, 1, or 2. 0\n// - 0: no reference, always a transient state going to either 1 or about to be deinit'd\n// - 1: either zig or v8 have a reference\n// - 2: both zig and v8 have a reference\n_rc: u8 = 0,\n\npub const EventPhase = enum(u8) {\n    none = 0,\n    capturing_phase = 1,\n    at_target = 2,\n    bubbling_phase = 3,\n};\n\npub const Type = union(enum) {\n    generic,\n    error_event: *@import(\"event/ErrorEvent.zig\"),\n    custom_event: *@import(\"event/CustomEvent.zig\"),\n    message_event: *@import(\"event/MessageEvent.zig\"),\n    progress_event: *@import(\"event/ProgressEvent.zig\"),\n    composition_event: *@import(\"event/CompositionEvent.zig\"),\n    navigation_current_entry_change_event: *@import(\"event/NavigationCurrentEntryChangeEvent.zig\"),\n    page_transition_event: *@import(\"event/PageTransitionEvent.zig\"),\n    pop_state_event: *@import(\"event/PopStateEvent.zig\"),\n    ui_event: *@import(\"event/UIEvent.zig\"),\n    promise_rejection_event: *@import(\"event/PromiseRejectionEvent.zig\"),\n};\n\npub const Options = struct {\n    bubbles: bool = false,\n    cancelable: bool = false,\n    composed: bool = false,\n};\n\npub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {\n    const arena = try page.getArena(.{ .debug = \"Event\" });\n    errdefer page.releaseArena(arena);\n    const str = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, str, opts_, false);\n}\n\npub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {\n    const arena = try page.getArena(.{ .debug = \"Event.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, opts_, true);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, comptime trusted: bool) !*Event {\n    const opts = opts_ orelse Options{};\n\n    // Round to 2ms for privacy (browsers do this)\n    const raw_timestamp = @import(\"../../datetime.zig\").milliTimestamp(.monotonic);\n    const time_stamp = (raw_timestamp / 2) * 2;\n\n    const event = try arena.create(Event);\n    event.* = .{\n        ._arena = arena,\n        ._type = .generic,\n        ._bubbles = opts.bubbles,\n        ._time_stamp = time_stamp,\n        ._cancelable = opts.cancelable,\n        ._composed = opts.composed,\n        ._type_string = typ,\n        ._is_trusted = trusted,\n    };\n    return event;\n}\n\npub fn initEvent(\n    self: *Event,\n    event_string: []const u8,\n    bubbles: ?bool,\n    cancelable: ?bool,\n) !void {\n    if (self._event_phase != .none) {\n        return;\n    }\n\n    self._type_string = try String.init(self._arena, event_string, .{});\n    self._bubbles = bubbles orelse false;\n    self._cancelable = cancelable orelse false;\n    self._stop_propagation = false;\n    self._stop_immediate_propagation = false;\n    self._prevent_default = false;\n}\n\npub fn acquireRef(self: *Event) void {\n    self._rc += 1;\n}\n\npub fn deinit(self: *Event, shutdown: bool, session: *Session) void {\n    if (shutdown) {\n        session.releaseArena(self._arena);\n        return;\n    }\n\n    const rc = self._rc;\n    if (comptime IS_DEBUG) {\n        std.debug.assert(rc != 0);\n    }\n\n    if (rc == 1) {\n        session.releaseArena(self._arena);\n    } else {\n        self._rc = rc - 1;\n    }\n}\n\npub fn as(self: *Event, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn is(self: *Event, comptime T: type) ?*T {\n    switch (self._type) {\n        .generic => return if (T == Event) self else null,\n        .error_event => |e| return if (T == @import(\"event/ErrorEvent.zig\")) e else null,\n        .custom_event => |e| return if (T == @import(\"event/CustomEvent.zig\")) e else null,\n        .message_event => |e| return if (T == @import(\"event/MessageEvent.zig\")) e else null,\n        .progress_event => |e| return if (T == @import(\"event/ProgressEvent.zig\")) e else null,\n        .composition_event => |e| return if (T == @import(\"event/CompositionEvent.zig\")) e else null,\n        .navigation_current_entry_change_event => |e| return if (T == @import(\"event/NavigationCurrentEntryChangeEvent.zig\")) e else null,\n        .page_transition_event => |e| return if (T == @import(\"event/PageTransitionEvent.zig\")) e else null,\n        .pop_state_event => |e| return if (T == @import(\"event/PopStateEvent.zig\")) e else null,\n        .promise_rejection_event => |e| return if (T == @import(\"event/PromiseRejectionEvent.zig\")) e else null,\n        .ui_event => |e| {\n            if (T == @import(\"event/UIEvent.zig\")) {\n                return e;\n            }\n            return e.is(T);\n        },\n    }\n    return null;\n}\n\npub fn getType(self: *const Event) []const u8 {\n    return self._type_string.str();\n}\n\npub fn getBubbles(self: *const Event) bool {\n    return self._bubbles;\n}\n\npub fn getCancelable(self: *const Event) bool {\n    return self._cancelable;\n}\n\npub fn getComposed(self: *const Event) bool {\n    return self._composed;\n}\n\npub fn getTarget(self: *const Event) ?*EventTarget {\n    return self._target;\n}\n\npub fn getCurrentTarget(self: *const Event) ?*EventTarget {\n    return self._current_target;\n}\n\npub fn preventDefault(self: *Event) void {\n    if (self._cancelable) {\n        self._prevent_default = true;\n    }\n}\n\npub fn stopPropagation(self: *Event) void {\n    self._stop_propagation = true;\n}\n\npub fn stopImmediatePropagation(self: *Event) void {\n    self._stop_immediate_propagation = true;\n    self._stop_propagation = true;\n}\n\npub fn getDefaultPrevented(self: *const Event) bool {\n    return self._prevent_default;\n}\n\npub fn getReturnValue(self: *const Event) bool {\n    return !self._prevent_default;\n}\n\npub fn setReturnValue(self: *Event, v: bool) void {\n    if (!v) {\n        // Setting returnValue=false is equivalent to preventDefault()\n        if (self._cancelable) {\n            self._prevent_default = true;\n        }\n    }\n}\n\npub fn getCancelBubble(self: *const Event) bool {\n    return self._stop_propagation;\n}\n\npub fn setCancelBubble(self: *Event) void {\n    self.stopPropagation();\n}\n\npub fn getEventPhase(self: *const Event) u8 {\n    return @intFromEnum(self._event_phase);\n}\n\npub fn getTimeStamp(self: *const Event) u64 {\n    return self._time_stamp;\n}\n\npub fn setTrusted(self: *Event) void {\n    self._is_trusted = true;\n}\n\npub fn setUntrusted(self: *Event) void {\n    self._is_trusted = false;\n}\n\npub fn getIsTrusted(self: *const Event) bool {\n    return self._is_trusted;\n}\n\npub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {\n    // Return empty array if event is not being dispatched\n    if (self._event_phase == .none) {\n        return &.{};\n    }\n\n    // Use dispatch_target (original target) if available, otherwise fall back to target\n    // This is important because _target gets retargeted during event dispatch\n    const target = self._dispatch_target orelse self._target orelse return &.{};\n\n    // Only nodes have a propagation path\n    const target_node = switch (target._type) {\n        .node => |n| n,\n        else => return &.{},\n    };\n\n    // Build the path by walking up from target\n    var path_len: usize = 0;\n    var path_buffer: [128]*EventTarget = undefined;\n    var stopped_at_shadow_boundary = false;\n\n    // Track closed shadow boundaries (position in path and host position)\n    var closed_shadow_boundary: ?struct { shadow_end: usize, host_start: usize } = null;\n\n    var node: ?*Node = target_node;\n    while (node) |n| {\n        if (path_len >= path_buffer.len) {\n            break;\n        }\n        path_buffer[path_len] = n.asEventTarget();\n        path_len += 1;\n\n        // Check if this node is a shadow root\n        if (n._type == .document_fragment) {\n            if (n._type.document_fragment._type == .shadow_root) {\n                const shadow = n._type.document_fragment._type.shadow_root;\n\n                // If event is not composed, stop at shadow boundary\n                if (!self._composed) {\n                    stopped_at_shadow_boundary = true;\n                    break;\n                }\n\n                // Track the first closed shadow boundary we encounter\n                if (shadow._mode == .closed and closed_shadow_boundary == null) {\n                    // Mark where the shadow root is in the path\n                    // The next element will be the host\n                    closed_shadow_boundary = .{\n                        .shadow_end = path_len - 1, // index of shadow root\n                        .host_start = path_len, // index where host will be\n                    };\n                }\n\n                // Jump to the shadow host and continue\n                node = shadow._host.asNode();\n                continue;\n            }\n        }\n\n        node = n._parent;\n    }\n\n    // Add window at the end (unless we stopped at shadow boundary)\n    if (!stopped_at_shadow_boundary) {\n        if (path_len < path_buffer.len) {\n            path_buffer[path_len] = page.window.asEventTarget();\n            path_len += 1;\n        }\n    }\n\n    // Determine visible path based on current_target and closed shadow boundaries\n    var visible_start_index: usize = 0;\n\n    if (closed_shadow_boundary) |boundary| {\n        // Check if current_target is outside the closed shadow\n        // If current_target is null or is at/after the host position, hide shadow internals\n        const current_target = self._current_target;\n\n        if (current_target) |ct| {\n            // Find current_target in the path\n            var ct_index: ?usize = null;\n            for (path_buffer[0..path_len], 0..) |elem, i| {\n                if (elem == ct) {\n                    ct_index = i;\n                    break;\n                }\n            }\n\n            // If current_target is at or after the host (outside the closed shadow),\n            // hide everything from target up to the host\n            if (ct_index) |idx| {\n                if (idx >= boundary.host_start) {\n                    visible_start_index = boundary.host_start;\n                }\n            }\n        }\n    }\n\n    // Calculate the visible portion of the path\n    const visible_path_len = if (path_len > visible_start_index) path_len - visible_start_index else 0;\n\n    // Allocate and return the visible path using call_arena (short-lived)\n    const path = try page.call_arena.alloc(*EventTarget, visible_path_len);\n    @memcpy(path, path_buffer[visible_start_index..path_len]);\n    return path;\n}\n\npub fn populateFromOptions(self: *Event, opts: anytype) void {\n    self._bubbles = opts.bubbles;\n    self._cancelable = opts.cancelable;\n    self._composed = opts.composed;\n}\n\npub fn inheritOptions(comptime T: type, comptime additions: anytype) type {\n    var all_fields: []const std.builtin.Type.StructField = &.{};\n\n    if (@hasField(T, \"_proto\")) {\n        const t_fields = @typeInfo(T).@\"struct\".fields;\n\n        inline for (t_fields) |field| {\n            if (std.mem.eql(u8, field.name, \"_proto\")) {\n                const ProtoType = @typeInfo(field.type).pointer.child;\n                if (@hasDecl(ProtoType, \"Options\")) {\n                    const parent_options = @typeInfo(ProtoType.Options);\n                    all_fields = all_fields ++ parent_options.@\"struct\".fields;\n                }\n            }\n        }\n    }\n\n    const additions_info = @typeInfo(additions);\n    all_fields = all_fields ++ additions_info.@\"struct\".fields;\n\n    return @Type(.{\n        .@\"struct\" = .{\n            .layout = .auto,\n            .fields = all_fields,\n            .decls = &.{},\n            .is_tuple = false,\n        },\n    });\n}\n\npub fn populatePrototypes(self: anytype, opts: anytype, trusted: bool) void {\n    const T = @TypeOf(self.*);\n\n    if (@hasField(T, \"_proto\")) {\n        populatePrototypes(self._proto, opts, trusted);\n    }\n\n    if (@hasDecl(T, \"populateFromOptions\")) {\n        T.populateFromOptions(self, opts);\n    }\n\n    // Set isTrusted at the Event level (base of prototype chain)\n    if (T == Event or @hasField(T, \"is_trusted\")) {\n        self._is_trusted = trusted;\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Event);\n\n    pub const Meta = struct {\n        pub const name = \"Event\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(Event.deinit);\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(Event.init, .{});\n    pub const @\"type\" = bridge.accessor(Event.getType, null, .{});\n    pub const bubbles = bridge.accessor(Event.getBubbles, null, .{});\n    pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});\n    pub const composed = bridge.accessor(Event.getComposed, null, .{});\n    pub const target = bridge.accessor(Event.getTarget, null, .{});\n    pub const srcElement = bridge.accessor(Event.getTarget, null, .{});\n    pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});\n    pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});\n    pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});\n    pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{});\n    pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{});\n    pub const preventDefault = bridge.function(Event.preventDefault, .{});\n    pub const stopPropagation = bridge.function(Event.stopPropagation, .{});\n    pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});\n    pub const composedPath = bridge.function(Event.composedPath, .{});\n    pub const initEvent = bridge.function(Event.initEvent, .{});\n    // deprecated\n    pub const returnValue = bridge.accessor(Event.getReturnValue, Event.setReturnValue, .{});\n    // deprecated\n    pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});\n\n    // Event phase constants\n    pub const NONE = bridge.property(@intFromEnum(EventPhase.none), .{ .template = true });\n    pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase), .{ .template = true });\n    pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target), .{ .template = true });\n    pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase), .{ .template = true });\n};\n\n// tested in event_target\n"
  },
  {
    "path": "src/browser/webapi/EventTarget.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst EventManager = @import(\"../EventManager.zig\");\nconst RegisterOptions = EventManager.RegisterOptions;\n\nconst Event = @import(\"Event.zig\");\n\nconst EventTarget = @This();\n\npub const _prototype_root = true;\n_type: Type,\n\npub const Type = union(enum) {\n    generic: void,\n    node: *@import(\"Node.zig\"),\n    window: *@import(\"Window.zig\"),\n    xhr: *@import(\"net/XMLHttpRequestEventTarget.zig\"),\n    abort_signal: *@import(\"AbortSignal.zig\"),\n    media_query_list: *@import(\"css/MediaQueryList.zig\"),\n    message_port: *@import(\"MessagePort.zig\"),\n    text_track_cue: *@import(\"media/TextTrackCue.zig\"),\n    navigation: *@import(\"navigation/Navigation.zig\"),\n    screen: *@import(\"Screen.zig\"),\n    screen_orientation: *@import(\"Screen.zig\").Orientation,\n    visual_viewport: *@import(\"VisualViewport.zig\"),\n    file_reader: *@import(\"FileReader.zig\"),\n    font_face_set: *@import(\"css/FontFaceSet.zig\"),\n};\n\npub fn init(page: *Page) !*EventTarget {\n    return page._factory.create(EventTarget{\n        ._type = .generic,\n    });\n}\n\npub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {\n    if (event._event_phase != .none) {\n        return error.InvalidStateError;\n    }\n    event._is_trusted = false;\n\n    event.acquireRef();\n    defer event.deinit(false, page._session);\n    try page._event_manager.dispatch(self, event);\n    return !event._cancelable or !event._prevent_default;\n}\n\nconst AddEventListenerOptions = union(enum) {\n    capture: bool,\n    options: RegisterOptions,\n};\n\npub const EventListenerCallback = union(enum) {\n    function: js.Function,\n    object: js.Object,\n};\npub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void {\n    const callback = callback_ orelse return;\n\n    const em_callback = switch (callback) {\n        .object => |obj| EventManager.Callback{ .object = obj },\n        .function => |func| EventManager.Callback{ .function = func },\n    };\n\n    const options = blk: {\n        const o = opts_ orelse break :blk RegisterOptions{};\n        break :blk switch (o) {\n            .options => |opts| opts,\n            .capture => |capture| RegisterOptions{ .capture = capture },\n        };\n    };\n    return page._event_manager.register(self, typ, em_callback, options);\n}\n\nconst RemoveEventListenerOptions = union(enum) {\n    capture: bool,\n    options: Options,\n\n    const Options = struct {\n        capture: bool = false,\n    };\n};\npub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, page: *Page) !void {\n    const callback = callback_ orelse return;\n\n    // For object callbacks, check if handleEvent exists\n    if (callback == .object) {\n        if (try callback.object.getFunction(\"handleEvent\") == null) {\n            return;\n        }\n    }\n\n    const em_callback = switch (callback) {\n        .function => |func| EventManager.Callback{ .function = func },\n        .object => |obj| EventManager.Callback{ .object = obj },\n    };\n\n    const use_capture = blk: {\n        const o = opts_ orelse break :blk false;\n        break :blk switch (o) {\n            .capture => |capture| capture,\n            .options => |opts| opts.capture,\n        };\n    };\n    return page._event_manager.remove(self, typ, em_callback, use_capture);\n}\n\npub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {\n    return switch (self._type) {\n        .node => |n| n.format(writer),\n        .generic => writer.writeAll(\"<EventTarget>\"),\n        .window => writer.writeAll(\"<Window>\"),\n        .xhr => writer.writeAll(\"<XMLHttpRequestEventTarget>\"),\n        .abort_signal => writer.writeAll(\"<AbortSignal>\"),\n        .media_query_list => writer.writeAll(\"<MediaQueryList>\"),\n        .message_port => writer.writeAll(\"<MessagePort>\"),\n        .text_track_cue => writer.writeAll(\"<TextTrackCue>\"),\n        .navigation => writer.writeAll(\"<Navigation>\"),\n        .screen => writer.writeAll(\"<Screen>\"),\n        .screen_orientation => writer.writeAll(\"<ScreenOrientation>\"),\n        .visual_viewport => writer.writeAll(\"<VisualViewport>\"),\n        .file_reader => writer.writeAll(\"<FileReader>\"),\n        .font_face_set => writer.writeAll(\"<FontFaceSet>\"),\n    };\n}\n\npub fn toString(self: *EventTarget) []const u8 {\n    return switch (self._type) {\n        .node => return \"[object Node]\",\n        .generic => return \"[object EventTarget]\",\n        .window => return \"[object Window]\",\n        .xhr => return \"[object XMLHttpRequestEventTarget]\",\n        .abort_signal => return \"[object AbortSignal]\",\n        .media_query_list => return \"[object MediaQueryList]\",\n        .message_port => return \"[object MessagePort]\",\n        .text_track_cue => return \"[object TextTrackCue]\",\n        .navigation => return \"[object Navigation]\",\n        .screen => return \"[object Screen]\",\n        .screen_orientation => return \"[object ScreenOrientation]\",\n        .visual_viewport => return \"[object VisualViewport]\",\n        .file_reader => return \"[object FileReader]\",\n        .font_face_set => return \"[object FontFaceSet]\",\n    };\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(EventTarget);\n\n    pub const Meta = struct {\n        pub const name = \"EventTarget\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(EventTarget.init, .{});\n    pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{ .dom_exception = true });\n    pub const addEventListener = bridge.function(EventTarget.addEventListener, .{});\n    pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: EventTarget\" {\n    // we create thousands of these per page. Nothing should bloat it.\n    try testing.expectEqual(16, @sizeOf(EventTarget));\n    try testing.htmlRunner(\"events.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/File.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst Blob = @import(\"Blob.zig\");\n\nconst File = @This();\n\n/// `File` inherits `Blob`.\n_proto: *Blob,\n\n// TODO: Implement File API.\npub fn init(page: *Page) !*File {\n    const arena = try page.getArena(.{ .debug = \"File\" });\n    errdefer page.releaseArena(arena);\n    return page._factory.blob(arena, File{ ._proto = undefined });\n}\n\npub fn deinit(self: *File, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(File);\n\n    pub const Meta = struct {\n        pub const name = \"File\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(File.deinit);\n    };\n\n    pub const constructor = bridge.constructor(File.init, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: File\" {\n    try testing.htmlRunner(\"file.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/FileList.zig",
    "content": "const js = @import(\"../js/js.zig\");\n\nconst FileList = @This();\n\n/// Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n_pad: bool = false,\n\npub fn getLength(_: *const FileList) u32 {\n    return 0;\n}\n\npub fn item(_: *const FileList, _: u32) ?*@import(\"File.zig\") {\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FileList);\n\n    pub const Meta = struct {\n        pub const name = \"FileList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const length = bridge.accessor(FileList.getLength, null, .{});\n    pub const item = bridge.function(FileList.item, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/FileReader.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\nconst ProgressEvent = @import(\"event/ProgressEvent.zig\");\nconst Blob = @import(\"Blob.zig\");\n\nconst Allocator = std.mem.Allocator;\n\n/// https://w3c.github.io/FileAPI/#dfn-filereader\n/// https://developer.mozilla.org/en-US/docs/Web/API/FileReader\nconst FileReader = @This();\n\n_page: *Page,\n_proto: *EventTarget,\n_arena: Allocator,\n\n_ready_state: ReadyState = .empty,\n_result: ?Result = null,\n_error: ?[]const u8 = null,\n\n_on_abort: ?js.Function.Temp = null,\n_on_error: ?js.Function.Temp = null,\n_on_load: ?js.Function.Temp = null,\n_on_load_end: ?js.Function.Temp = null,\n_on_load_start: ?js.Function.Temp = null,\n_on_progress: ?js.Function.Temp = null,\n\n_aborted: bool = false,\n\nconst ReadyState = enum(u8) {\n    empty = 0,\n    loading = 1,\n    done = 2,\n};\n\nconst Result = union(enum) {\n    string: []const u8,\n    arraybuffer: js.ArrayBuffer,\n};\n\npub fn init(page: *Page) !*FileReader {\n    const arena = try page.getArena(.{ .debug = \"FileReader\" });\n    errdefer page.releaseArena(arena);\n\n    return page._factory.eventTargetWithAllocator(arena, FileReader{\n        ._page = page,\n        ._arena = arena,\n        ._proto = undefined,\n    });\n}\n\npub fn deinit(self: *FileReader, _: bool, session: *Session) void {\n    if (self._on_abort) |func| func.release();\n    if (self._on_error) |func| func.release();\n    if (self._on_load) |func| func.release();\n    if (self._on_load_end) |func| func.release();\n    if (self._on_load_start) |func| func.release();\n    if (self._on_progress) |func| func.release();\n\n    session.releaseArena(self._arena);\n}\n\nfn asEventTarget(self: *FileReader) *EventTarget {\n    return self._proto;\n}\n\npub fn getOnAbort(self: *const FileReader) ?js.Function.Temp {\n    return self._on_abort;\n}\n\npub fn setOnAbort(self: *FileReader, cb: ?js.Function.Temp) !void {\n    self._on_abort = cb;\n}\n\npub fn getOnError(self: *const FileReader) ?js.Function.Temp {\n    return self._on_error;\n}\n\npub fn setOnError(self: *FileReader, cb: ?js.Function.Temp) !void {\n    self._on_error = cb;\n}\n\npub fn getOnLoad(self: *const FileReader) ?js.Function.Temp {\n    return self._on_load;\n}\n\npub fn setOnLoad(self: *FileReader, cb: ?js.Function.Temp) !void {\n    self._on_load = cb;\n}\n\npub fn getOnLoadEnd(self: *const FileReader) ?js.Function.Temp {\n    return self._on_load_end;\n}\n\npub fn setOnLoadEnd(self: *FileReader, cb: ?js.Function.Temp) !void {\n    self._on_load_end = cb;\n}\n\npub fn getOnLoadStart(self: *const FileReader) ?js.Function.Temp {\n    return self._on_load_start;\n}\n\npub fn setOnLoadStart(self: *FileReader, cb: ?js.Function.Temp) !void {\n    self._on_load_start = cb;\n}\n\npub fn getOnProgress(self: *const FileReader) ?js.Function.Temp {\n    return self._on_progress;\n}\n\npub fn setOnProgress(self: *FileReader, cb: ?js.Function.Temp) !void {\n    self._on_progress = cb;\n}\n\npub fn getReadyState(self: *const FileReader) u8 {\n    return @intFromEnum(self._ready_state);\n}\n\npub fn getResult(self: *const FileReader) ?Result {\n    return self._result;\n}\n\npub fn getError(self: *const FileReader) ?[]const u8 {\n    return self._error;\n}\n\npub fn readAsArrayBuffer(self: *FileReader, blob: *Blob) !void {\n    try self.readInternal(blob, .arraybuffer);\n}\n\npub fn readAsBinaryString(self: *FileReader, blob: *Blob) !void {\n    try self.readInternal(blob, .binary_string);\n}\n\npub fn readAsText(self: *FileReader, blob: *Blob, encoding_: ?[]const u8) !void {\n    _ = encoding_; // TODO: Handle encoding properly\n    try self.readInternal(blob, .text);\n}\n\npub fn readAsDataURL(self: *FileReader, blob: *Blob) !void {\n    try self.readInternal(blob, .data_url);\n}\n\nconst ReadType = enum {\n    arraybuffer,\n    binary_string,\n    text,\n    data_url,\n};\n\nfn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {\n    if (self._ready_state == .loading) {\n        return error.InvalidStateError;\n    }\n\n    // Reset state\n    self._ready_state = .loading;\n    self._result = null;\n    self._error = null;\n    self._aborted = false;\n\n    const page = self._page;\n\n    try self.dispatch(.load_start, .{ .loaded = 0, .total = blob.getSize() }, page);\n    if (self._aborted) {\n        return;\n    }\n\n    // Perform the read (synchronous since data is in memory)\n    const data = blob._slice;\n    const size = data.len;\n    try self.dispatch(.progress, .{ .loaded = size, .total = size }, page);\n    if (self._aborted) {\n        return;\n    }\n\n    // Process the data based on read type\n    self._result = switch (read_type) {\n        .arraybuffer => .{ .arraybuffer = .{ .values = data } },\n        .binary_string => .{ .string = data },\n        .text => .{ .string = data },\n        .data_url => blk: {\n            // Create data URL with base64 encoding\n            const mime = if (blob._mime.len > 0) blob._mime else \"application/octet-stream\";\n            const data_url = try encodeDataURL(self._arena, mime, data);\n            break :blk .{ .string = data_url };\n        },\n    };\n\n    self._ready_state = .done;\n\n    try self.dispatch(.load, .{ .loaded = size, .total = size }, page);\n    try self.dispatch(.load_end, .{ .loaded = size, .total = size }, page);\n}\n\npub fn abort(self: *FileReader) !void {\n    if (self._ready_state != .loading) {\n        return;\n    }\n\n    self._aborted = true;\n    self._ready_state = .done;\n    self._result = null;\n\n    const page = self._page;\n\n    try self.dispatch(.abort, null, page);\n\n    try self.dispatch(.load_end, null, page);\n}\n\nfn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Progress, page: *Page) !void {\n    const field, const typ = comptime blk: {\n        break :blk switch (event_type) {\n            .abort => .{ \"_on_abort\", \"abort\" },\n            .err => .{ \"_on_error\", \"error\" },\n            .load => .{ \"_on_load\", \"load\" },\n            .load_end => .{ \"_on_load_end\", \"loadend\" },\n            .load_start => .{ \"_on_load_start\", \"loadstart\" },\n            .progress => .{ \"_on_progress\", \"progress\" },\n        };\n    };\n\n    const progress = progress_ orelse Progress{};\n    const event = (try ProgressEvent.initTrusted(\n        comptime .wrap(typ),\n        .{ .total = progress.total, .loaded = progress.loaded },\n        page,\n    )).asEvent();\n\n    return page._event_manager.dispatchDirect(\n        self.asEventTarget(),\n        event,\n        @field(self, field),\n        .{ .context = \"FileReader \" ++ typ },\n    );\n}\n\nconst DispatchType = enum {\n    abort,\n    err,\n    load,\n    load_end,\n    load_start,\n    progress,\n};\n\nconst Progress = struct {\n    loaded: usize = 0,\n    total: usize = 0,\n};\n\n/// Encodes binary data as a data URL with base64 encoding.\n/// Format: data:[<mediatype>][;base64],<data>\nfn encodeDataURL(arena: Allocator, mime: []const u8, data: []const u8) ![]const u8 {\n    const base64 = std.base64.standard.Encoder;\n\n    // Calculate size needed for base64 encoding\n    const encoded_size = base64.calcSize(data.len);\n\n    // Allocate buffer for the full data URL\n    // Format: \"data:\" + mime + \";base64,\" + encoded_data\n    const prefix = \"data:\";\n    const suffix = \";base64,\";\n    const total_size = prefix.len + mime.len + suffix.len + encoded_size;\n\n    var pos: usize = 0;\n    const buf = try arena.alloc(u8, total_size);\n\n    @memcpy(buf[pos..][0..prefix.len], prefix);\n    pos += prefix.len;\n\n    @memcpy(buf[pos..][0..mime.len], mime);\n    pos += mime.len;\n\n    @memcpy(buf[pos..][0..suffix.len], suffix);\n    pos += suffix.len;\n\n    _ = base64.encode(buf[pos..], data);\n\n    return buf;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FileReader);\n\n    pub const Meta = struct {\n        pub const name = \"FileReader\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(FileReader.deinit);\n    };\n\n    pub const constructor = bridge.constructor(FileReader.init, .{});\n\n    // State constants\n    pub const EMPTY = bridge.property(@intFromEnum(FileReader.ReadyState.empty), .{ .template = true });\n    pub const LOADING = bridge.property(@intFromEnum(FileReader.ReadyState.loading), .{ .template = true });\n    pub const DONE = bridge.property(@intFromEnum(FileReader.ReadyState.done), .{ .template = true });\n\n    // Properties\n    pub const readyState = bridge.accessor(FileReader.getReadyState, null, .{});\n    pub const result = bridge.accessor(FileReader.getResult, null, .{});\n    pub const @\"error\" = bridge.accessor(FileReader.getError, null, .{});\n\n    // Event handlers\n    pub const onabort = bridge.accessor(FileReader.getOnAbort, FileReader.setOnAbort, .{});\n    pub const onerror = bridge.accessor(FileReader.getOnError, FileReader.setOnError, .{});\n    pub const onload = bridge.accessor(FileReader.getOnLoad, FileReader.setOnLoad, .{});\n    pub const onloadend = bridge.accessor(FileReader.getOnLoadEnd, FileReader.setOnLoadEnd, .{});\n    pub const onloadstart = bridge.accessor(FileReader.getOnLoadStart, FileReader.setOnLoadStart, .{});\n    pub const onprogress = bridge.accessor(FileReader.getOnProgress, FileReader.setOnProgress, .{});\n\n    // Methods\n    pub const readAsArrayBuffer = bridge.function(FileReader.readAsArrayBuffer, .{ .dom_exception = true });\n    pub const readAsBinaryString = bridge.function(FileReader.readAsBinaryString, .{ .dom_exception = true });\n    pub const readAsText = bridge.function(FileReader.readAsText, .{ .dom_exception = true });\n    pub const readAsDataURL = bridge.function(FileReader.readAsDataURL, .{ .dom_exception = true });\n    pub const abort = bridge.function(FileReader.abort, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: FileReader\" {\n    try testing.htmlRunner(\"file_reader.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/HTMLDocument.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst String = @import(\"../../string.zig\").String;\n\nconst Page = @import(\"../Page.zig\");\nconst Node = @import(\"Node.zig\");\nconst Document = @import(\"Document.zig\");\nconst Element = @import(\"Element.zig\");\nconst DocumentType = @import(\"DocumentType.zig\");\nconst collections = @import(\"collections.zig\");\n\nconst HTMLDocument = @This();\n\n_proto: *Document,\n_document_type: ?*DocumentType = null,\n\npub fn asDocument(self: *HTMLDocument) *Document {\n    return self._proto;\n}\n\npub fn asNode(self: *HTMLDocument) *Node {\n    return self._proto.asNode();\n}\n\npub fn asEventTarget(self: *HTMLDocument) *@import(\"EventTarget.zig\") {\n    return self._proto.asEventTarget();\n}\n\n// HTML-specific accessors\npub fn getHead(self: *HTMLDocument) ?*Element.Html.Head {\n    const doc_el = self._proto.getDocumentElement() orelse return null;\n    var child = doc_el.asNode().firstChild();\n    while (child) |node| {\n        if (node.is(Element.Html.Head)) |head| {\n            return head;\n        }\n        child = node.nextSibling();\n    }\n    return null;\n}\n\npub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {\n    const doc_el = self._proto.getDocumentElement() orelse return null;\n    var child = doc_el.asNode().firstChild();\n    while (child) |node| {\n        if (node.is(Element.Html.Body)) |body| {\n            return body;\n        }\n        child = node.nextSibling();\n    }\n    return null;\n}\n\npub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {\n    // Search the entire document for the first <title> element\n    const root = self._proto.getDocumentElement() orelse return \"\";\n    const title_element = blk: {\n        var walker = @import(\"TreeWalker.zig\").Full.init(root.asNode(), .{});\n        while (walker.next()) |node| {\n            if (node.is(Element.Html.Title)) |title| {\n                break :blk title;\n            }\n        }\n        return \"\";\n    };\n\n    var buf = std.Io.Writer.Allocating.init(page.call_arena);\n    try title_element.asNode().getTextContent(&buf.writer);\n    const text = buf.written();\n\n    if (text.len == 0) {\n        return \"\";\n    }\n\n    var started = false;\n    var in_whitespace = false;\n    var result: std.ArrayList(u8) = .empty;\n    try result.ensureTotalCapacity(page.call_arena, text.len);\n\n    for (text) |c| {\n        const is_ascii_ws = c == ' ' or c == '\\t' or c == '\\n' or c == '\\r' or c == '\\x0C';\n\n        if (is_ascii_ws) {\n            if (started) {\n                in_whitespace = true;\n            }\n        } else {\n            if (in_whitespace) {\n                result.appendAssumeCapacity(' ');\n                in_whitespace = false;\n            }\n            result.appendAssumeCapacity(c);\n            started = true;\n        }\n    }\n\n    return result.items;\n}\n\npub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {\n    const head = self.getHead() orelse return;\n\n    // Find existing title element in head\n    var it = head.asNode().childrenIterator();\n    while (it.next()) |node| {\n        if (node.is(Element.Html.Title)) |title_element| {\n            // Replace children, but don't create text node for empty string\n            if (title.len == 0) {\n                return title_element.asElement().replaceChildren(&.{}, page);\n            } else {\n                return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);\n            }\n        }\n    }\n\n    // No title element found, create one\n    const title_node = try page.createElementNS(.html, \"title\", null);\n    const title_element = title_node.as(Element);\n\n    // Only add text if non-empty\n    if (title.len > 0) {\n        try title_element.replaceChildren(&.{.{ .text = title }}, page);\n    }\n\n    _ = try head.asNode().appendChild(title_node, page);\n}\n\npub fn getImages(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {\n    return collections.NodeLive(.tag).init(self.asNode(), .img, page);\n}\n\npub fn getScripts(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {\n    return collections.NodeLive(.tag).init(self.asNode(), .script, page);\n}\n\npub fn getLinks(self: *HTMLDocument, page: *Page) !collections.NodeLive(.links) {\n    return collections.NodeLive(.links).init(self.asNode(), {}, page);\n}\n\npub fn getAnchors(self: *HTMLDocument, page: *Page) !collections.NodeLive(.anchors) {\n    return collections.NodeLive(.anchors).init(self.asNode(), {}, page);\n}\n\npub fn getForms(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {\n    return collections.NodeLive(.tag).init(self.asNode(), .form, page);\n}\n\npub fn getEmbeds(self: *HTMLDocument, page: *Page) !collections.NodeLive(.tag) {\n    return collections.NodeLive(.tag).init(self.asNode(), .embed, page);\n}\n\npub fn getApplets(_: *const HTMLDocument) collections.HTMLCollection {\n    return .{ ._data = .empty };\n}\n\npub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script {\n    return self._proto._current_script;\n}\n\npub fn getLocation(self: *const HTMLDocument) ?*@import(\"Location.zig\") {\n    return self._proto._location;\n}\n\npub fn setLocation(self: *HTMLDocument, url: [:0]const u8, page: *Page) !void {\n    return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._page });\n}\n\npub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection {\n    return page._factory.create(collections.HTMLAllCollection.init(self.asNode(), page));\n}\n\npub fn getCookie(_: *HTMLDocument, page: *Page) ![]const u8 {\n    var buf: std.ArrayList(u8) = .empty;\n    try page._session.cookie_jar.forRequest(page.url, buf.writer(page.call_arena), .{\n        .is_http = false,\n        .is_navigation = true,\n    });\n    return buf.items;\n}\n\npub fn setCookie(_: *HTMLDocument, cookie_str: []const u8, page: *Page) ![]const u8 {\n    // we use the cookie jar's allocator to parse the cookie because it\n    // outlives the page's arena.\n    const Cookie = @import(\"storage/Cookie.zig\");\n    const c = Cookie.parse(page._session.cookie_jar.allocator, page.url, cookie_str) catch {\n        // Invalid cookies should be silently ignored, not throw errors\n        return \"\";\n    };\n    errdefer c.deinit();\n    if (c.http_only) {\n        c.deinit();\n        return \"\"; // HttpOnly cookies cannot be set from JS\n    }\n    try page._session.cookie_jar.add(c, std.time.timestamp());\n    return cookie_str;\n}\n\npub fn getDocType(self: *HTMLDocument, page: *Page) !*DocumentType {\n    if (self._document_type) |dt| {\n        return dt;\n    }\n\n    var tw = @import(\"TreeWalker.zig\").Full.init(self.asNode(), .{});\n    while (tw.next()) |node| {\n        if (node._type == .document_type) {\n            self._document_type = node.as(DocumentType);\n            return self._document_type.?;\n        }\n    }\n\n    self._document_type = try page._factory.node(DocumentType{\n        ._proto = undefined,\n        ._name = \"html\",\n        ._public_id = \"\",\n        ._system_id = \"\",\n    });\n    return self._document_type.?;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HTMLDocument);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDocument\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(_constructor, .{});\n    fn _constructor(page: *Page) !*HTMLDocument {\n        return page._factory.document(HTMLDocument{\n            ._proto = undefined,\n        });\n    }\n\n    pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});\n    pub const body = bridge.accessor(HTMLDocument.getBody, null, .{});\n    pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{});\n    pub const images = bridge.accessor(HTMLDocument.getImages, null, .{});\n    pub const scripts = bridge.accessor(HTMLDocument.getScripts, null, .{});\n    pub const links = bridge.accessor(HTMLDocument.getLinks, null, .{});\n    pub const anchors = bridge.accessor(HTMLDocument.getAnchors, null, .{});\n    pub const forms = bridge.accessor(HTMLDocument.getForms, null, .{});\n    pub const embeds = bridge.accessor(HTMLDocument.getEmbeds, null, .{});\n    pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{});\n    pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{});\n    pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});\n    pub const location = bridge.accessor(HTMLDocument.getLocation, HTMLDocument.setLocation, .{});\n    pub const all = bridge.accessor(HTMLDocument.getAll, null, .{});\n    pub const cookie = bridge.accessor(HTMLDocument.getCookie, HTMLDocument.setCookie, .{});\n    pub const doctype = bridge.accessor(HTMLDocument.getDocType, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/History.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst PopStateEvent = @import(\"event/PopStateEvent.zig\");\n\nconst History = @This();\n\nconst ScrollRestoration = enum { auto, manual };\n\n_scroll_restoration: ScrollRestoration = .auto,\n\npub fn getLength(_: *const History, page: *Page) u32 {\n    return @intCast(page._session.navigation._entries.items.len);\n}\n\npub fn getState(_: *const History, page: *Page) !?js.Value {\n    if (page._session.navigation.getCurrentEntry()._state.value) |state| {\n        const value = try page.js.local.?.parseJSON(state);\n        return value;\n    } else return null;\n}\n\npub fn getScrollRestoration(self: *History) []const u8 {\n    return @tagName(self._scroll_restoration);\n}\n\npub fn setScrollRestoration(self: *History, str: []const u8) void {\n    if (std.meta.stringToEnum(ScrollRestoration, str)) |sr| {\n        self._scroll_restoration = sr;\n    }\n}\n\npub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {\n    const arena = page._session.arena;\n    const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);\n\n    const json = state.toJson(arena) catch return error.DataClone;\n    _ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);\n}\n\npub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {\n    const arena = page._session.arena;\n    const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);\n\n    const json = state.toJson(arena) catch return error.DataClone;\n    _ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);\n}\n\nfn goInner(delta: i32, page: *Page) !void {\n    // 0 behaves the same as no argument, both reloading the page.\n\n    const current = page._session.navigation._index;\n    const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));\n    if (index_s < 0 or index_s > page._session.navigation._entries.items.len - 1) {\n        return;\n    }\n\n    const index = @as(usize, @intCast(index_s));\n    const entry = page._session.navigation._entries.items[index];\n\n    if (entry._url) |url| {\n        if (try page.isSameOrigin(url)) {\n            const target = page.window.asEventTarget();\n            if (page._event_manager.hasDirectListeners(target, \"popstate\", page.window._on_popstate)) {\n                const event = (try PopStateEvent.initTrusted(comptime .wrap(\"popstate\"), .{ .state = entry._state.value }, page)).asEvent();\n                try page._event_manager.dispatchDirect(target, event, page.window._on_popstate, .{ .context = \"Pop State\" });\n            }\n        }\n    }\n\n    _ = try page._session.navigation.navigateInner(entry._url, .{ .traverse = index }, page);\n}\n\npub fn back(_: *History, page: *Page) !void {\n    try goInner(-1, page);\n}\n\npub fn forward(_: *History, page: *Page) !void {\n    try goInner(1, page);\n}\n\npub fn go(_: *History, delta: ?i32, page: *Page) !void {\n    try goInner(delta orelse 0, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(History);\n\n    pub const Meta = struct {\n        pub const name = \"History\";\n        pub var class_id: bridge.ClassId = 0;\n        pub const prototype_chain = bridge.prototypeChain();\n    };\n\n    pub const length = bridge.accessor(History.getLength, null, .{});\n    pub const scrollRestoration = bridge.accessor(History.getScrollRestoration, History.setScrollRestoration, .{});\n    pub const state = bridge.accessor(History.getState, null, .{});\n    pub const pushState = bridge.function(History.pushState, .{});\n    pub const replaceState = bridge.function(History.replaceState, .{});\n    pub const back = bridge.function(History.back, .{});\n    pub const forward = bridge.function(History.forward, .{});\n    pub const go = bridge.function(History.go, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: History\" {\n    try testing.htmlRunner(\"history.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/IdleDeadline.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst IdleDeadline = @This();\n\n// Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n_pad: bool = false,\n\npub fn init() IdleDeadline {\n    return .{};\n}\n\npub fn timeRemaining(_: *const IdleDeadline) f64 {\n    // Return a fixed 50ms.\n    // This allows idle callbacks to perform work without complex\n    // timing infrastructure.\n    return 50.0;\n}\n\npub const JsApi = struct {\n    const js = @import(\"../js/js.zig\");\n    pub const bridge = js.Bridge(IdleDeadline);\n\n    pub const Meta = struct {\n        pub const name = \"IdleDeadline\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const timeRemaining = bridge.function(IdleDeadline.timeRemaining, .{});\n    pub const didTimeout = bridge.property(false, .{ .template = false });\n};\n"
  },
  {
    "path": "src/browser/webapi/ImageData.zig",
    "content": "// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst String = @import(\"../../string.zig\").String;\nconst log = @import(\"../../log.zig\");\n\nconst js = @import(\"../js/js.zig\");\nconst color = @import(\"../color.zig\");\nconst Page = @import(\"../Page.zig\");\n\n/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData\nconst ImageData = @This();\n_width: u32,\n_height: u32,\n_data: js.ArrayBufferRef(.uint8_clamped).Global,\n\npub const ConstructorSettings = struct {\n    /// Specifies the color space of the image data.\n    /// Can be set to \"srgb\" for the sRGB color space or \"display-p3\" for the display-p3 color space.\n    colorSpace: String = .wrap(\"srgb\"),\n    /// Specifies the pixel format.\n    /// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createImageData#pixelformat\n    pixelFormat: String = .wrap(\"rgba-unorm8\"),\n};\n\n/// This has many constructors:\n///\n/// ```js\n/// new ImageData(width, height)\n/// new ImageData(width, height, settings)\n///\n/// new ImageData(dataArray, width)\n/// new ImageData(dataArray, width, height)\n/// new ImageData(dataArray, width, height, settings)\n/// ```\n///\n/// We currently support only the first 2.\npub fn init(\n    width: u32,\n    height: u32,\n    maybe_settings: ?ConstructorSettings,\n    page: *Page,\n) !*ImageData {\n    // Though arguments are unsigned long, these are capped to max. i32 on Chrome.\n    // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/html/canvas/image_data.cc#L61\n    const max_i32 = std.math.maxInt(i32);\n    if (width == 0 or width > max_i32 or height == 0 or height > max_i32) {\n        return error.IndexSizeError;\n    }\n\n    const settings: ConstructorSettings = maybe_settings orelse .{};\n    if (settings.colorSpace.eql(comptime .wrap(\"srgb\")) == false) {\n        return error.TypeError;\n    }\n    if (settings.pixelFormat.eql(comptime .wrap(\"rgba-unorm8\")) == false) {\n        return error.TypeError;\n    }\n\n    var size, var overflown = @mulWithOverflow(width, height);\n    if (overflown == 1) return error.IndexSizeError;\n    size, overflown = @mulWithOverflow(size, 4);\n    if (overflown == 1) return error.IndexSizeError;\n\n    return page._factory.create(ImageData{\n        ._width = width,\n        ._height = height,\n        ._data = try page.js.local.?.createTypedArray(.uint8_clamped, size).persist(),\n    });\n}\n\npub fn getWidth(self: *const ImageData) u32 {\n    return self._width;\n}\n\npub fn getHeight(self: *const ImageData) u32 {\n    return self._height;\n}\n\npub fn getData(self: *const ImageData) js.ArrayBufferRef(.uint8_clamped).Global {\n    return self._data;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ImageData);\n\n    pub const Meta = struct {\n        pub const name = \"ImageData\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(ImageData.init, .{ .dom_exception = true });\n\n    pub const colorSpace = bridge.property(\"srgb\", .{ .template = false, .readonly = true });\n    pub const pixelFormat = bridge.property(\"rgba-unorm8\", .{ .template = false, .readonly = true });\n\n    pub const data = bridge.accessor(ImageData.getData, null, .{});\n    pub const width = bridge.accessor(ImageData.getWidth, null, .{});\n    pub const height = bridge.accessor(ImageData.getHeight, null, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: ImageData\" {\n    try testing.htmlRunner(\"image_data.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/IntersectionObserver.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Allocator = std.mem.Allocator;\n\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\nconst Element = @import(\"Element.zig\");\nconst DOMRect = @import(\"DOMRect.zig\");\n\npub fn registerTypes() []const type {\n    return &.{\n        IntersectionObserver,\n        IntersectionObserverEntry,\n    };\n}\n\nconst IntersectionObserver = @This();\n\n_arena: Allocator,\n_callback: js.Function.Temp,\n_observing: std.ArrayList(*Element) = .{},\n_root: ?*Element = null,\n_root_margin: []const u8 = \"0px\",\n_threshold: []const f64 = &.{0.0},\n_pending_entries: std.ArrayList(*IntersectionObserverEntry) = .{},\n_previous_states: std.AutoHashMapUnmanaged(*Element, bool) = .{},\n\n// Shared zero DOMRect to avoid repeated allocations for non-intersecting elements\nvar zero_rect: DOMRect = .{\n    ._x = 0.0,\n    ._y = 0.0,\n    ._width = 0.0,\n    ._height = 0.0,\n};\n\npub const ObserverInit = struct {\n    root: ?*Element = null,\n    rootMargin: ?[]const u8 = null,\n    threshold: Threshold = .{ .scalar = 0.0 },\n\n    const Threshold = union(enum) {\n        scalar: f64,\n        array: []const f64,\n    };\n};\n\npub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*IntersectionObserver {\n    const arena = try page.getArena(.{ .debug = \"IntersectionObserver\" });\n    errdefer page.releaseArena(arena);\n\n    const opts = options orelse ObserverInit{};\n    const root_margin = if (opts.rootMargin) |rm| try arena.dupe(u8, rm) else \"0px\";\n\n    const threshold = switch (opts.threshold) {\n        .scalar => |s| blk: {\n            const arr = try arena.alloc(f64, 1);\n            arr[0] = s;\n            break :blk arr;\n        },\n        .array => |arr| try arena.dupe(f64, arr),\n    };\n\n    const self = try arena.create(IntersectionObserver);\n    self.* = .{\n        ._arena = arena,\n        ._callback = callback,\n        ._root = opts.root,\n        ._root_margin = root_margin,\n        ._threshold = threshold,\n    };\n    return self;\n}\n\npub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {\n    if (shutdown) {\n        self._callback.release();\n        session.releaseArena(self._arena);\n    } else if (comptime IS_DEBUG) {\n        std.debug.assert(false);\n    }\n}\n\npub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {\n    // Check if already observing this target\n    for (self._observing.items) |elem| {\n        if (elem == target) {\n            return;\n        }\n    }\n\n    // Register with page if this is our first observation\n    if (self._observing.items.len == 0) {\n        try page.registerIntersectionObserver(self);\n    }\n\n    try self._observing.append(self._arena, target);\n\n    // Don't initialize previous state yet - let checkIntersection do it\n    // This ensures we get an entry on first observation\n\n    // Check intersection for this new target and schedule delivery\n    try self.checkIntersection(target, page);\n    if (self._pending_entries.items.len > 0) {\n        try page.scheduleIntersectionDelivery();\n    }\n}\n\npub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {\n    for (self._observing.items, 0..) |elem, i| {\n        if (elem == target) {\n            _ = self._observing.swapRemove(i);\n            _ = self._previous_states.remove(target);\n\n            // Remove any pending entries for this target\n            var j: usize = 0;\n            while (j < self._pending_entries.items.len) {\n                if (self._pending_entries.items[j]._target == target) {\n                    const entry = self._pending_entries.swapRemove(j);\n                    entry.deinit(false, page._session);\n                } else {\n                    j += 1;\n                }\n            }\n            break;\n        }\n    }\n}\n\npub fn disconnect(self: *IntersectionObserver, page: *Page) void {\n    self._previous_states.clearRetainingCapacity();\n\n    for (self._pending_entries.items) |entry| {\n        entry.deinit(false, page._session);\n    }\n    self._pending_entries.clearRetainingCapacity();\n\n    self._observing.clearRetainingCapacity();\n    page.unregisterIntersectionObserver(self);\n}\n\npub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {\n    const entries = try page.call_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);\n    self._pending_entries.clearRetainingCapacity();\n    return entries;\n}\n\nfn calculateIntersection(\n    self: *IntersectionObserver,\n    target: *Element,\n    page: *Page,\n) !IntersectionData {\n    const target_rect = target.getBoundingClientRect(page);\n\n    // Use root element's rect or viewport (simplified: assume 1920x1080)\n    const root_rect = if (self._root) |root|\n        root.getBoundingClientRect(page)\n    else\n        // Simplified viewport - assume 1920x1080 for now\n        DOMRect{\n            ._x = 0.0,\n            ._y = 0.0,\n            ._width = 1920.0,\n            ._height = 1080.0,\n        };\n\n    // For a headless browser without real layout, we treat all elements as fully visible.\n    // This avoids fingerprinting issues (massive viewports) and matches the behavior\n    // scripts expect when querying element visibility.\n    // However, elements without a parent cannot intersect (they have no containing block).\n    const has_parent = target.asNode().parentNode() != null;\n    const is_intersecting = has_parent;\n    const intersection_ratio: f64 = if (has_parent) 1.0 else 0.0;\n\n    // Intersection rect is the same as the target rect if visible, otherwise zero rect\n    const intersection_rect = if (has_parent) target_rect else zero_rect;\n\n    return .{\n        .is_intersecting = is_intersecting,\n        .intersection_ratio = intersection_ratio,\n        .intersection_rect = intersection_rect,\n        .bounding_client_rect = target_rect,\n        .root_bounds = root_rect,\n    };\n}\n\nconst IntersectionData = struct {\n    is_intersecting: bool,\n    intersection_ratio: f64,\n    intersection_rect: DOMRect,\n    bounding_client_rect: DOMRect,\n    root_bounds: DOMRect,\n};\n\nfn meetsThreshold(self: *IntersectionObserver, ratio: f64) bool {\n    for (self._threshold) |threshold| {\n        if (ratio >= threshold) {\n            return true;\n        }\n    }\n    return false;\n}\n\nfn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page) !void {\n    const data = try self.calculateIntersection(target, page);\n    const was_intersecting_opt = self._previous_states.get(target);\n    const is_now_intersecting = data.is_intersecting and self.meetsThreshold(data.intersection_ratio);\n\n    // Create entry if:\n    // 1. First time observing this target AND it's intersecting\n    // 2. State changed\n    const should_report = (was_intersecting_opt == null and is_now_intersecting) or\n        (was_intersecting_opt != null and was_intersecting_opt.? != is_now_intersecting);\n\n    if (should_report) {\n        const arena = try page.getArena(.{ .debug = \"IntersectionObserverEntry\" });\n        errdefer page.releaseArena(arena);\n\n        const entry = try arena.create(IntersectionObserverEntry);\n        entry.* = .{\n            ._arena = arena,\n            ._target = target,\n            ._time = page.window._performance.now(),\n            ._is_intersecting = is_now_intersecting,\n            ._root_bounds = try page._factory.create(data.root_bounds),\n            ._intersection_rect = try page._factory.create(data.intersection_rect),\n            ._bounding_client_rect = try page._factory.create(data.bounding_client_rect),\n            ._intersection_ratio = data.intersection_ratio,\n        };\n\n        try self._pending_entries.append(self._arena, entry);\n    }\n\n    // Always update the previous state, even if we didn't report\n    // This ensures we can detect state changes on subsequent checks\n    try self._previous_states.put(self._arena, target, is_now_intersecting);\n}\n\npub fn checkIntersections(self: *IntersectionObserver, page: *Page) !void {\n    if (self._observing.items.len == 0) {\n        return;\n    }\n\n    for (self._observing.items) |target| {\n        try self.checkIntersection(target, page);\n    }\n\n    if (self._pending_entries.items.len > 0) {\n        try page.scheduleIntersectionDelivery();\n    }\n}\n\npub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {\n    if (self._pending_entries.items.len == 0) {\n        return;\n    }\n\n    const entries = try self.takeRecords(page);\n    var caught: js.TryCatch.Caught = undefined;\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    ls.toLocal(self._callback).tryCall(void, .{ entries, self }, &caught) catch |err| {\n        log.err(.page, \"IntsctObserver.deliverEntries\", .{ .err = err, .caught = caught });\n        return err;\n    };\n}\n\npub const IntersectionObserverEntry = struct {\n    _arena: Allocator,\n    _time: f64,\n    _target: *Element,\n    _bounding_client_rect: *DOMRect,\n    _intersection_rect: *DOMRect,\n    _root_bounds: *DOMRect,\n    _intersection_ratio: f64,\n    _is_intersecting: bool,\n\n    pub fn deinit(self: *IntersectionObserverEntry, _: bool, session: *Session) void {\n        session.releaseArena(self._arena);\n    }\n\n    pub fn getTarget(self: *const IntersectionObserverEntry) *Element {\n        return self._target;\n    }\n\n    pub fn getTime(self: *const IntersectionObserverEntry) f64 {\n        return self._time;\n    }\n\n    pub fn getBoundingClientRect(self: *const IntersectionObserverEntry) *DOMRect {\n        return self._bounding_client_rect;\n    }\n\n    pub fn getIntersectionRect(self: *const IntersectionObserverEntry) *DOMRect {\n        return self._intersection_rect;\n    }\n\n    pub fn getRootBounds(self: *const IntersectionObserverEntry) ?*DOMRect {\n        return self._root_bounds;\n    }\n\n    pub fn getIntersectionRatio(self: *const IntersectionObserverEntry) f64 {\n        return self._intersection_ratio;\n    }\n\n    pub fn getIsIntersecting(self: *const IntersectionObserverEntry) bool {\n        return self._is_intersecting;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(IntersectionObserverEntry);\n\n        pub const Meta = struct {\n            pub const name = \"IntersectionObserverEntry\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n            pub const weak = true;\n            pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit);\n        };\n\n        pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});\n        pub const time = bridge.accessor(IntersectionObserverEntry.getTime, null, .{});\n        pub const boundingClientRect = bridge.accessor(IntersectionObserverEntry.getBoundingClientRect, null, .{});\n        pub const intersectionRect = bridge.accessor(IntersectionObserverEntry.getIntersectionRect, null, .{});\n        pub const rootBounds = bridge.accessor(IntersectionObserverEntry.getRootBounds, null, .{});\n        pub const intersectionRatio = bridge.accessor(IntersectionObserverEntry.getIntersectionRatio, null, .{});\n        pub const isIntersecting = bridge.accessor(IntersectionObserverEntry.getIsIntersecting, null, .{});\n    };\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(IntersectionObserver);\n\n    pub const Meta = struct {\n        pub const name = \"IntersectionObserver\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);\n    };\n\n    pub const constructor = bridge.constructor(init, .{});\n\n    pub const observe = bridge.function(IntersectionObserver.observe, .{});\n    pub const unobserve = bridge.function(IntersectionObserver.unobserve, .{});\n    pub const disconnect = bridge.function(IntersectionObserver.disconnect, .{});\n    pub const takeRecords = bridge.function(IntersectionObserver.takeRecords, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: IntersectionObserver\" {\n    try testing.htmlRunner(\"intersection_observer\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/KeyValueList.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub fn registerTypes() []const type {\n    return &.{\n        KeyIterator,\n        ValueIterator,\n        EntryIterator,\n    };\n}\n\nconst Normalizer = *const fn ([]const u8, *Page) []const u8;\n\npub const Entry = struct {\n    name: String,\n    value: String,\n\n    pub fn format(self: Entry, writer: *std.Io.Writer) !void {\n        return writer.print(\"{f}: {f}\", .{ self.name, self.value });\n    }\n};\n\npub const KeyValueList = @This();\n\n_entries: std.ArrayList(Entry) = .empty,\n\npub const empty: KeyValueList = .{\n    ._entries = .empty,\n};\n\npub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList {\n    var list = KeyValueList.init();\n    try list.ensureTotalCapacity(arena, original.len());\n    for (original._entries.items) |entry| {\n        try list.appendAssumeCapacity(arena, entry.name.str(), entry.value.str());\n    }\n    return list;\n}\n\npub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList {\n    var it = try js_obj.nameIterator();\n    var list = KeyValueList.init();\n    try list.ensureTotalCapacity(arena, it.count);\n\n    while (try it.next()) |name| {\n        const js_value = try js_obj.get(name);\n        const normalized = if (comptime normalizer) |n| n(name, page) else name;\n\n        list._entries.appendAssumeCapacity(.{\n            .name = try String.init(arena, normalized, .{}),\n            .value = try js_value.toSSOWithAlloc(arena),\n        });\n    }\n\n    return list;\n}\n\npub fn fromArray(arena: Allocator, kvs: []const [2][]const u8, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList {\n    var list = KeyValueList.init();\n    try list.ensureTotalCapacity(arena, kvs.len);\n\n    for (kvs) |pair| {\n        const normalized = if (comptime normalizer) |n| n(pair[0], page) else pair[0];\n\n        list._entries.appendAssumeCapacity(.{\n            .name = try String.init(arena, normalized, .{}),\n            .value = try String.init(arena, pair[1], .{}),\n        });\n    }\n    return list;\n}\n\npub fn init() KeyValueList {\n    return .{};\n}\n\npub fn ensureTotalCapacity(self: *KeyValueList, allocator: Allocator, n: usize) !void {\n    return self._entries.ensureTotalCapacity(allocator, n);\n}\n\npub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 {\n    for (self._entries.items) |*entry| {\n        if (entry.name.eqlSlice(name)) {\n            return entry.value.str();\n        }\n    }\n    return null;\n}\n\npub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 {\n    const arena = page.call_arena;\n    var arr: std.ArrayList([]const u8) = .empty;\n    for (self._entries.items) |*entry| {\n        if (entry.name.eqlSlice(name)) {\n            try arr.append(arena, entry.value.str());\n        }\n    }\n    return arr.items;\n}\n\npub fn has(self: *const KeyValueList, name: []const u8) bool {\n    for (self._entries.items) |*entry| {\n        if (entry.name.eqlSlice(name)) {\n            return true;\n        }\n    }\n    return false;\n}\n\npub fn append(self: *KeyValueList, allocator: Allocator, name: []const u8, value: []const u8) !void {\n    try self._entries.append(allocator, .{\n        .name = try String.init(allocator, name, .{}),\n        .value = try String.init(allocator, value, .{}),\n    });\n}\n\npub fn appendAssumeCapacity(self: *KeyValueList, allocator: Allocator, name: []const u8, value: []const u8) !void {\n    self._entries.appendAssumeCapacity(.{\n        .name = try String.init(allocator, name, .{}),\n        .value = try String.init(allocator, value, .{}),\n    });\n}\n\npub fn delete(self: *KeyValueList, name: []const u8, value: ?[]const u8) void {\n    var i: usize = 0;\n    while (i < self._entries.items.len) {\n        const entry = self._entries.items[i];\n        if (entry.name.eqlSlice(name)) {\n            if (value == null or entry.value.eqlSlice(value.?)) {\n                _ = self._entries.swapRemove(i);\n                continue;\n            }\n        }\n        i += 1;\n    }\n}\n\npub fn set(self: *KeyValueList, allocator: Allocator, name: []const u8, value: []const u8) !void {\n    self.delete(name, null);\n    try self.append(allocator, name, value);\n}\n\npub fn len(self: *const KeyValueList) usize {\n    return self._entries.items.len;\n}\n\npub fn items(self: *const KeyValueList) []const Entry {\n    return self._entries.items;\n}\n\nconst URLEncodeMode = enum {\n    form,\n    query,\n};\n\npub fn urlEncode(self: *const KeyValueList, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {\n    const entries = self._entries.items;\n    if (entries.len == 0) {\n        return;\n    }\n\n    try urlEncodeEntry(entries[0], mode, writer);\n    for (entries[1..]) |entry| {\n        try writer.writeByte('&');\n        try urlEncodeEntry(entry, mode, writer);\n    }\n}\n\nfn urlEncodeEntry(entry: Entry, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {\n    try urlEncodeValue(entry.name.str(), mode, writer);\n\n    // for a form, for an empty value, we'll do \"spice=\"\n    // but for a query, we do \"spice\"\n    if ((comptime mode == .query) and entry.value.len == 0) {\n        return;\n    }\n\n    try writer.writeByte('=');\n    try urlEncodeValue(entry.value.str(), mode, writer);\n}\n\nfn urlEncodeValue(value: []const u8, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {\n    if (!urlEncodeShouldEscape(value, mode)) {\n        return writer.writeAll(value);\n    }\n\n    for (value) |b| {\n        if (urlEncodeUnreserved(b, mode)) {\n            try writer.writeByte(b);\n        } else if (b == ' ') {\n            try writer.writeByte('+');\n        } else if (b >= 0x80) {\n            // Double-encode: treat byte as Latin-1 code point, encode to UTF-8, then percent-encode\n            // For bytes 0x80-0xFF (U+0080 to U+00FF), UTF-8 encoding is 2 bytes:\n            // [0xC0 | (b >> 6), 0x80 | (b & 0x3F)]\n            const byte1 = 0xC0 | (b >> 6);\n            const byte2 = 0x80 | (b & 0x3F);\n            try writer.print(\"%{X:0>2}%{X:0>2}\", .{ byte1, byte2 });\n        } else {\n            try writer.print(\"%{X:0>2}\", .{b});\n        }\n    }\n}\n\nfn urlEncodeShouldEscape(value: []const u8, comptime mode: URLEncodeMode) bool {\n    for (value) |b| {\n        if (!urlEncodeUnreserved(b, mode)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nfn urlEncodeUnreserved(b: u8, comptime mode: URLEncodeMode) bool {\n    return switch (b) {\n        'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '*' => true,\n        '~' => comptime mode == .form,\n        else => false,\n    };\n}\n\npub const Iterator = struct {\n    index: u32 = 0,\n    kv: *KeyValueList,\n\n    // Why? Because whenever an Iterator is created, we need to increment the\n    // RC of what it's iterating. And when the iterator is destroyed, we need\n    // to decrement it. The generic iterator which will wrap this handles that\n    // by using this \"list\" field. Most things that use the GenericIterator can\n    // just set `list: *ZigCollection`, and everything will work. But KeyValueList\n    // is being composed by various types, so it can't reference those types.\n    // Using *anyopaque here is \"dangerous\", in that it requires the composer\n    // to pass the right value, which normally would be itself (`*Self`), but\n    // only because (as of now) everyting that uses KeyValueList has no prototype\n    list: *anyopaque,\n\n    pub const Entry = struct { []const u8, []const u8 };\n\n    pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry {\n        const index = self.index;\n        const entries = self.kv._entries.items;\n        if (index >= entries.len) {\n            return null;\n        }\n        self.index = index + 1;\n\n        const e = &entries[index];\n        return .{ e.name.str(), e.value.str() };\n    }\n};\n\npub fn iterator(self: *const KeyValueList) Iterator {\n    return .{ .list = self };\n}\n\nconst GenericIterator = @import(\"collections/iterator.zig\").Entry;\npub const KeyIterator = GenericIterator(Iterator, \"0\");\npub const ValueIterator = GenericIterator(Iterator, \"1\");\npub const EntryIterator = GenericIterator(Iterator, null);\n"
  },
  {
    "path": "src/browser/webapi/Location.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst URL = @import(\"URL.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst Location = @This();\n\n_url: *URL,\n\npub fn init(raw_url: [:0]const u8, page: *Page) !*Location {\n    const url = try URL.init(raw_url, null, page);\n    return page._factory.create(Location{\n        ._url = url,\n    });\n}\n\npub fn getPathname(self: *const Location) []const u8 {\n    return self._url.getPathname();\n}\n\npub fn getProtocol(self: *const Location) []const u8 {\n    return self._url.getProtocol();\n}\n\npub fn getHostname(self: *const Location) []const u8 {\n    return self._url.getHostname();\n}\n\npub fn getHost(self: *const Location) []const u8 {\n    return self._url.getHost();\n}\n\npub fn getPort(self: *const Location) []const u8 {\n    return self._url.getPort();\n}\n\npub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 {\n    return self._url.getOrigin(page);\n}\n\npub fn getSearch(self: *const Location, page: *const Page) ![]const u8 {\n    return self._url.getSearch(page);\n}\n\npub fn getHash(self: *const Location) []const u8 {\n    return self._url.getHash();\n}\n\npub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void {\n    const normalized_hash = blk: {\n        if (hash.len == 0) {\n            const old_url = page.url;\n\n            break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|\n                old_url[0..index]\n            else\n                old_url;\n        } else if (hash[0] == '#')\n            break :blk hash\n        else\n            break :blk try std.fmt.allocPrint(page.call_arena, \"#{s}\", .{hash});\n    };\n\n    return page.scheduleNavigation(normalized_hash, .{\n        .reason = .script,\n        .kind = .{ .replace = null },\n    }, .{ .script = page });\n}\n\npub fn assign(_: *const Location, url: [:0]const u8, page: *Page) !void {\n    return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = page });\n}\n\npub fn replace(_: *const Location, url: [:0]const u8, page: *Page) !void {\n    return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .{ .script = page });\n}\n\npub fn reload(_: *const Location, page: *Page) !void {\n    return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });\n}\n\npub fn toString(self: *const Location, page: *const Page) ![:0]const u8 {\n    return self._url.toString(page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Location);\n\n    pub const Meta = struct {\n        pub const name = \"Location\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const toString = bridge.function(Location.toString, .{});\n    pub const href = bridge.accessor(Location.toString, setHref, .{});\n    fn setHref(self: *const Location, url: [:0]const u8, page: *Page) !void {\n        return self.assign(url, page);\n    }\n\n    pub const search = bridge.accessor(Location.getSearch, null, .{});\n    pub const hash = bridge.accessor(Location.getHash, Location.setHash, .{});\n    pub const pathname = bridge.accessor(Location.getPathname, null, .{});\n    pub const hostname = bridge.accessor(Location.getHostname, null, .{});\n    pub const host = bridge.accessor(Location.getHost, null, .{});\n    pub const port = bridge.accessor(Location.getPort, null, .{});\n    pub const origin = bridge.accessor(Location.getOrigin, null, .{});\n    pub const protocol = bridge.accessor(Location.getProtocol, null, .{});\n    pub const assign = bridge.function(Location.assign, .{});\n    pub const replace = bridge.function(Location.replace, .{});\n    pub const reload = bridge.function(Location.reload, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/MessageChannel.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst MessagePort = @import(\"MessagePort.zig\");\n\nconst MessageChannel = @This();\n\n_port1: *MessagePort,\n_port2: *MessagePort,\n\npub fn init(page: *Page) !*MessageChannel {\n    const port1 = try MessagePort.init(page);\n    const port2 = try MessagePort.init(page);\n\n    MessagePort.entangle(port1, port2);\n\n    return page._factory.create(MessageChannel{\n        ._port1 = port1,\n        ._port2 = port2,\n    });\n}\n\npub fn getPort1(self: *const MessageChannel) *MessagePort {\n    return self._port1;\n}\n\npub fn getPort2(self: *const MessageChannel) *MessagePort {\n    return self._port2;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MessageChannel);\n\n    pub const Meta = struct {\n        pub const name = \"MessageChannel\";\n        pub var class_id: bridge.ClassId = undefined;\n        pub const prototype_chain = bridge.prototypeChain();\n    };\n\n    pub const constructor = bridge.constructor(MessageChannel.init, .{});\n    pub const port1 = bridge.accessor(MessageChannel.getPort1, null, .{});\n    pub const port2 = bridge.accessor(MessageChannel.getPort2, null, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: MessageChannel\" {\n    try testing.htmlRunner(\"message_channel.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/MessagePort.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\nconst MessageEvent = @import(\"event/MessageEvent.zig\");\n\nconst MessagePort = @This();\n\n_proto: *EventTarget,\n_enabled: bool = false,\n_closed: bool = false,\n_on_message: ?js.Function.Global = null,\n_on_message_error: ?js.Function.Global = null,\n_entangled_port: ?*MessagePort = null,\n\npub fn init(page: *Page) !*MessagePort {\n    return page._factory.eventTarget(MessagePort{\n        ._proto = undefined,\n    });\n}\n\npub fn asEventTarget(self: *MessagePort) *EventTarget {\n    return self._proto;\n}\n\npub fn entangle(port1: *MessagePort, port2: *MessagePort) void {\n    port1._entangled_port = port2;\n    port2._entangled_port = port1;\n}\n\npub fn postMessage(self: *MessagePort, message: js.Value.Temp, page: *Page) !void {\n    if (self._closed) {\n        return;\n    }\n\n    const other = self._entangled_port orelse return;\n    if (other._closed) {\n        return;\n    }\n\n    // Create callback to deliver message\n    const callback = try page._factory.create(PostMessageCallback{\n        .page = page,\n        .port = other,\n        .message = message,\n    });\n\n    try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{\n        .name = \"MessagePort.postMessage\",\n        .low_priority = false,\n    });\n}\n\npub fn start(self: *MessagePort) void {\n    if (self._closed) {\n        return;\n    }\n    self._enabled = true;\n}\n\npub fn close(self: *MessagePort) void {\n    self._closed = true;\n\n    // Break entanglement\n    if (self._entangled_port) |other| {\n        other._entangled_port = null;\n    }\n    self._entangled_port = null;\n}\n\npub fn getOnMessage(self: *const MessagePort) ?js.Function.Global {\n    return self._on_message;\n}\n\npub fn setOnMessage(self: *MessagePort, cb: ?js.Function.Global) !void {\n    self._on_message = cb;\n}\n\npub fn getOnMessageError(self: *const MessagePort) ?js.Function.Global {\n    return self._on_message_error;\n}\n\npub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {\n    self._on_message_error = cb;\n}\n\nconst PostMessageCallback = struct {\n    port: *MessagePort,\n    message: js.Value.Temp,\n    page: *Page,\n\n    fn deinit(self: *PostMessageCallback) void {\n        self.page._factory.destroy(self);\n    }\n\n    fn run(ctx: *anyopaque) !?u32 {\n        const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));\n        defer self.deinit();\n        const page = self.page;\n\n        if (self.port._closed) {\n            return null;\n        }\n\n        const target = self.port.asEventTarget();\n        if (page._event_manager.hasDirectListeners(target, \"message\", self.port._on_message)) {\n            const event = (MessageEvent.initTrusted(comptime .wrap(\"message\"), .{\n                .data = self.message,\n                .origin = \"\",\n                .source = null,\n            }, page) catch |err| {\n                log.err(.dom, \"MessagePort.postMessage\", .{ .err = err });\n                return null;\n            }).asEvent();\n\n            page._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = \"MessagePort message\" }) catch |err| {\n                log.err(.dom, \"MessagePort.postMessage\", .{ .err = err });\n            };\n        }\n\n        return null;\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MessagePort);\n\n    pub const Meta = struct {\n        pub const name = \"MessagePort\";\n        pub var class_id: bridge.ClassId = undefined;\n        pub const prototype_chain = bridge.prototypeChain();\n    };\n\n    pub const postMessage = bridge.function(MessagePort.postMessage, .{});\n    pub const start = bridge.function(MessagePort.start, .{});\n    pub const close = bridge.function(MessagePort.close, .{});\n\n    pub const onmessage = bridge.accessor(MessagePort.getOnMessage, MessagePort.setOnMessage, .{});\n    pub const onmessageerror = bridge.accessor(MessagePort.getOnMessageError, MessagePort.setOnMessageError, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/MutationObserver.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\nconst Node = @import(\"Node.zig\");\nconst Element = @import(\"Element.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Allocator = std.mem.Allocator;\n\npub fn registerTypes() []const type {\n    return &.{\n        MutationObserver,\n        MutationRecord,\n    };\n}\n\nconst MutationObserver = @This();\n\n_arena: Allocator,\n_callback: js.Function.Temp,\n_observing: std.ArrayList(Observing) = .{},\n_pending_records: std.ArrayList(*MutationRecord) = .{},\n\n/// Intrusively linked to next element (see Page.zig).\nnode: std.DoublyLinkedList.Node = .{},\n\nconst Observing = struct {\n    target: *Node,\n    options: ResolvedOptions,\n};\n\n/// Internal options with all nullable bools resolved to concrete values.\nconst ResolvedOptions = struct {\n    attributes: bool = false,\n    attributeOldValue: bool = false,\n    childList: bool = false,\n    characterData: bool = false,\n    characterDataOldValue: bool = false,\n    subtree: bool = false,\n    attributeFilter: ?[]const []const u8 = null,\n};\n\npub const ObserveOptions = struct {\n    attributes: ?bool = null,\n    attributeOldValue: ?bool = null,\n    childList: bool = false,\n    characterData: ?bool = null,\n    characterDataOldValue: ?bool = null,\n    subtree: bool = false,\n    attributeFilter: ?[]const []const u8 = null,\n};\n\npub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {\n    const arena = try page.getArena(.{ .debug = \"MutationObserver\" });\n    errdefer page.releaseArena(arena);\n\n    const self = try arena.create(MutationObserver);\n    self.* = .{\n        ._arena = arena,\n        ._callback = callback,\n    };\n    return self;\n}\n\npub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {\n    if (shutdown) {\n        self._callback.release();\n        session.releaseArena(self._arena);\n    } else if (comptime IS_DEBUG) {\n        std.debug.assert(false);\n    }\n}\n\npub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {\n    const arena = self._arena;\n\n    // Per spec: if attributeOldValue/attributeFilter present and attributes\n    // not explicitly set, imply attributes=true. Same for characterData.\n    var resolved = options;\n    if (resolved.attributes == null and (resolved.attributeOldValue != null or resolved.attributeFilter != null)) {\n        resolved.attributes = true;\n    }\n    if (resolved.characterData == null and resolved.characterDataOldValue != null) {\n        resolved.characterData = true;\n    }\n\n    const attributes = resolved.attributes orelse false;\n    const character_data = resolved.characterData orelse false;\n\n    // Validate: at least one of childList/attributes/characterData must be true\n    if (!resolved.childList and !attributes and !character_data) {\n        return error.TypeError;\n    }\n\n    // Validate: attributeOldValue/attributeFilter require attributes != false\n    if ((resolved.attributeOldValue orelse false) and !attributes) {\n        return error.TypeError;\n    }\n    if (resolved.attributeFilter != null and !attributes) {\n        return error.TypeError;\n    }\n\n    // Validate: characterDataOldValue requires characterData != false\n    if ((resolved.characterDataOldValue orelse false) and !character_data) {\n        return error.TypeError;\n    }\n\n    // Build resolved options with concrete bool values\n    var store_options = ResolvedOptions{\n        .attributes = attributes,\n        .attributeOldValue = resolved.attributeOldValue orelse false,\n        .childList = resolved.childList,\n        .characterData = character_data,\n        .characterDataOldValue = resolved.characterDataOldValue orelse false,\n        .subtree = resolved.subtree,\n        .attributeFilter = resolved.attributeFilter,\n    };\n\n    // Deep copy attributeFilter if present\n    if (options.attributeFilter) |filter| {\n        const filter_copy = try arena.alloc([]const u8, filter.len);\n        for (filter, 0..) |name, i| {\n            filter_copy[i] = try arena.dupe(u8, name);\n        }\n        store_options.attributeFilter = filter_copy;\n    }\n\n    // Check if already observing this target\n    for (self._observing.items) |*obs| {\n        if (obs.target == target) {\n            obs.options = store_options;\n            return;\n        }\n    }\n\n    // Register with page if this is our first observation\n    if (self._observing.items.len == 0) {\n        try page.registerMutationObserver(self);\n    }\n\n    try self._observing.append(arena, .{\n        .target = target,\n        .options = store_options,\n    });\n}\n\npub fn disconnect(self: *MutationObserver, page: *Page) void {\n    for (self._pending_records.items) |record| {\n        record.deinit(false, page._session);\n    }\n    self._pending_records.clearRetainingCapacity();\n\n    self._observing.clearRetainingCapacity();\n    page.unregisterMutationObserver(self);\n}\n\npub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {\n    const records = try page.call_arena.dupe(*MutationRecord, self._pending_records.items);\n    self._pending_records.clearRetainingCapacity();\n    return records;\n}\n\n// Called when an attribute changes on any element\npub fn notifyAttributeChange(\n    self: *MutationObserver,\n    target: *Element,\n    attribute_name: String,\n    old_value: ?String,\n    page: *Page,\n) !void {\n    const target_node = target.asNode();\n\n    for (self._observing.items) |obs| {\n        if (obs.target != target_node) {\n            if (!obs.options.subtree) {\n                continue;\n            }\n            if (!obs.target.contains(target_node)) {\n                continue;\n            }\n        }\n        if (!obs.options.attributes) {\n            continue;\n        }\n        if (obs.options.attributeFilter) |filter| {\n            for (filter) |name| {\n                if (attribute_name.eqlSlice(name)) {\n                    break;\n                }\n            } else {\n                continue;\n            }\n        }\n\n        const arena = try page.getArena(.{ .debug = \"MutationRecord\" });\n        const record = try arena.create(MutationRecord);\n        record.* = .{\n            ._arena = arena,\n            ._type = .attributes,\n            ._target = target_node,\n            ._attribute_name = try arena.dupe(u8, attribute_name.str()),\n            ._old_value = if (obs.options.attributeOldValue and old_value != null)\n                try arena.dupe(u8, old_value.?.str())\n            else\n                null,\n            ._added_nodes = &.{},\n            ._removed_nodes = &.{},\n            ._previous_sibling = null,\n            ._next_sibling = null,\n        };\n\n        try self._pending_records.append(self._arena, record);\n\n        try page.scheduleMutationDelivery();\n        break;\n    }\n}\n\n// Called when character data changes on a text node\npub fn notifyCharacterDataChange(\n    self: *MutationObserver,\n    target: *Node,\n    old_value: ?String,\n    page: *Page,\n) !void {\n    for (self._observing.items) |obs| {\n        if (obs.target != target) {\n            if (!obs.options.subtree) {\n                continue;\n            }\n            if (!obs.target.contains(target)) {\n                continue;\n            }\n        }\n        if (!obs.options.characterData) {\n            continue;\n        }\n\n        const arena = try page.getArena(.{ .debug = \"MutationRecord\" });\n        const record = try arena.create(MutationRecord);\n        record.* = .{\n            ._arena = arena,\n            ._type = .characterData,\n            ._target = target,\n            ._attribute_name = null,\n            ._old_value = if (obs.options.characterDataOldValue and old_value != null)\n                try arena.dupe(u8, old_value.?.str())\n            else\n                null,\n            ._added_nodes = &.{},\n            ._removed_nodes = &.{},\n            ._previous_sibling = null,\n            ._next_sibling = null,\n        };\n\n        try self._pending_records.append(self._arena, record);\n\n        try page.scheduleMutationDelivery();\n        break;\n    }\n}\n\n// Called when children are added or removed from a node\npub fn notifyChildListChange(\n    self: *MutationObserver,\n    target: *Node,\n    added_nodes: []const *Node,\n    removed_nodes: []const *Node,\n    previous_sibling: ?*Node,\n    next_sibling: ?*Node,\n    page: *Page,\n) !void {\n    for (self._observing.items) |obs| {\n        if (obs.target != target) {\n            if (!obs.options.subtree) {\n                continue;\n            }\n            if (!obs.target.contains(target)) {\n                continue;\n            }\n        }\n        if (!obs.options.childList) {\n            continue;\n        }\n\n        const arena = try page.getArena(.{ .debug = \"MutationRecord\" });\n        const record = try arena.create(MutationRecord);\n        record.* = .{\n            ._arena = arena,\n            ._type = .childList,\n            ._target = target,\n            ._attribute_name = null,\n            ._old_value = null,\n            ._added_nodes = try arena.dupe(*Node, added_nodes),\n            ._removed_nodes = try arena.dupe(*Node, removed_nodes),\n            ._previous_sibling = previous_sibling,\n            ._next_sibling = next_sibling,\n        };\n\n        try self._pending_records.append(self._arena, record);\n\n        try page.scheduleMutationDelivery();\n        break;\n    }\n}\n\npub fn deliverRecords(self: *MutationObserver, page: *Page) !void {\n    if (self._pending_records.items.len == 0) {\n        return;\n    }\n\n    // Take a copy of the records and clear the list before calling callback\n    // This ensures mutations triggered during the callback go into a fresh list\n    const records = try self.takeRecords(page);\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var caught: js.TryCatch.Caught = undefined;\n    ls.toLocal(self._callback).tryCall(void, .{ records, self }, &caught) catch |err| {\n        log.err(.page, \"MutObserver.deliverRecords\", .{ .err = err, .caught = caught });\n        return err;\n    };\n}\n\npub const MutationRecord = struct {\n    _type: Type,\n    _target: *Node,\n    _arena: Allocator,\n    _attribute_name: ?[]const u8,\n    _old_value: ?[]const u8,\n    _added_nodes: []const *Node,\n    _removed_nodes: []const *Node,\n    _previous_sibling: ?*Node,\n    _next_sibling: ?*Node,\n\n    pub const Type = enum {\n        attributes,\n        childList,\n        characterData,\n    };\n\n    pub fn deinit(self: *MutationRecord, _: bool, session: *Session) void {\n        session.releaseArena(self._arena);\n    }\n\n    pub fn getType(self: *const MutationRecord) []const u8 {\n        return switch (self._type) {\n            .attributes => \"attributes\",\n            .childList => \"childList\",\n            .characterData => \"characterData\",\n        };\n    }\n\n    pub fn getTarget(self: *const MutationRecord) *Node {\n        return self._target;\n    }\n\n    pub fn getAttributeNamespace(self: *const MutationRecord) ?[]const u8 {\n        _ = self;\n        // Non-namespaced attribute mutations return null. Full namespace tracking\n        // for setAttributeNS mutations is not yet implemented.\n        return null;\n    }\n\n    pub fn getAttributeName(self: *const MutationRecord) ?[]const u8 {\n        return self._attribute_name;\n    }\n\n    pub fn getOldValue(self: *const MutationRecord) ?[]const u8 {\n        return self._old_value;\n    }\n\n    pub fn getAddedNodes(self: *const MutationRecord) []const *Node {\n        return self._added_nodes;\n    }\n\n    pub fn getRemovedNodes(self: *const MutationRecord) []const *Node {\n        return self._removed_nodes;\n    }\n\n    pub fn getPreviousSibling(self: *const MutationRecord) ?*Node {\n        return self._previous_sibling;\n    }\n\n    pub fn getNextSibling(self: *const MutationRecord) ?*Node {\n        return self._next_sibling;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(MutationRecord);\n\n        pub const Meta = struct {\n            pub const name = \"MutationRecord\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n            pub const weak = true;\n            pub const finalizer = bridge.finalizer(MutationRecord.deinit);\n        };\n\n        pub const @\"type\" = bridge.accessor(MutationRecord.getType, null, .{});\n        pub const target = bridge.accessor(MutationRecord.getTarget, null, .{});\n        pub const attributeName = bridge.accessor(MutationRecord.getAttributeName, null, .{});\n        pub const attributeNamespace = bridge.accessor(MutationRecord.getAttributeNamespace, null, .{});\n        pub const oldValue = bridge.accessor(MutationRecord.getOldValue, null, .{});\n        pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{});\n        pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{});\n        pub const previousSibling = bridge.accessor(MutationRecord.getPreviousSibling, null, .{});\n        pub const nextSibling = bridge.accessor(MutationRecord.getNextSibling, null, .{});\n    };\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MutationObserver);\n\n    pub const Meta = struct {\n        pub const name = \"MutationObserver\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const finalizer = bridge.finalizer(MutationObserver.deinit);\n    };\n\n    pub const constructor = bridge.constructor(MutationObserver.init, .{});\n\n    pub const observe = bridge.function(MutationObserver.observe, .{});\n    pub const disconnect = bridge.function(MutationObserver.disconnect, .{});\n    pub const takeRecords = bridge.function(MutationObserver.takeRecords, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: MutationObserver\" {\n    try testing.htmlRunner(\"mutation_observer\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/Navigator.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst log = @import(\"../../log.zig\");\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\nconst PluginArray = @import(\"PluginArray.zig\");\nconst Permissions = @import(\"Permissions.zig\");\nconst StorageManager = @import(\"StorageManager.zig\");\n\nconst Navigator = @This();\n_pad: bool = false,\n_plugins: PluginArray = .{},\n_permissions: Permissions = .{},\n_storage: StorageManager = .{},\n\npub const init: Navigator = .{};\n\npub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {\n    return page._session.browser.app.config.http_headers.user_agent;\n}\n\npub fn getLanguages(_: *const Navigator) [1][]const u8 {\n    return .{\"en-US\"};\n}\n\npub fn getPlatform(_: *const Navigator) []const u8 {\n    return switch (builtin.os.tag) {\n        .macos => \"MacIntel\",\n        .windows => \"Win32\",\n        .linux => \"Linux x86_64\",\n        .freebsd => \"FreeBSD\",\n        else => \"Unknown\",\n    };\n}\n\n/// Returns whether Java is enabled (always false)\npub fn javaEnabled(_: *const Navigator) bool {\n    return false;\n}\n\npub fn getPlugins(self: *Navigator) *PluginArray {\n    return &self._plugins;\n}\n\npub fn getPermissions(self: *Navigator) *Permissions {\n    return &self._permissions;\n}\n\npub fn getStorage(self: *Navigator) *StorageManager {\n    return &self._storage;\n}\n\npub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {\n    log.info(.not_implemented, \"navigator.getBattery\", .{});\n    return page.js.local.?.rejectErrorPromise(.{ .dom_exception = error.NotSupported });\n}\n\npub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {\n    try validateProtocolHandlerScheme(scheme);\n    try validateProtocolHandlerURL(url, page);\n}\npub fn unregisterProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {\n    try validateProtocolHandlerScheme(scheme);\n    try validateProtocolHandlerURL(url, page);\n}\n\nfn validateProtocolHandlerScheme(scheme: []const u8) !void {\n    const allowed = std.StaticStringMap(void).initComptime(.{\n        .{ \"bitcoin\", {} },\n        .{ \"cabal\", {} },\n        .{ \"dat\", {} },\n        .{ \"did\", {} },\n        .{ \"dweb\", {} },\n        .{ \"ethereum\", .{} },\n        .{ \"ftp\", {} },\n        .{ \"ftps\", {} },\n        .{ \"geo\", {} },\n        .{ \"im\", {} },\n        .{ \"ipfs\", {} },\n        .{ \"ipns\", .{} },\n        .{ \"irc\", {} },\n        .{ \"ircs\", {} },\n        .{ \"hyper\", {} },\n        .{ \"magnet\", {} },\n        .{ \"mailto\", {} },\n        .{ \"matrix\", {} },\n        .{ \"mms\", {} },\n        .{ \"news\", {} },\n        .{ \"nntp\", {} },\n        .{ \"openpgp4fpr\", {} },\n        .{ \"sftp\", {} },\n        .{ \"sip\", {} },\n        .{ \"sms\", {} },\n        .{ \"smsto\", {} },\n        .{ \"ssb\", {} },\n        .{ \"ssh\", {} },\n        .{ \"tel\", {} },\n        .{ \"urn\", {} },\n        .{ \"webcal\", {} },\n        .{ \"wtai\", {} },\n        .{ \"xmpp\", {} },\n    });\n    if (allowed.has(scheme)) {\n        return;\n    }\n\n    if (scheme.len < 5 or !std.mem.startsWith(u8, scheme, \"web+\")) {\n        return error.SecurityError;\n    }\n    for (scheme[4..]) |b| {\n        if (std.ascii.isLower(b) == false) {\n            return error.SecurityError;\n        }\n    }\n}\n\nfn validateProtocolHandlerURL(url: [:0]const u8, page: *const Page) !void {\n    if (std.mem.indexOf(u8, url, \"%s\") == null) {\n        return error.SyntaxError;\n    }\n    if (try page.isSameOrigin(url) == false) {\n        return error.SyntaxError;\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Navigator);\n\n    pub const Meta = struct {\n        pub const name = \"Navigator\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    // Read-only properties\n    pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{});\n    pub const appName = bridge.property(\"Netscape\", .{ .template = false });\n    pub const appCodeName = bridge.property(\"Netscape\", .{ .template = false });\n    pub const appVersion = bridge.property(\"1.0\", .{ .template = false });\n    pub const platform = bridge.accessor(Navigator.getPlatform, null, .{});\n    pub const language = bridge.property(\"en-US\", .{ .template = false });\n    pub const languages = bridge.accessor(Navigator.getLanguages, null, .{});\n    pub const onLine = bridge.property(true, .{ .template = false });\n    pub const cookieEnabled = bridge.property(true, .{ .template = false });\n    pub const hardwareConcurrency = bridge.property(4, .{ .template = false });\n    pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false });\n    pub const maxTouchPoints = bridge.property(0, .{ .template = false });\n    pub const vendor = bridge.property(\"\", .{ .template = false });\n    pub const product = bridge.property(\"Gecko\", .{ .template = false });\n    pub const webdriver = bridge.property(false, .{ .template = false });\n    pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{});\n    pub const doNotTrack = bridge.property(null, .{ .template = false });\n    pub const globalPrivacyControl = bridge.property(true, .{ .template = false });\n    pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .dom_exception = true });\n    pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .dom_exception = true });\n\n    // Methods\n    pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});\n    pub const getBattery = bridge.function(Navigator.getBattery, .{});\n    pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{});\n    pub const storage = bridge.accessor(Navigator.getStorage, null, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Navigator\" {\n    try testing.htmlRunner(\"navigator\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/Node.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../log.zig\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst reflect = @import(\"../reflect.zig\");\n\nconst EventTarget = @import(\"EventTarget.zig\");\nconst collections = @import(\"collections.zig\");\n\npub const CData = @import(\"CData.zig\");\npub const Element = @import(\"Element.zig\");\npub const Document = @import(\"Document.zig\");\npub const HTMLDocument = @import(\"HTMLDocument.zig\");\npub const Children = @import(\"children.zig\").Children;\npub const DocumentFragment = @import(\"DocumentFragment.zig\");\npub const DocumentType = @import(\"DocumentType.zig\");\npub const ShadowRoot = @import(\"ShadowRoot.zig\");\n\nconst Allocator = std.mem.Allocator;\nconst LinkedList = std.DoublyLinkedList;\n\nconst Node = @This();\n\n_type: Type,\n_proto: *EventTarget,\n_parent: ?*Node = null,\n_children: ?*Children = null,\n_child_link: LinkedList.Node = .{},\n\n// Lookup for nodes that have a different owner document than page.document\npub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document);\n\npub const Type = union(enum) {\n    cdata: *CData,\n    element: *Element,\n    document: *Document,\n    document_type: *DocumentType,\n    attribute: *Element.Attribute,\n    document_fragment: *DocumentFragment,\n};\n\npub fn asEventTarget(self: *Node) *EventTarget {\n    return self._proto;\n}\n\n// Returns the node as a more specific type. Will crash if node is not a `T`.\n// Use `is` to optionally get the node as T\npub fn as(self: *Node, comptime T: type) *T {\n    return self.is(T).?;\n}\n\n// Return the node as a more specific type or `null` if the node is not a `T`.\npub fn is(self: *Node, comptime T: type) ?*T {\n    const type_name = @typeName(T);\n    switch (self._type) {\n        .element => |el| {\n            if (T == Element) {\n                return el;\n            }\n            if (comptime std.mem.startsWith(u8, type_name, \"browser.webapi.element.\")) {\n                return el.is(T);\n            }\n        },\n        .cdata => |cd| {\n            if (T == CData) {\n                return cd;\n            }\n            if (comptime std.mem.startsWith(u8, type_name, \"browser.webapi.cdata.\")) {\n                return cd.is(T);\n            }\n        },\n        .attribute => |attr| {\n            if (T == Element.Attribute) {\n                return attr;\n            }\n        },\n        .document => |doc| {\n            if (T == Document) {\n                return doc;\n            }\n            if (comptime std.mem.startsWith(u8, type_name, \"browser.webapi.htmldocument.\")) {\n                return doc.is(T);\n            }\n        },\n        .document_type => |dt| {\n            if (T == DocumentType) {\n                return dt;\n            }\n        },\n        .document_fragment => |doc| {\n            if (T == DocumentFragment) {\n                return doc;\n            }\n            if (T == ShadowRoot) {\n                return doc.is(ShadowRoot);\n            }\n        },\n    }\n    return null;\n}\n\n/// Given a position, returns target and previous nodes required for\n/// insertAdjacentHTML, insertAdjacentElement and insertAdjacentText.\n/// * `target_node` is `*Node` (where we actually insert),\n/// * `previous_node` is `?*Node`.\npub fn findAdjacentNodes(self: *Node, position: []const u8) !struct { *Node, ?*Node } {\n    // Case-insensitive match per HTML spec.\n    // \"beforeend\" was the most common case in my tests; we might adjust the order\n    // depending on which ones websites prefer most.\n    if (std.ascii.eqlIgnoreCase(position, \"beforeend\")) {\n        return .{ self, null };\n    }\n\n    if (std.ascii.eqlIgnoreCase(position, \"afterbegin\")) {\n        // Get the first child; null indicates there are no children.\n        return .{ self, self.firstChild() };\n    }\n\n    if (std.ascii.eqlIgnoreCase(position, \"beforebegin\")) {\n        // The node must have a parent node in order to use this variant.\n        const parent_node = self.parentNode() orelse return error.NoModificationAllowed;\n        // Parent cannot be Document.\n        switch (parent_node._type) {\n            .document, .document_fragment => return error.NoModificationAllowed,\n            else => {},\n        }\n\n        return .{ parent_node, self };\n    }\n\n    if (std.ascii.eqlIgnoreCase(position, \"afterend\")) {\n        // The node must have a parent node in order to use this variant.\n        const parent_node = self.parentNode() orelse return error.NoModificationAllowed;\n        // Parent cannot be Document.\n        switch (parent_node._type) {\n            .document, .document_fragment => return error.NoModificationAllowed,\n            else => {},\n        }\n\n        // Get the next sibling or null; null indicates our node is the only one.\n        return .{ parent_node, self.nextSibling() };\n    }\n\n    // Returned if:\n    // * position is not one of the four listed values.\n    // * The input is XML that is not well-formed.\n    return error.Syntax;\n}\n\npub fn firstChild(self: *const Node) ?*Node {\n    const children = self._children orelse return null;\n    return children.first();\n}\n\npub fn lastChild(self: *const Node) ?*Node {\n    const children = self._children orelse return null;\n    return children.last();\n}\n\npub fn nextSibling(self: *const Node) ?*Node {\n    return linkToNodeOrNull(self._child_link.next);\n}\n\npub fn previousSibling(self: *const Node) ?*Node {\n    return linkToNodeOrNull(self._child_link.prev);\n}\n\npub fn parentNode(self: *const Node) ?*Node {\n    return self._parent;\n}\n\npub fn parentElement(self: *const Node) ?*Element {\n    const parent = self._parent orelse return null;\n    return parent.is(Element);\n}\n\n// Validates that a node can be inserted as a child of parent.\nfn validateNodeInsertion(parent: *Node, node: *Node) !void {\n    // Check if parent is a valid type to have children\n    if (parent._type != .document and parent._type != .element and parent._type != .document_fragment) {\n        return error.HierarchyError;\n    }\n\n    // Check if node contains parent (would create a cycle)\n    if (node.contains(parent)) {\n        return error.HierarchyError;\n    }\n\n    if (node._type == .attribute) {\n        return error.HierarchyError;\n    }\n\n    // Doctype nodes can only be inserted into a Document\n    if (node._type == .document_type and parent._type != .document) {\n        return error.HierarchyError;\n    }\n}\n\npub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {\n    if (child.is(DocumentFragment)) |_| {\n        try page.appendAllChildren(child, self);\n        return child;\n    }\n\n    try validateNodeInsertion(self, child);\n\n    page.domChanged();\n\n    // If the child is currently connected, and if its new parent is connected,\n    // then we can remove + add a bit more efficiently (we don't have to fully\n    // disconnect then reconnect)\n    const child_connected = child.isConnected();\n\n    // Check if we're adopting the node to a different document\n    const child_owner = child.ownerDocument(page);\n    const parent_owner = self.ownerDocument(page) orelse self.as(Document);\n    const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;\n\n    if (child._parent) |parent| {\n        // we can signal removeNode that the child will remain connected\n        // (when it's appended to self) so that it can be a bit more efficient.\n        page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() });\n    }\n\n    // Adopt the node tree if moving between documents\n    if (adopting_to_new_document) {\n        try page.adoptNodeTree(child, parent_owner);\n    }\n\n    try page.appendNode(self, child, .{\n        .child_already_connected = child_connected,\n        .adopting_to_new_document = adopting_to_new_document,\n    });\n    return child;\n}\n\npub fn childNodes(self: *Node, page: *Page) !*collections.ChildNodes {\n    return collections.ChildNodes.init(self, page);\n}\n\npub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {\n    switch (self._type) {\n        .element, .document_fragment => {\n            var it = self.childrenIterator();\n            while (it.next()) |child| {\n                // ignore comments and processing instructions.\n                if (child.is(CData.Comment) != null or child.is(CData.ProcessingInstruction) != null) {\n                    continue;\n                }\n                try child.getTextContent(writer);\n            }\n        },\n        .cdata => |c| try writer.writeAll(c._data.str()),\n        .document => {},\n        .document_type => {},\n        .attribute => |attr| try writer.writeAll(attr._value.str()),\n    }\n}\n\npub fn getTextContentAlloc(self: *Node, allocator: Allocator) error{WriteFailed}![:0]const u8 {\n    var buf = std.Io.Writer.Allocating.init(allocator);\n    try self.getTextContent(&buf.writer);\n    try buf.writer.writeByte(0);\n    const data = buf.written();\n    return data[0 .. data.len - 1 :0];\n}\n\n/// Returns the \"child text content\" which is the concatenation of the data\n/// of all the Text node children of the node, in tree order.\n/// This differs from textContent which includes all descendant text.\n/// See: https://dom.spec.whatwg.org/#concept-child-text-content\npub fn getChildTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {\n    var it = self.childrenIterator();\n    while (it.next()) |child| {\n        if (child.is(CData.Text)) |text| {\n            try writer.writeAll(text._proto._data.str());\n        }\n    }\n}\n\npub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {\n    switch (self._type) {\n        .element => |el| {\n            if (data.len == 0) {\n                return el.replaceChildren(&.{}, page);\n            }\n            return el.replaceChildren(&.{.{ .text = data }}, page);\n        },\n        // Per spec, setting textContent on CharacterData runs replaceData(0, length, value)\n        .cdata => |c| try c.replaceData(0, c.getLength(), data, page),\n        .document => {},\n        .document_type => {},\n        .document_fragment => |frag| {\n            if (data.len == 0) {\n                return frag.replaceChildren(&.{}, page);\n            }\n            return frag.replaceChildren(&.{.{ .text = data }}, page);\n        },\n        .attribute => |attr| return attr.setValue(.wrap(data), page),\n    }\n}\n\npub fn getNodeName(self: *const Node, buf: []u8) []const u8 {\n    return switch (self._type) {\n        .element => |el| el.getTagNameSpec(buf),\n        .cdata => |cd| switch (cd._type) {\n            .text => \"#text\",\n            .cdata_section => \"#cdata-section\",\n            .comment => \"#comment\",\n            .processing_instruction => |pi| pi._target,\n        },\n        .document => \"#document\",\n        .document_type => |dt| dt.getName(),\n        .document_fragment => \"#document-fragment\",\n        .attribute => |attr| attr._name.str(),\n    };\n}\n\npub fn getNodeType(self: *const Node) u8 {\n    return switch (self._type) {\n        .element => 1,\n        .attribute => 2,\n        .cdata => |cd| switch (cd._type) {\n            .text => 3,\n            .cdata_section => 4,\n            .processing_instruction => 7,\n            .comment => 8,\n        },\n        .document => 9,\n        .document_type => 10,\n        .document_fragment => 11,\n    };\n}\n\npub fn lookupNamespaceURI(self: *Node, prefix_arg: ?[]const u8, page: *Page) ?[]const u8 {\n    const prefix: ?[]const u8 = if (prefix_arg) |p| (if (p.len == 0) null else p) else null;\n\n    switch (self._type) {\n        .element => |el| return el.lookupNamespaceURIForElement(prefix, page),\n        .document => |doc| {\n            const de = doc.getDocumentElement() orelse return null;\n            return de.lookupNamespaceURIForElement(prefix, page);\n        },\n        .document_type, .document_fragment => return null,\n        .attribute => |attr| {\n            const owner = attr.getOwnerElement() orelse return null;\n            return owner.lookupNamespaceURIForElement(prefix, page);\n        },\n        .cdata => {\n            const parent = self.parentElement() orelse return null;\n            return parent.lookupNamespaceURIForElement(prefix, page);\n        },\n    }\n}\n\npub fn isDefaultNamespace(self: *Node, namespace_arg: ?[]const u8, page: *Page) bool {\n    const namespace: ?[]const u8 = if (namespace_arg) |ns| (if (ns.len == 0) null else ns) else null;\n    const default_ns = self.lookupNamespaceURI(null, page);\n    if (default_ns == null and namespace == null) return true;\n    if (default_ns != null and namespace != null) return std.mem.eql(u8, default_ns.?, namespace.?);\n    return false;\n}\n\npub fn isEqualNode(self: *Node, other: *Node) bool {\n    if (self == other) {\n        return true;\n    }\n\n    // Make sure types match.\n    if (self.getNodeType() != other.getNodeType()) {\n        return false;\n    }\n\n    // TODO: Compare `localName` and prefix.\n    return switch (self._type) {\n        .element => self.as(Element).isEqualNode(other.as(Element)),\n        .attribute => self.as(Element.Attribute).isEqualNode(other.as(Element.Attribute)),\n        .cdata => self.as(CData).isEqualNode(other.as(CData)),\n        .document_fragment => self.as(DocumentFragment).isEqualNode(other.as(DocumentFragment)),\n        .document_type => self.as(DocumentType).isEqualNode(other.as(DocumentType)),\n        .document => {\n            // Document comparison is complex and rarely used in practice\n            log.warn(.not_implemented, \"Node.isEqualNode\", .{\n                .type = \"document\",\n            });\n            return false;\n        },\n    };\n}\n\npub fn isInShadowTree(self: *Node) bool {\n    var node = self._parent;\n    while (node) |n| {\n        if (n.is(ShadowRoot) != null) {\n            return true;\n        }\n        node = n._parent;\n    }\n    return false;\n}\n\npub fn isConnected(self: *const Node) bool {\n    // Walk up to find the root node\n    var root = self;\n    while (root._parent) |parent| {\n        root = parent;\n    }\n\n    switch (root._type) {\n        .document => return true,\n        .document_fragment => |df| {\n            const sr = df.is(ShadowRoot) orelse return false;\n            return sr._host.asNode().isConnected();\n        },\n        else => return false,\n    }\n}\n\nconst GetRootNodeOpts = struct {\n    composed: bool = false,\n};\npub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node {\n    const opts = opts_ orelse GetRootNodeOpts{};\n\n    var root = self;\n    while (root._parent) |parent| {\n        root = parent;\n    }\n\n    // If composed is true, traverse through shadow boundaries\n    if (opts.composed) {\n        while (true) {\n            const shadow_root = @constCast(root).is(ShadowRoot) orelse break;\n            root = shadow_root.getHost().asNode();\n            while (root._parent) |parent| {\n                root = parent;\n            }\n        }\n    }\n\n    return root;\n}\n\npub fn contains(self: *const Node, child_: ?*const Node) bool {\n    const child = child_ orelse return false;\n\n    if (self == child) {\n        // yes, this is correct\n        return true;\n    }\n\n    var parent = child._parent;\n    while (parent) |p| {\n        if (p == self) {\n            return true;\n        }\n        parent = p._parent;\n    }\n    return false;\n}\n\npub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {\n    // A document node does not have an owner.\n    if (self._type == .document) {\n        return null;\n    }\n\n    // The root of the tree that a node belongs to is its owner.\n    var current = self;\n    while (current._parent) |parent| {\n        current = parent;\n    }\n\n    // If the root is a document, then that's our owner.\n    if (current._type == .document) {\n        return current._type.document;\n    }\n\n    // Otherwise, this is a detached node. Check if it has a specific owner\n    // document registered (for nodes created via non-main documents).\n    if (page._node_owner_documents.get(@constCast(self))) |owner| {\n        return owner;\n    }\n\n    // Default to the main document for detached nodes without a specific owner.\n    return page.document;\n}\n\npub fn ownerPage(self: *const Node, default: *Page) *Page {\n    const doc = self.ownerDocument(default) orelse return default;\n    return doc._page orelse default;\n}\n\npub fn isSameDocumentAs(self: *const Node, other: *const Node, page: *const Page) bool {\n    // Get the root document for each node\n    const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(page);\n    const other_doc = if (other._type == .document) other._type.document else other.ownerDocument(page);\n    return self_doc == other_doc;\n}\n\npub fn hasChildNodes(self: *const Node) bool {\n    return self.firstChild() != null;\n}\n\npub fn isSameNode(self: *const Node, other: ?*Node) bool {\n    return self == other;\n}\n\npub fn removeChild(self: *Node, child: *Node, page: *Page) !*Node {\n    var it = self.childrenIterator();\n    while (it.next()) |n| {\n        if (n == child) {\n            page.domChanged();\n            page.removeNode(self, child, .{ .will_be_reconnected = false });\n            return child;\n        }\n    }\n    return error.NotFound;\n}\n\npub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page) !*Node {\n    const ref_node = ref_node_ orelse {\n        return self.appendChild(new_node, page);\n    };\n\n    // special case: if nodes are the same, ignore the change.\n    if (new_node == ref_node_) {\n        page.domChanged();\n\n        if (page.hasMutationObservers()) {\n            const parent = new_node._parent.?;\n            const previous_sibling = new_node.previousSibling();\n            const next_sibling = new_node.nextSibling();\n            const replaced = [_]*Node{new_node};\n            page.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling);\n        }\n\n        return new_node;\n    }\n\n    if (ref_node._parent == null or ref_node._parent.? != self) {\n        return error.NotFound;\n    }\n\n    if (new_node.is(DocumentFragment)) |_| {\n        try page.insertAllChildrenBefore(new_node, self, ref_node);\n        return new_node;\n    }\n\n    try validateNodeInsertion(self, new_node);\n\n    const child_already_connected = new_node.isConnected();\n\n    // Check if we're adopting the node to a different document\n    const child_owner = new_node.ownerDocument(page);\n    const parent_owner = self.ownerDocument(page) orelse self.as(Document);\n    const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;\n\n    page.domChanged();\n    const will_be_reconnected = self.isConnected();\n    if (new_node._parent) |parent| {\n        page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });\n    }\n\n    // Adopt the node tree if moving between documents\n    if (adopting_to_new_document) {\n        try page.adoptNodeTree(new_node, parent_owner);\n    }\n\n    try page.insertNodeRelative(\n        self,\n        new_node,\n        .{ .before = ref_node },\n        .{\n            .child_already_connected = child_already_connected,\n            .adopting_to_new_document = adopting_to_new_document,\n        },\n    );\n\n    return new_node;\n}\n\npub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page) !*Node {\n    if (old_child._parent == null or old_child._parent.? != self) {\n        return error.HierarchyError;\n    }\n\n    try validateNodeInsertion(self, new_child);\n\n    _ = try self.insertBefore(new_child, old_child, page);\n\n    // Special case: if we replace a node by itself, we don't remove it.\n    // insertBefore is an noop in this case.\n    if (new_child != old_child) {\n        page.removeNode(self, old_child, .{ .will_be_reconnected = false });\n    }\n\n    return old_child;\n}\n\npub fn getNodeValue(self: *const Node) ?String {\n    return switch (self._type) {\n        .cdata => |c| c.getData(),\n        .attribute => |attr| attr._value,\n        .element => null,\n        .document => null,\n        .document_type => null,\n        .document_fragment => null,\n    };\n}\n\npub fn setNodeValue(self: *const Node, value: ?String, page: *Page) !void {\n    switch (self._type) {\n        // Per spec, setting nodeValue on CharacterData runs replaceData(0, length, value)\n        .cdata => |c| {\n            const new_value: []const u8 = if (value) |v| v.str() else \"\";\n            try c.replaceData(0, c.getLength(), new_value, page);\n        },\n        .attribute => |attr| try attr.setValue(value, page),\n        .element => {},\n        .document => {},\n        .document_type => {},\n        .document_fragment => {},\n    }\n}\n\npub fn format(self: *Node, writer: *std.Io.Writer) !void {\n    // // If you need extra debugging:\n    // return @import(\"../dump.zig\").deep(self, .{}, writer);\n    return switch (self._type) {\n        .cdata => |cd| cd.format(writer),\n        .element => |el| writer.print(\"{f}\", .{el}),\n        .document => writer.writeAll(\"<document>\"),\n        .document_type => writer.writeAll(\"<doctype>\"),\n        .document_fragment => writer.writeAll(\"<document_fragment>\"),\n        .attribute => |attr| writer.print(\"{f}\", .{attr}),\n    };\n}\n\n// Returns an iterator the can be used to iterate through the node's children\n// For internal use.\npub fn childrenIterator(self: *Node) NodeIterator {\n    const children = self._children orelse {\n        return .{ .node = null };\n    };\n\n    return .{\n        .node = children.first(),\n    };\n}\n\npub fn getChildrenCount(self: *Node) usize {\n    return switch (self._type) {\n        .element, .document, .document_fragment => self.getLength(),\n        .document_type, .attribute, .cdata => return 0,\n    };\n}\n\npub fn getLength(self: *Node) u32 {\n    switch (self._type) {\n        .cdata => |cdata| {\n            return @intCast(cdata.getData().len);\n        },\n        .element, .document, .document_fragment => {\n            var count: u32 = 0;\n            var it = self.childrenIterator();\n            while (it.next()) |_| {\n                count += 1;\n            }\n            return count;\n        },\n        .document_type, .attribute => return 0,\n    }\n}\n\npub fn getChildIndex(self: *Node, target: *const Node) ?u32 {\n    var i: u32 = 0;\n    var it = self.childrenIterator();\n    while (it.next()) |child| {\n        if (child == target) {\n            return i;\n        }\n        i += 1;\n    }\n    return null;\n}\n\npub fn getChildAt(self: *Node, index: u32) ?*Node {\n    var i: u32 = 0;\n    var it = self.childrenIterator();\n    while (it.next()) |child| {\n        if (i == index) {\n            return child;\n        }\n        i += 1;\n    }\n    return null;\n}\n\npub fn getData(self: *const Node) String {\n    return switch (self._type) {\n        .cdata => |c| c.getData(),\n        else => .empty,\n    };\n}\n\npub fn setData(self: *Node, data: []const u8, page: *Page) !void {\n    switch (self._type) {\n        .cdata => |c| try c.setData(data, page),\n        else => {},\n    }\n}\n\npub fn normalize(self: *Node, page: *Page) !void {\n    var buffer: std.ArrayList(u8) = .empty;\n    return self._normalize(page.call_arena, &buffer, page);\n}\n\nconst CloneError = error{\n    OutOfMemory,\n    StringTooLarge,\n    NotSupported,\n    NotImplemented,\n    InvalidCharacterError,\n    CloneError,\n    IFrameLoadError,\n    TooManyContexts,\n    LinkLoadError,\n    StyleLoadError,\n    TypeError,\n    CompilationError,\n    JsException,\n};\npub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {\n    const deep = deep_ orelse false;\n    switch (self._type) {\n        .cdata => |cd| {\n            const data = cd.getData().str();\n            return switch (cd._type) {\n                .text => page.createTextNode(data),\n                .cdata_section => page.createCDATASection(data),\n                .comment => page.createComment(data),\n                .processing_instruction => |pi| page.createProcessingInstruction(pi._target, data),\n            };\n        },\n        .element => |el| return el.clone(deep, page),\n        .document => return error.NotSupported,\n        .document_type => |dt| {\n            const cloned = dt.clone(page) catch return error.CloneError;\n            return cloned.asNode();\n        },\n        .document_fragment => |frag| return frag.cloneFragment(deep, page),\n        .attribute => |attr| {\n            const cloned = attr.clone(page) catch return error.CloneError;\n            return cloned._proto;\n        },\n    }\n}\n\n/// Clone a node for the purpose of appending to a parent.\n/// Returns null if the cloned node was already attached somewhere by a custom element\n/// constructor, indicating that the constructor's decision should be respected.\n///\n/// This helper is used when iterating over children to clone them. The typical pattern is:\n///   while (child_it.next()) |child| {\n///       if (try child.cloneNodeForAppending(true, page)) |cloned| {\n///           try page.appendNode(parent, cloned, opts);\n///       }\n///   }\n///\n/// The only case where a cloned node would already have a parent is when a custom element\n/// constructor (which runs during cloning per the HTML spec) explicitly attaches the element\n/// somewhere. In that case, we respect the constructor's decision and return null to signal\n/// that the cloned node should not be appended to our intended parent.\npub fn cloneNodeForAppending(self: *Node, deep: bool, page: *Page) CloneError!?*Node {\n    const cloned = try self.cloneNode(deep, page);\n    if (cloned._parent != null) {\n        return null;\n    }\n    return cloned;\n}\n\npub fn compareDocumentPosition(self: *Node, other: *Node) u16 {\n    const DISCONNECTED: u16 = 0x01;\n    const PRECEDING: u16 = 0x02;\n    const FOLLOWING: u16 = 0x04;\n    const CONTAINS: u16 = 0x08;\n    const CONTAINED_BY: u16 = 0x10;\n    const IMPLEMENTATION_SPECIFIC: u16 = 0x20;\n\n    if (self == other) {\n        return 0;\n    }\n\n    // Check if either node is disconnected\n    const self_root = self.getRootNode(.{});\n    const other_root = other.getRootNode(.{});\n\n    if (self_root != other_root) {\n        // Nodes are in different trees - disconnected\n        // Use pointer comparison for implementation-specific ordering\n        return DISCONNECTED | IMPLEMENTATION_SPECIFIC | if (@intFromPtr(self) < @intFromPtr(other)) FOLLOWING else PRECEDING;\n    }\n\n    // Check if one contains the other\n    if (self.contains(other)) {\n        return FOLLOWING | CONTAINED_BY;\n    }\n\n    if (other.contains(self)) {\n        return PRECEDING | CONTAINS;\n    }\n\n    // Neither contains the other - find common ancestor and compare positions\n    // Walk up from self to build ancestor chain\n    var self_ancestors: [256]*const Node = undefined;\n    var ancestor_count: usize = 0;\n    var current: ?*const Node = self;\n    while (current) |node| : (current = node._parent) {\n        if (ancestor_count >= self_ancestors.len) break;\n        self_ancestors[ancestor_count] = node;\n        ancestor_count += 1;\n    }\n\n    const ancestors = self_ancestors[0..ancestor_count];\n\n    // Walk up from other until we find common ancestor\n    current = other;\n    while (current) |node| : (current = node._parent) {\n        // Check if this node is in self's ancestor chain\n        for (ancestors, 0..) |ancestor, i| {\n            if (ancestor != node) {\n                continue;\n            }\n\n            // Found common ancestor\n            // Compare the children that are ancestors of self and other\n            if (i == 0) {\n                // self is directly under the common ancestor\n                // Find other's ancestor that's a child of the common ancestor\n                if (other == node) {\n                    // other is the common ancestor, so self follows it\n                    return FOLLOWING;\n                }\n                var other_ancestor = other;\n                while (other_ancestor._parent) |p| {\n                    if (p == node) break;\n                    other_ancestor = p;\n                }\n                return if (isNodeBefore(self, other_ancestor)) FOLLOWING else PRECEDING;\n            }\n\n            const self_ancestor = self_ancestors[i - 1];\n            // Find other's ancestor that's a child of the common ancestor\n            var other_ancestor = other;\n            if (other == node) {\n                // other is the common ancestor, so self is contained by it\n                return PRECEDING | CONTAINS;\n            }\n            while (other_ancestor._parent) |p| {\n                if (p == node) break;\n                other_ancestor = p;\n            }\n            return if (isNodeBefore(self_ancestor, other_ancestor)) FOLLOWING else PRECEDING;\n        }\n    }\n\n    // Shouldn't reach here if both nodes are in the same tree\n    return DISCONNECTED;\n}\n\n// faster to compare the linked list node links directly\nfn isNodeBefore(node1: *const Node, node2: *const Node) bool {\n    var current = node1._child_link.next;\n    const target = &node2._child_link;\n    while (current) |link| {\n        if (link == target) return true;\n        current = link.next;\n    }\n    return false;\n}\n\nfn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), page: *Page) !void {\n    var it = self.childrenIterator();\n    while (it.next()) |child| {\n        try child._normalize(allocator, buffer, page);\n    }\n\n    var child = self.firstChild();\n    while (child) |current_node| {\n        var next_node = current_node.nextSibling();\n\n        const text_node = current_node.is(CData.Text) orelse {\n            child = next_node;\n            continue;\n        };\n\n        if (text_node._proto.getData().len == 0) {\n            page.removeNode(self, current_node, .{ .will_be_reconnected = false });\n            child = next_node;\n            continue;\n        }\n\n        if (next_node) |next| {\n            if (next.is(CData.Text)) |_| {\n                try buffer.appendSlice(allocator, text_node.getWholeText());\n\n                while (next_node) |node_to_merge| {\n                    const next_text_node = node_to_merge.is(CData.Text) orelse break;\n                    try buffer.appendSlice(allocator, next_text_node.getWholeText());\n\n                    const to_remove = node_to_merge;\n                    next_node = node_to_merge.nextSibling();\n                    page.removeNode(self, to_remove, .{ .will_be_reconnected = false });\n                }\n                text_node._proto._data = try page.dupeSSO(buffer.items);\n                buffer.clearRetainingCapacity();\n            }\n        }\n\n        child = next_node;\n    }\n}\n\npub const GetElementsByTagNameResult = union(enum) {\n    tag: collections.NodeLive(.tag),\n    tag_name: collections.NodeLive(.tag_name),\n    all_elements: collections.NodeLive(.all_elements),\n};\n// Not exposed in the WebAPI, but used by both Element and Document\npub fn getElementsByTagName(self: *Node, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {\n    if (tag_name.len > 256) {\n        // 256 seems generous.\n        return error.InvalidTagName;\n    }\n\n    if (std.mem.eql(u8, tag_name, \"*\")) {\n        return .{\n            .all_elements = collections.NodeLive(.all_elements).init(self, {}, page),\n        };\n    }\n\n    const lower = std.ascii.lowerString(&page.buf, tag_name);\n    if (Node.Element.Tag.parseForMatch(lower)) |known| {\n        // optimized for known tag names, comparis\n        return .{\n            .tag = collections.NodeLive(.tag).init(self, known, page),\n        };\n    }\n\n    const arena = page.arena;\n    const filter = try String.init(arena, lower, .{});\n    return .{ .tag_name = collections.NodeLive(.tag_name).init(self, filter, page) };\n}\n\n// Not exposed in the WebAPI, but used by both Element and Document\npub fn getElementsByTagNameNS(self: *Node, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {\n    if (local_name.len > 256) {\n        return error.InvalidTagName;\n    }\n\n    // Parse namespace - \"*\" means wildcard (null), null means Element.Namespace.null\n    const ns: ?Element.Namespace = if (namespace) |ns_str|\n        if (std.mem.eql(u8, ns_str, \"*\")) null else Element.Namespace.parse(ns_str)\n    else\n        Element.Namespace.null;\n\n    return collections.NodeLive(.tag_name_ns).init(self, .{\n        .namespace = ns,\n        .local_name = try String.init(page.arena, local_name, .{}),\n    }, page);\n}\n\n// Not exposed in the WebAPI, but used by both Element and Document\npub fn getElementsByClassName(self: *Node, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {\n    const arena = page.arena;\n\n    // Parse space-separated class names\n    var class_names: std.ArrayList([]const u8) = .empty;\n    var it = std.mem.tokenizeAny(u8, class_name, \"\\t\\n\\x0C\\r \");\n    while (it.next()) |name| {\n        try class_names.append(arena, try page.dupeString(name));\n    }\n\n    return collections.NodeLive(.class_name).init(self, class_names.items, page);\n}\n\n// Writes a JSON representation of the node and its children\npub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {\n    // stupid json api requires this to be const,\n    // so we @constCast it because our stringify re-uses code that can be\n    // used to iterate nodes, e.g. the NodeIterator\n    return @import(\"../dump.zig\").toJSON(@constCast(self), writer);\n}\n\nconst NodeIterator = struct {\n    node: ?*Node,\n    pub fn next(self: *NodeIterator) ?*Node {\n        const node = self.node orelse return null;\n        self.node = linkToNodeOrNull(node._child_link.next);\n        return node;\n    }\n};\n\n// Turns a linked list node into a Node\npub fn linkToNode(n: *LinkedList.Node) *Node {\n    return @fieldParentPtr(\"_child_link\", n);\n}\n\npub fn linkToNodeOrNull(n_: ?*LinkedList.Node) ?*Node {\n    return if (n_) |n| linkToNode(n) else null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Node);\n\n    pub const Meta = struct {\n        pub const name = \"Node\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const ELEMENT_NODE = bridge.property(1, .{ .template = true });\n    pub const ATTRIBUTE_NODE = bridge.property(2, .{ .template = true });\n    pub const TEXT_NODE = bridge.property(3, .{ .template = true });\n    pub const CDATA_SECTION_NODE = bridge.property(4, .{ .template = true });\n    pub const ENTITY_REFERENCE_NODE = bridge.property(5, .{ .template = true });\n    pub const ENTITY_NODE = bridge.property(6, .{ .template = true });\n    pub const PROCESSING_INSTRUCTION_NODE = bridge.property(7, .{ .template = true });\n    pub const COMMENT_NODE = bridge.property(8, .{ .template = true });\n    pub const DOCUMENT_NODE = bridge.property(9, .{ .template = true });\n    pub const DOCUMENT_TYPE_NODE = bridge.property(10, .{ .template = true });\n    pub const DOCUMENT_FRAGMENT_NODE = bridge.property(11, .{ .template = true });\n    pub const NOTATION_NODE = bridge.property(12, .{ .template = true });\n\n    pub const DOCUMENT_POSITION_DISCONNECTED = bridge.property(0x01, .{ .template = true });\n    pub const DOCUMENT_POSITION_PRECEDING = bridge.property(0x02, .{ .template = true });\n    pub const DOCUMENT_POSITION_FOLLOWING = bridge.property(0x04, .{ .template = true });\n    pub const DOCUMENT_POSITION_CONTAINS = bridge.property(0x08, .{ .template = true });\n    pub const DOCUMENT_POSITION_CONTAINED_BY = bridge.property(0x10, .{ .template = true });\n    pub const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = bridge.property(0x20, .{ .template = true });\n\n    pub const nodeName = bridge.accessor(struct {\n        fn wrap(self: *const Node, page: *Page) []const u8 {\n            return self.getNodeName(&page.buf);\n        }\n    }.wrap, null, .{});\n    pub const nodeType = bridge.accessor(Node.getNodeType, null, .{});\n\n    pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{});\n    fn _textContext(self: *Node, page: *const Page) !?[]const u8 {\n        // cdata and attributes can return value directly, avoiding the copy\n        switch (self._type) {\n            .element, .document_fragment => {\n                var buf = std.Io.Writer.Allocating.init(page.call_arena);\n                try self.getTextContent(&buf.writer);\n                return buf.written();\n            },\n            .cdata => |cdata| return cdata._data.str(),\n            .attribute => |attr| return attr._value.str(),\n            .document => return null,\n            .document_type => return null,\n        }\n    }\n\n    pub const firstChild = bridge.accessor(Node.firstChild, null, .{});\n    pub const lastChild = bridge.accessor(Node.lastChild, null, .{});\n    pub const nextSibling = bridge.accessor(Node.nextSibling, null, .{});\n    pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{});\n    pub const parentNode = bridge.accessor(Node.parentNode, null, .{});\n    pub const parentElement = bridge.accessor(Node.parentElement, null, .{});\n    pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true });\n    pub const childNodes = bridge.accessor(Node.childNodes, null, .{ .cache = .{ .private = \"child_nodes\" } });\n    pub const isConnected = bridge.accessor(Node.isConnected, null, .{});\n    pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{});\n    pub const hasChildNodes = bridge.function(Node.hasChildNodes, .{});\n    pub const isSameNode = bridge.function(Node.isSameNode, .{});\n    pub const contains = bridge.function(Node.contains, .{});\n    pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true });\n    pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{});\n    pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true });\n    pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true });\n    pub const normalize = bridge.function(Node.normalize, .{});\n    pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true });\n    pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{});\n    pub const getRootNode = bridge.function(Node.getRootNode, .{});\n    pub const isEqualNode = bridge.function(Node.isEqualNode, .{});\n    pub const lookupNamespaceURI = bridge.function(Node.lookupNamespaceURI, .{});\n    pub const isDefaultNamespace = bridge.function(Node.isDefaultNamespace, .{});\n\n    fn _baseURI(_: *Node, page: *const Page) []const u8 {\n        return page.base();\n    }\n    pub const baseURI = bridge.accessor(_baseURI, null, .{});\n};\n\npub const Build = struct {\n    // Calls `func_name` with `args` on the most specific type where it is\n    // implement. This could be on the Node itself (as a last-resort);\n    pub fn call(self: *const Node, comptime func_name: []const u8, args: anytype) !void {\n        inline for (@typeInfo(Node.Type).@\"union\".fields) |f| {\n            // The inner type has its own \"call\" method. Defer to it.\n            if (@field(Node.Type, f.name) == self._type) {\n                const S = reflect.Struct(f.type);\n                if (@hasDecl(S, \"Build\")) {\n                    if (@hasDecl(S.Build, \"call\")) {\n                        const sub = @field(self._type, f.name);\n                        if (try S.Build.call(sub, func_name, args)) {\n                            return;\n                        }\n                    }\n                    // The inner type implements this function. Call it and we're done.\n                    if (@hasDecl(S, func_name)) {\n                        return @call(.auto, @field(f.type, func_name), args);\n                    }\n                }\n            }\n        }\n\n        if (@hasDecl(Node.Build, func_name)) {\n            // Our last resort - the node implements this function.\n            return @call(.auto, @field(Node.Build, func_name), args);\n        }\n    }\n};\n\npub const NodeOrText = union(enum) {\n    node: *Node,\n    text: []const u8,\n\n    pub fn format(self: *const NodeOrText, writer: *std.io.Writer) !void {\n        switch (self.*) {\n            .node => |n| try n.format(writer),\n            .text => |text| {\n                try writer.writeByte('\\'');\n                try writer.writeAll(text);\n                try writer.writeByte('\\'');\n            },\n        }\n    }\n\n    pub fn toNode(self: *const NodeOrText, page: *Page) !*Node {\n        return switch (self.*) {\n            .node => |n| n,\n            .text => |txt| page.createTextNode(txt),\n        };\n    }\n\n    /// DOM spec: first following sibling of `node` that is not in `nodes`.\n    pub fn viableNextSibling(node: *Node, nodes: []const NodeOrText) ?*Node {\n        var sibling = node.nextSibling() orelse return null;\n        blk: while (true) {\n            for (nodes) |n| {\n                switch (n) {\n                    .node => |nn| if (sibling == nn) {\n                        sibling = sibling.nextSibling() orelse return null;\n                        continue :blk;\n                    },\n                    .text => {},\n                }\n            } else {\n                return sibling;\n            }\n        }\n        return null;\n    }\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Node\" {\n    try testing.htmlRunner(\"node\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/NodeFilter.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Node = @import(\"Node.zig\");\n\nconst NodeFilter = @This();\n\n_func: ?js.Function.Global,\n_original_filter: ?FilterOpts,\n\npub const FilterOpts = union(enum) {\n    function: js.Function.Global,\n    object: struct {\n        pub const js_as_object = true;\n        acceptNode: js.Function.Global,\n    },\n};\n\npub fn init(opts_: ?FilterOpts) !NodeFilter {\n    const opts = opts_ orelse return .{ ._func = null, ._original_filter = null };\n    const func = switch (opts) {\n        .function => |func| func,\n        .object => |obj| obj.acceptNode,\n    };\n    return .{\n        ._func = func,\n        ._original_filter = opts_,\n    };\n}\n\n// Constants\npub const FILTER_ACCEPT: i32 = 1;\npub const FILTER_REJECT: i32 = 2;\npub const FILTER_SKIP: i32 = 3;\n\n// whatToShow constants\npub const SHOW_ALL: u32 = 0xFFFFFFFF;\npub const SHOW_ELEMENT: u32 = 0x1;\npub const SHOW_ATTRIBUTE: u32 = 0x2;\npub const SHOW_TEXT: u32 = 0x4;\npub const SHOW_CDATA_SECTION: u32 = 0x8;\npub const SHOW_ENTITY_REFERENCE: u32 = 0x10;\npub const SHOW_ENTITY: u32 = 0x20;\npub const SHOW_PROCESSING_INSTRUCTION: u32 = 0x40;\npub const SHOW_COMMENT: u32 = 0x80;\npub const SHOW_DOCUMENT: u32 = 0x100;\npub const SHOW_DOCUMENT_TYPE: u32 = 0x200;\npub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400;\npub const SHOW_NOTATION: u32 = 0x800;\n\npub fn acceptNode(self: *const NodeFilter, node: *Node, local: *const js.Local) !i32 {\n    const func = self._func orelse return FILTER_ACCEPT;\n    return local.toLocal(func).callRethrow(i32, .{node});\n}\n\npub fn shouldShow(node: *const Node, what_to_show: u32) bool {\n    // TODO: Test this mapping thoroughly!\n    // nodeType values (1=ELEMENT, 3=TEXT, 9=DOCUMENT, etc.) need to map to\n    // SHOW_* bitmask positions (0x1, 0x4, 0x100, etc.)\n    const node_type_value = node.getNodeType();\n    const bit_position = node_type_value - 1;\n    const node_type_bit: u32 = @as(u32, 1) << @intCast(bit_position);\n    return (what_to_show & node_type_bit) != 0;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(NodeFilter);\n\n    pub const Meta = struct {\n        pub const name = \"NodeFilter\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n        pub const enumerable = false;\n    };\n\n    pub const FILTER_ACCEPT = bridge.property(NodeFilter.FILTER_ACCEPT, .{ .template = true });\n    pub const FILTER_REJECT = bridge.property(NodeFilter.FILTER_REJECT, .{ .template = true });\n    pub const FILTER_SKIP = bridge.property(NodeFilter.FILTER_SKIP, .{ .template = true });\n\n    pub const SHOW_ALL = bridge.property(NodeFilter.SHOW_ALL, .{ .template = true });\n    pub const SHOW_ELEMENT = bridge.property(NodeFilter.SHOW_ELEMENT, .{ .template = true });\n    pub const SHOW_ATTRIBUTE = bridge.property(NodeFilter.SHOW_ATTRIBUTE, .{ .template = true });\n    pub const SHOW_TEXT = bridge.property(NodeFilter.SHOW_TEXT, .{ .template = true });\n    pub const SHOW_CDATA_SECTION = bridge.property(NodeFilter.SHOW_CDATA_SECTION, .{ .template = true });\n    pub const SHOW_ENTITY_REFERENCE = bridge.property(NodeFilter.SHOW_ENTITY_REFERENCE, .{ .template = true });\n    pub const SHOW_ENTITY = bridge.property(NodeFilter.SHOW_ENTITY, .{ .template = true });\n    pub const SHOW_PROCESSING_INSTRUCTION = bridge.property(NodeFilter.SHOW_PROCESSING_INSTRUCTION, .{ .template = true });\n    pub const SHOW_COMMENT = bridge.property(NodeFilter.SHOW_COMMENT, .{ .template = true });\n    pub const SHOW_DOCUMENT = bridge.property(NodeFilter.SHOW_DOCUMENT, .{ .template = true });\n    pub const SHOW_DOCUMENT_TYPE = bridge.property(NodeFilter.SHOW_DOCUMENT_TYPE, .{ .template = true });\n    pub const SHOW_DOCUMENT_FRAGMENT = bridge.property(NodeFilter.SHOW_DOCUMENT_FRAGMENT, .{ .template = true });\n    pub const SHOW_NOTATION = bridge.property(NodeFilter.SHOW_NOTATION, .{ .template = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/Performance.zig",
    "content": "const js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst datetime = @import(\"../../datetime.zig\");\n\npub fn registerTypes() []const type {\n    return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation };\n}\n\nconst std = @import(\"std\");\n\nconst Performance = @This();\n\n_time_origin: u64,\n_entries: std.ArrayList(*Entry) = .{},\n_timing: PerformanceTiming = .{},\n_navigation: PerformanceNavigation = .{},\n\n/// Get high-resolution timestamp in microseconds, rounded to 5μs increments\n/// to match browser behavior (prevents fingerprinting)\nfn highResTimestamp() u64 {\n    const ts = datetime.timespec();\n    const micros = @as(u64, @intCast(ts.sec)) * 1_000_000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000)));\n    // Round to nearest 5 microseconds (like Firefox default)\n    const rounded = @divTrunc(micros + 2, 5) * 5;\n    return rounded;\n}\n\npub fn init() Performance {\n    return .{\n        ._time_origin = highResTimestamp(),\n        ._entries = .{},\n        ._timing = .{},\n        ._navigation = .{},\n    };\n}\n\npub fn getTiming(self: *Performance) *PerformanceTiming {\n    return &self._timing;\n}\n\npub fn now(self: *const Performance) f64 {\n    const current = highResTimestamp();\n    const elapsed = current - self._time_origin;\n    // Return as milliseconds with microsecond precision\n    return @as(f64, @floatFromInt(elapsed)) / 1000.0;\n}\n\npub fn getTimeOrigin(self: *const Performance) f64 {\n    // Return as milliseconds\n    return @as(f64, @floatFromInt(self._time_origin)) / 1000.0;\n}\n\npub fn getNavigation(self: *Performance) *PerformanceNavigation {\n    return &self._navigation;\n}\n\npub fn mark(\n    self: *Performance,\n    name: []const u8,\n    _options: ?Mark.Options,\n    page: *Page,\n) !*Mark {\n    const m = try Mark.init(name, _options, page);\n    try self._entries.append(page.arena, m._proto);\n    // Notify about the change.\n    try page.notifyPerformanceObservers(m._proto);\n    return m;\n}\n\nconst MeasureOptionsOrStartMark = union(enum) {\n    measure_options: Measure.Options,\n    start_mark: []const u8,\n};\n\npub fn measure(\n    self: *Performance,\n    name: []const u8,\n    maybe_options_or_start: ?MeasureOptionsOrStartMark,\n    maybe_end_mark: ?[]const u8,\n    page: *Page,\n) !*Measure {\n    if (maybe_options_or_start) |options_or_start| switch (options_or_start) {\n        .measure_options => |options| {\n            // Get start timestamp.\n            const start_timestamp = blk: {\n                if (options.start) |timestamp_or_mark| {\n                    break :blk switch (timestamp_or_mark) {\n                        .timestamp => |timestamp| timestamp,\n                        .mark => |mark_name| try self.getMarkTime(mark_name),\n                    };\n                }\n\n                break :blk 0.0;\n            };\n\n            // Get end timestamp.\n            const end_timestamp = blk: {\n                if (options.end) |timestamp_or_mark| {\n                    break :blk switch (timestamp_or_mark) {\n                        .timestamp => |timestamp| timestamp,\n                        .mark => |mark_name| try self.getMarkTime(mark_name),\n                    };\n                }\n\n                break :blk self.now();\n            };\n\n            const m = try Measure.init(\n                name,\n                options.detail,\n                start_timestamp,\n                end_timestamp,\n                options.duration,\n                page,\n            );\n            try self._entries.append(page.arena, m._proto);\n            // Notify about the change.\n            try page.notifyPerformanceObservers(m._proto);\n            return m;\n        },\n        .start_mark => |start_mark| {\n            // Get start timestamp.\n            const start_timestamp = try self.getMarkTime(start_mark);\n            // Get end timestamp.\n            const end_timestamp = blk: {\n                if (maybe_end_mark) |mark_name| {\n                    break :blk try self.getMarkTime(mark_name);\n                }\n\n                break :blk self.now();\n            };\n\n            const m = try Measure.init(\n                name,\n                null,\n                start_timestamp,\n                end_timestamp,\n                null,\n                page,\n            );\n            try self._entries.append(page.arena, m._proto);\n            // Notify about the change.\n            try page.notifyPerformanceObservers(m._proto);\n            return m;\n        },\n    };\n\n    const m = try Measure.init(name, null, 0.0, self.now(), null, page);\n    try self._entries.append(page.arena, m._proto);\n    // Notify about the change.\n    try page.notifyPerformanceObservers(m._proto);\n    return m;\n}\n\npub fn clearMarks(self: *Performance, mark_name: ?[]const u8) void {\n    var i: usize = 0;\n    while (i < self._entries.items.len) {\n        const entry = self._entries.items[i];\n        if (entry._type == .mark and (mark_name == null or std.mem.eql(u8, entry._name, mark_name.?))) {\n            _ = self._entries.orderedRemove(i);\n        } else {\n            i += 1;\n        }\n    }\n}\n\npub fn clearMeasures(self: *Performance, measure_name: ?[]const u8) void {\n    var i: usize = 0;\n    while (i < self._entries.items.len) {\n        const entry = self._entries.items[i];\n        if (entry._type == .measure and (measure_name == null or std.mem.eql(u8, entry._name, measure_name.?))) {\n            _ = self._entries.orderedRemove(i);\n        } else {\n            i += 1;\n        }\n    }\n}\n\npub fn getEntries(self: *const Performance) []*Entry {\n    return self._entries.items;\n}\n\npub fn getEntriesByType(self: *const Performance, entry_type: []const u8, page: *Page) ![]const *Entry {\n    var result: std.ArrayList(*Entry) = .empty;\n\n    for (self._entries.items) |entry| {\n        if (std.mem.eql(u8, entry.getEntryType(), entry_type)) {\n            try result.append(page.call_arena, entry);\n        }\n    }\n\n    return result.items;\n}\n\npub fn getEntriesByName(self: *const Performance, name: []const u8, entry_type: ?[]const u8, page: *Page) ![]const *Entry {\n    var result: std.ArrayList(*Entry) = .empty;\n\n    for (self._entries.items) |entry| {\n        if (!std.mem.eql(u8, entry._name, name)) {\n            continue;\n        }\n\n        const et = entry_type orelse {\n            try result.append(page.call_arena, entry);\n            continue;\n        };\n\n        if (std.mem.eql(u8, entry.getEntryType(), et)) {\n            try result.append(page.call_arena, entry);\n        }\n    }\n\n    return result.items;\n}\n\nfn getMarkTime(self: *const Performance, mark_name: []const u8) !f64 {\n    for (self._entries.items) |entry| {\n        if (entry._type == .mark and std.mem.eql(u8, entry._name, mark_name)) {\n            return entry._start_time;\n        }\n    }\n\n    // PerformanceTiming attribute names are valid start/end marks per the\n    // W3C User Timing Level 2 spec. All are relative to navigationStart (= 0).\n    // https://www.w3.org/TR/user-timing/#dom-performance-measure\n    //\n    // `navigationStart` is an equivalent to 0.\n    // Others are dependant to request arrival, end of request etc, but we\n    // return a dummy 0 value for now.\n    const navigation_timing_marks = std.StaticStringMap(void).initComptime(.{\n        .{ \"navigationStart\", {} },\n        .{ \"unloadEventStart\", {} },\n        .{ \"unloadEventEnd\", {} },\n        .{ \"redirectStart\", {} },\n        .{ \"redirectEnd\", {} },\n        .{ \"fetchStart\", {} },\n        .{ \"domainLookupStart\", {} },\n        .{ \"domainLookupEnd\", {} },\n        .{ \"connectStart\", {} },\n        .{ \"connectEnd\", {} },\n        .{ \"secureConnectionStart\", {} },\n        .{ \"requestStart\", {} },\n        .{ \"responseStart\", {} },\n        .{ \"responseEnd\", {} },\n        .{ \"domLoading\", {} },\n        .{ \"domInteractive\", {} },\n        .{ \"domContentLoadedEventStart\", {} },\n        .{ \"domContentLoadedEventEnd\", {} },\n        .{ \"domComplete\", {} },\n        .{ \"loadEventStart\", {} },\n        .{ \"loadEventEnd\", {} },\n    });\n    if (navigation_timing_marks.has(mark_name)) {\n        return 0;\n    }\n\n    return error.SyntaxError; // Mark not found\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Performance);\n\n    pub const Meta = struct {\n        pub const name = \"Performance\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const now = bridge.function(Performance.now, .{});\n    pub const mark = bridge.function(Performance.mark, .{});\n    pub const measure = bridge.function(Performance.measure, .{ .dom_exception = true });\n    pub const clearMarks = bridge.function(Performance.clearMarks, .{});\n    pub const clearMeasures = bridge.function(Performance.clearMeasures, .{});\n    pub const getEntries = bridge.function(Performance.getEntries, .{});\n    pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{});\n    pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{});\n    pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});\n    pub const timing = bridge.accessor(Performance.getTiming, null, .{});\n    pub const navigation = bridge.accessor(Performance.getNavigation, null, .{});\n};\n\npub const Entry = struct {\n    _duration: f64 = 0.0,\n    _type: Type,\n    _name: []const u8,\n    _start_time: f64 = 0.0,\n\n    pub const Type = union(Enum) {\n        element,\n        event,\n        first_input,\n        @\"largest-contentful-paint\",\n        @\"layout-shift\",\n        @\"long-animation-frame\",\n        longtask,\n        measure: *Measure,\n        navigation,\n        paint,\n        resource,\n        taskattribution,\n        @\"visibility-state\",\n        mark: *Mark,\n\n        pub const Enum = enum(u8) {\n            element = 1, // Changing this affect PerformanceObserver's behavior.\n            event = 2,\n            first_input = 3,\n            @\"largest-contentful-paint\" = 4,\n            @\"layout-shift\" = 5,\n            @\"long-animation-frame\" = 6,\n            longtask = 7,\n            measure = 8,\n            navigation = 9,\n            paint = 10,\n            resource = 11,\n            taskattribution = 12,\n            @\"visibility-state\" = 13,\n            mark = 14,\n            // If we ever have types more than 16, we have to update entry\n            // table of PerformanceObserver too.\n        };\n    };\n\n    pub fn getDuration(self: *const Entry) f64 {\n        return self._duration;\n    }\n\n    pub fn getEntryType(self: *const Entry) []const u8 {\n        return switch (self._type) {\n            else => |t| @tagName(t),\n        };\n    }\n\n    pub fn getName(self: *const Entry) []const u8 {\n        return self._name;\n    }\n\n    pub fn getStartTime(self: *const Entry) f64 {\n        return self._start_time;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(Entry);\n\n        pub const Meta = struct {\n            pub const name = \"PerformanceEntry\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n        pub const name = bridge.accessor(Entry.getName, null, .{});\n        pub const duration = bridge.accessor(Entry.getDuration, null, .{});\n        pub const entryType = bridge.accessor(Entry.getEntryType, null, .{});\n        pub const startTime = bridge.accessor(Entry.getStartTime, null, .{});\n    };\n};\n\npub const Mark = struct {\n    _proto: *Entry,\n    _detail: ?js.Value.Global,\n\n    const Options = struct {\n        detail: ?js.Value = null,\n        startTime: ?f64 = null,\n    };\n\n    pub fn init(name: []const u8, _opts: ?Options, page: *Page) !*Mark {\n        const opts = _opts orelse Options{};\n        const start_time = opts.startTime orelse page.window._performance.now();\n\n        if (start_time < 0.0) {\n            return error.TypeError;\n        }\n\n        const detail = if (opts.detail) |d| try d.persist() else null;\n        const m = try page._factory.create(Mark{\n            ._proto = undefined,\n            ._detail = detail,\n        });\n\n        const entry = try page._factory.create(Entry{\n            ._start_time = start_time,\n            ._name = try page.dupeString(name),\n            ._type = .{ .mark = m },\n        });\n        m._proto = entry;\n        return m;\n    }\n\n    pub fn getDetail(self: *const Mark) ?js.Value.Global {\n        return self._detail;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(Mark);\n\n        pub const Meta = struct {\n            pub const name = \"PerformanceMark\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n        pub const detail = bridge.accessor(Mark.getDetail, null, .{});\n    };\n};\n\npub const Measure = struct {\n    _proto: *Entry,\n    _detail: ?js.Value.Global,\n\n    const Options = struct {\n        detail: ?js.Value = null,\n        start: ?TimestampOrMark,\n        end: ?TimestampOrMark,\n        duration: ?f64 = null,\n\n        const TimestampOrMark = union(enum) {\n            timestamp: f64,\n            mark: []const u8,\n        };\n    };\n\n    pub fn init(\n        name: []const u8,\n        maybe_detail: ?js.Value,\n        start_timestamp: f64,\n        end_timestamp: f64,\n        maybe_duration: ?f64,\n        page: *Page,\n    ) !*Measure {\n        const duration = maybe_duration orelse (end_timestamp - start_timestamp);\n        if (duration < 0.0) {\n            return error.TypeError;\n        }\n\n        const detail = if (maybe_detail) |d| try d.persist() else null;\n        const m = try page._factory.create(Measure{\n            ._proto = undefined,\n            ._detail = detail,\n        });\n\n        const entry = try page._factory.create(Entry{\n            ._start_time = start_timestamp,\n            ._duration = duration,\n            ._name = try page.dupeString(name),\n            ._type = .{ .measure = m },\n        });\n        m._proto = entry;\n        return m;\n    }\n\n    pub fn getDetail(self: *const Measure) ?js.Value.Global {\n        return self._detail;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(Measure);\n\n        pub const Meta = struct {\n            pub const name = \"PerformanceMeasure\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n        pub const detail = bridge.accessor(Measure.getDetail, null, .{});\n    };\n};\n\n/// PerformanceTiming — Navigation Timing Level 1 (legacy, but widely used).\n/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming\n/// All properties return 0 as stub values; the object must not be undefined\n/// so that scripts accessing performance.timing.navigationStart don't crash.\npub const PerformanceTiming = struct {\n    // Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n    _pad: bool = false,\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(PerformanceTiming);\n\n        pub const Meta = struct {\n            pub const name = \"PerformanceTiming\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n            pub const empty_with_no_proto = true;\n        };\n\n        pub const navigationStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const unloadEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const unloadEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const redirectStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const redirectEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const fetchStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domainLookupStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domainLookupEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const connectStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const connectEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const secureConnectionStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const requestStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const responseStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const responseEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domLoading = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domInteractive = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domContentLoadedEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domContentLoadedEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const domComplete = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const loadEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const loadEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });\n    };\n};\n\n// PerformanceNavigation implements the Navigation Timing Level 1 API.\n// https://www.w3.org/TR/navigation-timing/#sec-navigation-navigation-timing-interface\n// Stub implementation — returns 0 for type (TYPE_NAVIGATE) and 0 for redirectCount.\npub const PerformanceNavigation = struct {\n    // Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n    _pad: bool = false,\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(PerformanceNavigation);\n\n        pub const Meta = struct {\n            pub const name = \"PerformanceNavigation\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n            pub const empty_with_no_proto = true;\n        };\n\n        pub const @\"type\" = bridge.property(0.0, .{ .template = false, .readonly = true });\n        pub const redirectCount = bridge.property(0.0, .{ .template = false, .readonly = true });\n    };\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Performance\" {\n    try testing.htmlRunner(\"performance.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/PerformanceObserver.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../js/js.zig\");\nconst log = @import(\"../../log.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Performance = @import(\"Performance.zig\");\n\npub fn registerTypes() []const type {\n    return &.{ PerformanceObserver, EntryList };\n}\n\n/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver\nconst PerformanceObserver = @This();\n\n/// Emitted when there are events with same interests.\n_callback: js.Function.Global,\n/// The threshold to deliver `PerformanceEventTiming` entries.\n_duration_threshold: f64,\n/// Entry types we're looking for are encoded as bit flags.\n_interests: u16,\n/// Entries this observer hold.\n/// Don't mutate these; other observers may hold pointers to them.\n_entries: std.ArrayList(*Performance.Entry),\n\nconst DefaultDurationThreshold: f64 = 104;\n\n/// Creates a new PerformanceObserver object with the given observer callback.\npub fn init(callback: js.Function.Global, page: *Page) !*PerformanceObserver {\n    return page._factory.create(PerformanceObserver{\n        ._callback = callback,\n        ._duration_threshold = DefaultDurationThreshold,\n        ._interests = 0,\n        ._entries = .{},\n    });\n}\n\nconst ObserveOptions = struct {\n    buffered: bool = false,\n    durationThreshold: f64 = DefaultDurationThreshold,\n    entryTypes: ?[]const []const u8 = null,\n    type: ?[]const u8 = null,\n};\n\n/// TODO: Support `buffered` option.\npub fn observe(\n    self: *PerformanceObserver,\n    maybe_options: ?ObserveOptions,\n    page: *Page,\n) !void {\n    const options: ObserveOptions = maybe_options orelse .{};\n    // Update threshold.\n    self._duration_threshold = @max(@floor(options.durationThreshold / 8) * 8, 16);\n\n    const entry_types: []const []const u8 = blk: {\n        // More likely.\n        if (options.type) |entry_type| {\n            // Can't have both.\n            if (options.entryTypes != null) {\n                return error.TypeError;\n            }\n\n            break :blk &.{entry_type};\n        }\n\n        if (options.entryTypes) |entry_types| {\n            break :blk entry_types;\n        }\n\n        return error.TypeError;\n    };\n\n    // Update entries.\n    var interests: u16 = 0;\n    for (entry_types) |entry_type| {\n        const fields = @typeInfo(Performance.Entry.Type.Enum).@\"enum\".fields;\n        inline for (fields) |field| {\n            if (std.mem.eql(u8, field.name, entry_type)) {\n                const flag = @as(u16, 1) << @as(u16, field.value);\n                interests |= flag;\n            }\n        }\n    }\n\n    // Nothing has updated; no need to go further.\n    if (interests == 0) {\n        return;\n    }\n\n    // If we had no interests before, it means Page is not aware of\n    // this observer.\n    if (self._interests == 0) {\n        try page.registerPerformanceObserver(self);\n    }\n\n    // Update interests.\n    self._interests = interests;\n\n    // Deliver existing entries if buffered option is set.\n    // Per spec, buffered is only valid with the type option, not entryTypes.\n    // Delivery is async via a queued task, not synchronous.\n    if (options.buffered and options.type != null and !self.hasRecords()) {\n        for (page.window._performance._entries.items) |entry| {\n            if (self.interested(entry)) {\n                try self._entries.append(page.arena, entry);\n            }\n        }\n        if (self.hasRecords()) {\n            try page.schedulePerformanceObserverDelivery();\n        }\n    }\n}\n\npub fn disconnect(self: *PerformanceObserver, page: *Page) void {\n    page.unregisterPerformanceObserver(self);\n    // Reset observer.\n    self._duration_threshold = DefaultDurationThreshold;\n    self._interests = 0;\n    self._entries.clearRetainingCapacity();\n}\n\n/// Returns the current list of PerformanceEntry objects\n/// stored in the performance observer, emptying it out.\npub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry {\n    // Use page.arena instead of call_arena because this slice is wrapped in EntryList\n    // and may be accessed later.\n    const records = try page.arena.dupe(*Performance.Entry, self._entries.items);\n    self._entries.clearRetainingCapacity();\n    return records;\n}\n\npub fn getSupportedEntryTypes() []const []const u8 {\n    return &.{ \"mark\", \"measure\" };\n}\n\n/// Returns true if observer interested with given entry.\npub fn interested(\n    self: *const PerformanceObserver,\n    entry: *const Performance.Entry,\n) bool {\n    const flag = @as(u16, 1) << @intCast(@intFromEnum(entry._type));\n    return self._interests & flag != 0;\n}\n\npub inline fn hasRecords(self: *const PerformanceObserver) bool {\n    return self._entries.items.len > 0;\n}\n\n/// Runs the PerformanceObserver's callback with records; emptying it out.\npub fn dispatch(self: *PerformanceObserver, page: *Page) !void {\n    const records = try self.takeRecords(page);\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var caught: js.TryCatch.Caught = undefined;\n    ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {\n        log.err(.page, \"PerfObserver.dispatch\", .{ .err = err, .caught = caught });\n        return err;\n    };\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(PerformanceObserver);\n\n    pub const Meta = struct {\n        pub const name = \"PerformanceObserver\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(PerformanceObserver.init, .{ .dom_exception = true });\n\n    pub const observe = bridge.function(PerformanceObserver.observe, .{ .dom_exception = true });\n    pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{});\n    pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{ .dom_exception = true });\n    pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{ .static = true });\n};\n\n/// List of performance events that were explicitly\n/// observed via the observe() method.\n/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserverEntryList\npub const EntryList = struct {\n    _entries: []*Performance.Entry,\n\n    pub fn getEntries(self: *const EntryList) []*Performance.Entry {\n        return self._entries;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(EntryList);\n\n        pub const Meta = struct {\n            pub const name = \"PerformanceObserverEntryList\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n\n        pub const getEntries = bridge.function(EntryList.getEntries, .{});\n    };\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: PerformanceObserver\" {\n    try testing.htmlRunner(\"performance_observer\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/Permissions.zig",
    "content": "// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub fn registerTypes() []const type {\n    return &.{ Permissions, PermissionStatus };\n}\n\nconst Permissions = @This();\n\n// Padding to avoid zero-size struct pointer collisions\n_pad: bool = false,\n\nconst QueryDescriptor = struct {\n    name: []const u8,\n};\n// We always report 'prompt' (the default safe value — neither granted nor denied).\npub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promise {\n    const arena = try page.getArena(.{ .debug = \"PermissionStatus\" });\n    errdefer page.releaseArena(arena);\n\n    const status = try arena.create(PermissionStatus);\n    status.* = .{\n        ._arena = arena,\n        ._state = \"prompt\",\n        ._name = try arena.dupe(u8, qd.name),\n    };\n    return page.js.local.?.resolvePromise(status);\n}\n\nconst PermissionStatus = struct {\n    _arena: Allocator,\n    _name: []const u8,\n    _state: []const u8,\n\n    pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void {\n        session.releaseArena(self._arena);\n    }\n\n    fn getName(self: *const PermissionStatus) []const u8 {\n        return self._name;\n    }\n\n    fn getState(self: *const PermissionStatus) []const u8 {\n        return self._state;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(PermissionStatus);\n        pub const Meta = struct {\n            pub const name = \"PermissionStatus\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n            pub const weak = true;\n            pub const finalizer = bridge.finalizer(PermissionStatus.deinit);\n        };\n        pub const name = bridge.accessor(getName, null, .{});\n        pub const state = bridge.accessor(getState, null, .{});\n    };\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Permissions);\n\n    pub const Meta = struct {\n        pub const name = \"Permissions\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const query = bridge.function(Permissions.query, .{ .dom_exception = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/PluginArray.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../js/js.zig\");\n\npub fn registerTypes() []const type {\n    return &.{ PluginArray, Plugin };\n}\n\nconst PluginArray = @This();\n\n_pad: bool = false,\n\npub fn refresh(_: *const PluginArray) void {}\npub fn getAtIndex(_: *const PluginArray, index: usize) ?*Plugin {\n    _ = index;\n    return null;\n}\n\npub fn getByName(_: *const PluginArray, name: []const u8) ?*Plugin {\n    _ = name;\n    return null;\n}\n\n// Cannot be constructed, and we currently never return any, so no reason to\n// implement anything on it (for now)\nconst Plugin = struct {\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(Plugin);\n        pub const Meta = struct {\n            pub const name = \"Plugin\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n            pub const empty_with_no_proto = true;\n        };\n    };\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(PluginArray);\n\n    pub const Meta = struct {\n        pub const name = \"PluginArray\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const length = bridge.property(0, .{ .template = false });\n    pub const refresh = bridge.function(PluginArray.refresh, .{});\n    pub const @\"[int]\" = bridge.indexed(PluginArray.getAtIndex, null, .{ .null_as_undefined = true });\n    pub const @\"[str]\" = bridge.namedIndexed(PluginArray.getByName, null, null, .{ .null_as_undefined = true });\n    pub const item = bridge.function(_item, .{});\n    fn _item(self: *const PluginArray, index: i32) ?*Plugin {\n        if (index < 0) {\n            return null;\n        }\n        return self.getAtIndex(@intCast(index));\n    }\n    pub const namedItem = bridge.function(PluginArray.getByName, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/Range.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../string.zig\").String;\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst Node = @import(\"Node.zig\");\nconst DocumentFragment = @import(\"DocumentFragment.zig\");\nconst AbstractRange = @import(\"AbstractRange.zig\");\nconst DOMRect = @import(\"DOMRect.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst Range = @This();\n\n_proto: *AbstractRange,\n\npub fn init(page: *Page) !*Range {\n    const arena = try page.getArena(.{ .debug = \"Range\" });\n    errdefer page.releaseArena(arena);\n    return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);\n}\n\npub fn deinit(self: *Range, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asAbstractRange(self: *Range) *AbstractRange {\n    return self._proto;\n}\n\npub fn setStart(self: *Range, node: *Node, offset: u32) !void {\n    if (node._type == .document_type) {\n        return error.InvalidNodeType;\n    }\n\n    if (offset > node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    self._proto._start_container = node;\n    self._proto._start_offset = offset;\n\n    // If start is now after end, or nodes are in different trees, collapse to start\n    const end_root = self._proto._end_container.getRootNode(null);\n    const start_root = node.getRootNode(null);\n    if (end_root != start_root or self._proto.isStartAfterEnd()) {\n        self._proto._end_container = self._proto._start_container;\n        self._proto._end_offset = self._proto._start_offset;\n    }\n}\n\npub fn setEnd(self: *Range, node: *Node, offset: u32) !void {\n    if (node._type == .document_type) {\n        return error.InvalidNodeType;\n    }\n\n    // Validate offset\n    if (offset > node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    self._proto._end_container = node;\n    self._proto._end_offset = offset;\n\n    // If end is now before start, or nodes are in different trees, collapse to end\n    const start_root = self._proto._start_container.getRootNode(null);\n    const end_root = node.getRootNode(null);\n    if (start_root != end_root or self._proto.isStartAfterEnd()) {\n        self._proto._start_container = self._proto._end_container;\n        self._proto._start_offset = self._proto._end_offset;\n    }\n}\n\npub fn setStartBefore(self: *Range, node: *Node) !void {\n    const parent = node.parentNode() orelse return error.InvalidNodeType;\n    const offset = parent.getChildIndex(node) orelse return error.NotFound;\n    try self.setStart(parent, offset);\n}\n\npub fn setStartAfter(self: *Range, node: *Node) !void {\n    const parent = node.parentNode() orelse return error.InvalidNodeType;\n    const offset = parent.getChildIndex(node) orelse return error.NotFound;\n    try self.setStart(parent, offset + 1);\n}\n\npub fn setEndBefore(self: *Range, node: *Node) !void {\n    const parent = node.parentNode() orelse return error.InvalidNodeType;\n    const offset = parent.getChildIndex(node) orelse return error.NotFound;\n    try self.setEnd(parent, offset);\n}\n\npub fn setEndAfter(self: *Range, node: *Node) !void {\n    const parent = node.parentNode() orelse return error.InvalidNodeType;\n    const offset = parent.getChildIndex(node) orelse return error.NotFound;\n    try self.setEnd(parent, offset + 1);\n}\n\npub fn selectNode(self: *Range, node: *Node) !void {\n    const parent = node.parentNode() orelse return error.InvalidNodeType;\n    const offset = parent.getChildIndex(node) orelse return error.NotFound;\n    try self.setStart(parent, offset);\n    try self.setEnd(parent, offset + 1);\n}\n\npub fn selectNodeContents(self: *Range, node: *Node) !void {\n    const length = node.getLength();\n    try self.setStart(node, 0);\n    try self.setEnd(node, length);\n}\n\npub fn collapse(self: *Range, to_start: ?bool) void {\n    if (to_start orelse true) {\n        self._proto._end_container = self._proto._start_container;\n        self._proto._end_offset = self._proto._start_offset;\n    } else {\n        self._proto._start_container = self._proto._end_container;\n        self._proto._start_offset = self._proto._end_offset;\n    }\n}\n\npub fn detach(_: *Range) void {\n    // Legacy no-op method kept for backwards compatibility\n    // Modern spec: \"The detach() method must do nothing.\"\n}\n\npub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *const Range) !i16 {\n    // Convert how parameter per WebIDL unsigned short conversion\n    // This handles negative numbers and out-of-range values\n    const how_mod = @mod(how_raw, 65536);\n    const how: u16 = if (how_mod < 0) @intCast(@as(i32, how_mod) + 65536) else @intCast(how_mod);\n\n    // If how is not one of 0, 1, 2, or 3, throw NotSupportedError\n    if (how > 3) {\n        return error.NotSupported;\n    }\n\n    // If the two ranges' root is different, throw WrongDocumentError\n    const this_root = self._proto._start_container.getRootNode(null);\n    const source_root = source_range._proto._start_container.getRootNode(null);\n    if (this_root != source_root) {\n        return error.WrongDocument;\n    }\n\n    // Determine which boundary points to compare based on how parameter\n    const result = switch (how) {\n        0 => AbstractRange.compareBoundaryPoints( // START_TO_START\n            self._proto._start_container,\n            self._proto._start_offset,\n            source_range._proto._start_container,\n            source_range._proto._start_offset,\n        ),\n        1 => AbstractRange.compareBoundaryPoints( // START_TO_END\n            self._proto._end_container,\n            self._proto._end_offset,\n            source_range._proto._start_container,\n            source_range._proto._start_offset,\n        ),\n        2 => AbstractRange.compareBoundaryPoints( // END_TO_END\n            self._proto._end_container,\n            self._proto._end_offset,\n            source_range._proto._end_container,\n            source_range._proto._end_offset,\n        ),\n        3 => AbstractRange.compareBoundaryPoints( // END_TO_START\n            self._proto._start_container,\n            self._proto._start_offset,\n            source_range._proto._end_container,\n            source_range._proto._end_offset,\n        ),\n        else => unreachable,\n    };\n\n    return switch (result) {\n        .before => -1,\n        .equal => 0,\n        .after => 1,\n    };\n}\n\npub fn comparePoint(self: *const Range, node: *Node, offset: u32) !i16 {\n    // Check if node is in a different tree than the range\n    const node_root = node.getRootNode(null);\n    const start_root = self._proto._start_container.getRootNode(null);\n    if (node_root != start_root) {\n        return error.WrongDocument;\n    }\n\n    if (node._type == .document_type) {\n        return error.InvalidNodeType;\n    }\n\n    if (offset > node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    // Compare point with start boundary\n    const cmp_start = AbstractRange.compareBoundaryPoints(\n        node,\n        offset,\n        self._proto._start_container,\n        self._proto._start_offset,\n    );\n\n    if (cmp_start == .before) {\n        return -1;\n    }\n\n    const cmp_end = AbstractRange.compareBoundaryPoints(\n        node,\n        offset,\n        self._proto._end_container,\n        self._proto._end_offset,\n    );\n\n    return if (cmp_end == .after) 1 else 0;\n}\n\npub fn isPointInRange(self: *const Range, node: *Node, offset: u32) !bool {\n    // If node's root is different from the context object's root, return false\n    const node_root = node.getRootNode(null);\n    const start_root = self._proto._start_container.getRootNode(null);\n    if (node_root != start_root) {\n        return false;\n    }\n\n    if (node._type == .document_type) {\n        return error.InvalidNodeType;\n    }\n\n    // If offset is greater than node's length, throw IndexSizeError\n    if (offset > node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    // If (node, offset) is before start or after end, return false\n    const cmp_start = AbstractRange.compareBoundaryPoints(\n        node,\n        offset,\n        self._proto._start_container,\n        self._proto._start_offset,\n    );\n\n    if (cmp_start == .before) {\n        return false;\n    }\n\n    const cmp_end = AbstractRange.compareBoundaryPoints(\n        node,\n        offset,\n        self._proto._end_container,\n        self._proto._end_offset,\n    );\n\n    return cmp_end != .after;\n}\n\npub fn intersectsNode(self: *const Range, node: *Node) bool {\n    // If node's root is different from the context object's root, return false\n    const node_root = node.getRootNode(null);\n    const start_root = self._proto._start_container.getRootNode(null);\n    if (node_root != start_root) {\n        return false;\n    }\n\n    // Let parent be node's parent\n    const parent = node.parentNode() orelse {\n        // If parent is null, return true\n        return true;\n    };\n\n    // Let offset be node's index\n    const offset = parent.getChildIndex(node) orelse {\n        // Should not happen if node has a parent\n        return false;\n    };\n\n    // If (parent, offset) is before end and (parent, offset + 1) is after start, return true\n    const before_end = AbstractRange.compareBoundaryPoints(\n        parent,\n        offset,\n        self._proto._end_container,\n        self._proto._end_offset,\n    );\n\n    const after_start = AbstractRange.compareBoundaryPoints(\n        parent,\n        offset + 1,\n        self._proto._start_container,\n        self._proto._start_offset,\n    );\n\n    if (before_end == .before and after_start == .after) {\n        return true;\n    }\n\n    // Return false\n    return false;\n}\n\npub fn cloneRange(self: *const Range, page: *Page) !*Range {\n    const arena = try page.getArena(.{ .debug = \"Range.clone\" });\n    errdefer page.releaseArena(arena);\n\n    const clone = try page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);\n    clone._proto._end_offset = self._proto._end_offset;\n    clone._proto._start_offset = self._proto._start_offset;\n    clone._proto._end_container = self._proto._end_container;\n    clone._proto._start_container = self._proto._start_container;\n    return clone;\n}\n\npub fn insertNode(self: *Range, node: *Node, page: *Page) !void {\n    // Insert node at the start of the range\n    const container = self._proto._start_container;\n    const offset = self._proto._start_offset;\n\n    // Per spec: if range is collapsed, end offset should extend to include\n    // the inserted node. Capture before insertion since live range updates\n    // in the insert path will adjust non-collapsed ranges automatically.\n    const was_collapsed = self._proto.getCollapsed();\n\n    if (container.is(Node.CData)) |_| {\n        // If container is a text node, we need to split it\n        const parent = container.parentNode() orelse return error.InvalidNodeType;\n\n        if (offset == 0) {\n            _ = try parent.insertBefore(node, container, page);\n        } else {\n            const text_data = container.getData().str();\n            if (offset >= text_data.len) {\n                _ = try parent.insertBefore(node, container.nextSibling(), page);\n            } else {\n                // Split the text node into before and after parts\n                const before_text = text_data[0..offset];\n                const after_text = text_data[offset..];\n\n                const before = try page.createTextNode(before_text);\n                const after = try page.createTextNode(after_text);\n\n                _ = try parent.replaceChild(before, container, page);\n                _ = try parent.insertBefore(node, before.nextSibling(), page);\n                _ = try parent.insertBefore(after, node.nextSibling(), page);\n            }\n        }\n    } else {\n        // Container is an element, insert at offset\n        const ref_child = container.getChildAt(offset);\n        _ = try container.insertBefore(node, ref_child, page);\n    }\n\n    // Per spec step 11: if range was collapsed, extend end to include inserted node.\n    // Non-collapsed ranges are already handled by the live range update in the insert path.\n    if (was_collapsed) {\n        self._proto._end_offset = self._proto._start_offset + 1;\n    }\n}\n\npub fn deleteContents(self: *Range, page: *Page) !void {\n    if (self._proto.getCollapsed()) {\n        return;\n    }\n    page.domChanged();\n\n    // Simple case: same container\n    if (self._proto._start_container == self._proto._end_container) {\n        if (self._proto._start_container.is(Node.CData)) |cdata| {\n            // Delete part of text node\n            const old_value = cdata.getData();\n            const text_data = old_value.str();\n            cdata._data = try String.concat(\n                page.arena,\n                &.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },\n            );\n            page.characterDataChange(self._proto._start_container, old_value);\n        } else {\n            // Delete child nodes in range.\n            // Capture count before the loop: removeChild triggers live range\n            // updates that decrement _end_offset on each removal.\n            const count = self._proto._end_offset - self._proto._start_offset;\n            var i: u32 = 0;\n            while (i < count) : (i += 1) {\n                if (self._proto._start_container.getChildAt(self._proto._start_offset)) |child| {\n                    _ = try self._proto._start_container.removeChild(child, page);\n                }\n            }\n        }\n        self.collapse(true);\n        return;\n    }\n\n    // Complex case: different containers\n    // Handle start container - if it's a text node, truncate it\n    if (self._proto._start_container.is(Node.CData)) |cdata| {\n        const text_data = cdata._data.str();\n        if (self._proto._start_offset < text_data.len) {\n            // Keep only the part before start_offset\n            const new_text = text_data[0..self._proto._start_offset];\n            try self._proto._start_container.setData(new_text, page);\n        }\n    }\n\n    // Handle end container - if it's a text node, truncate it\n    if (self._proto._end_container.is(Node.CData)) |cdata| {\n        const text_data = cdata._data.str();\n        if (self._proto._end_offset < text_data.len) {\n            // Keep only the part from end_offset onwards\n            const new_text = text_data[self._proto._end_offset..];\n            try self._proto._end_container.setData(new_text, page);\n        } else if (self._proto._end_offset == text_data.len) {\n            // If we're at the end, set to empty (will be removed if needed)\n            try self._proto._end_container.setData(\"\", page);\n        }\n    }\n\n    // Remove nodes between start and end containers\n    // For now, handle the common case where they're siblings\n    if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {\n        var current = self._proto._start_container.nextSibling();\n        while (current != null and current != self._proto._end_container) {\n            const next = current.?.nextSibling();\n            if (current.?.parentNode()) |parent| {\n                _ = try parent.removeChild(current.?, page);\n            }\n            current = next;\n        }\n    }\n\n    self.collapse(true);\n}\n\npub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {\n    const fragment = try DocumentFragment.init(page);\n\n    if (self._proto.getCollapsed()) return fragment;\n\n    // Simple case: same container\n    if (self._proto._start_container == self._proto._end_container) {\n        if (self._proto._start_container.is(Node.CData)) |_| {\n            // Clone part of text node\n            const text_data = self._proto._start_container.getData().str();\n            if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {\n                const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];\n                const text_node = try page.createTextNode(cloned_text);\n                _ = try fragment.asNode().appendChild(text_node, page);\n            }\n        } else {\n            // Clone child nodes in range\n            var offset = self._proto._start_offset;\n            while (offset < self._proto._end_offset) : (offset += 1) {\n                if (self._proto._start_container.getChildAt(offset)) |child| {\n                    if (try child.cloneNodeForAppending(true, page)) |cloned| {\n                        _ = try fragment.asNode().appendChild(cloned, page);\n                    }\n                }\n            }\n        }\n    } else {\n        // Complex case: different containers\n        // Clone partial start container\n        if (self._proto._start_container.is(Node.CData)) |_| {\n            const text_data = self._proto._start_container.getData().str();\n            if (self._proto._start_offset < text_data.len) {\n                // Clone from start_offset to end of text\n                const cloned_text = text_data[self._proto._start_offset..];\n                const text_node = try page.createTextNode(cloned_text);\n                _ = try fragment.asNode().appendChild(text_node, page);\n            }\n        }\n\n        // Clone nodes between start and end containers (siblings case)\n        if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {\n            var current = self._proto._start_container.nextSibling();\n            while (current != null and current != self._proto._end_container) {\n                const next = current.?.nextSibling();\n                if (try current.?.cloneNodeForAppending(true, page)) |cloned| {\n                    _ = try fragment.asNode().appendChild(cloned, page);\n                }\n                current = next;\n            }\n        }\n\n        // Clone partial end container\n        if (self._proto._end_container.is(Node.CData)) |_| {\n            const text_data = self._proto._end_container.getData().str();\n            if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {\n                // Clone from start to end_offset\n                const cloned_text = text_data[0..self._proto._end_offset];\n                const text_node = try page.createTextNode(cloned_text);\n                _ = try fragment.asNode().appendChild(text_node, page);\n            }\n        }\n    }\n\n    return fragment;\n}\n\npub fn extractContents(self: *Range, page: *Page) !*DocumentFragment {\n    const fragment = try self.cloneContents(page);\n    try self.deleteContents(page);\n    return fragment;\n}\n\npub fn surroundContents(self: *Range, new_parent: *Node, page: *Page) !void {\n    // Extract contents\n    const contents = try self.extractContents(page);\n\n    // Insert the new parent\n    try self.insertNode(new_parent, page);\n\n    // Move contents into new parent\n    _ = try new_parent.appendChild(contents.asNode(), page);\n\n    // Select the new parent's contents\n    try self.selectNodeContents(new_parent);\n}\n\npub fn createContextualFragment(self: *const Range, html: []const u8, page: *Page) !*DocumentFragment {\n    var context_node = self._proto._start_container;\n\n    // If start container is a text node, use its parent as context\n    if (context_node.is(Node.CData)) |_| {\n        context_node = context_node.parentNode() orelse context_node;\n    }\n\n    const fragment = try DocumentFragment.init(page);\n\n    if (html.len == 0) {\n        return fragment;\n    }\n\n    // Create a temporary element of the same type as the context for parsing\n    // This preserves the parsing context without modifying the original node\n    const temp_node = if (context_node.is(Node.Element)) |el|\n        try page.createElementNS(el._namespace, el.getTagNameLower(), null)\n    else\n        try page.createElementNS(.html, \"div\", null);\n\n    try page.parseHtmlAsChildren(temp_node, html);\n\n    // Move all parsed children to the fragment\n    // Keep removing first child until temp element is empty\n    const fragment_node = fragment.asNode();\n    while (temp_node.firstChild()) |child| {\n        page.removeNode(temp_node, child, .{ .will_be_reconnected = true });\n        try page.appendNode(fragment_node, child, .{ .child_already_connected = false });\n    }\n\n    return fragment;\n}\n\npub fn toString(self: *const Range, page: *Page) ![]const u8 {\n    // Simplified implementation: just extract text content\n    var buf = std.Io.Writer.Allocating.init(page.call_arena);\n    try self.writeTextContent(&buf.writer);\n    return buf.written();\n}\n\nfn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {\n    if (self._proto.getCollapsed()) return;\n\n    const start_node = self._proto._start_container;\n    const end_node = self._proto._end_container;\n    const start_offset = self._proto._start_offset;\n    const end_offset = self._proto._end_offset;\n\n    // Same text node — just substring\n    if (start_node == end_node) {\n        if (start_node.is(Node.CData)) |cdata| {\n            if (!isCommentOrPI(cdata)) {\n                const data = cdata.getData().str();\n                const s = @min(start_offset, data.len);\n                const e = @min(end_offset, data.len);\n                try writer.writeAll(data[s..e]);\n            }\n            return;\n        }\n    }\n\n    const root = self._proto.getCommonAncestorContainer();\n\n    // Partial start: if start container is a text node, write from offset to end\n    if (start_node.is(Node.CData)) |cdata| {\n        if (!isCommentOrPI(cdata)) {\n            const data = cdata.getData().str();\n            const s = @min(start_offset, data.len);\n            try writer.writeAll(data[s..]);\n        }\n    }\n\n    // Walk fully-contained text nodes between the boundaries.\n    // For text containers, the walk starts after that node.\n    // For element containers, the walk starts at the child at offset.\n    const walk_start: ?*Node = if (start_node.is(Node.CData) != null)\n        nextInTreeOrder(start_node, root)\n    else\n        start_node.getChildAt(start_offset) orelse nextAfterSubtree(start_node, root);\n\n    const walk_end: ?*Node = if (end_node.is(Node.CData) != null)\n        end_node\n    else\n        end_node.getChildAt(end_offset) orelse nextAfterSubtree(end_node, root);\n\n    if (walk_start) |start| {\n        var current: ?*Node = start;\n        while (current) |n| {\n            if (walk_end) |we| {\n                if (n == we) break;\n            }\n            if (n.is(Node.CData)) |cdata| {\n                if (!isCommentOrPI(cdata)) {\n                    try writer.writeAll(cdata.getData().str());\n                }\n            }\n            current = nextInTreeOrder(n, root);\n        }\n    }\n\n    // Partial end: if end container is a different text node, write from start to offset\n    if (start_node != end_node) {\n        if (end_node.is(Node.CData)) |cdata| {\n            if (!isCommentOrPI(cdata)) {\n                const data = cdata.getData().str();\n                const e = @min(end_offset, data.len);\n                try writer.writeAll(data[0..e]);\n            }\n        }\n    }\n}\n\nfn isCommentOrPI(cdata: *Node.CData) bool {\n    return cdata.is(Node.CData.Comment) != null or cdata.is(Node.CData.ProcessingInstruction) != null;\n}\n\nfn nextInTreeOrder(node: *Node, root: *Node) ?*Node {\n    if (node.firstChild()) |child| return child;\n    return nextAfterSubtree(node, root);\n}\n\nfn nextAfterSubtree(node: *Node, root: *Node) ?*Node {\n    var current = node;\n    while (current != root) {\n        if (current.nextSibling()) |sibling| return sibling;\n        current = current.parentNode() orelse return null;\n    }\n    return null;\n}\n\npub fn getBoundingClientRect(self: *const Range, page: *Page) DOMRect {\n    if (self._proto.getCollapsed()) {\n        return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 };\n    }\n    const element = self.getContainerElement() orelse {\n        return .{ ._x = 0, ._y = 0, ._width = 0, ._height = 0 };\n    };\n    return element.getBoundingClientRect(page);\n}\n\npub fn getClientRects(self: *const Range, page: *Page) ![]DOMRect {\n    if (self._proto.getCollapsed()) {\n        return &.{};\n    }\n    const element = self.getContainerElement() orelse {\n        return &.{};\n    };\n    return element.getClientRects(page);\n}\n\nfn getContainerElement(self: *const Range) ?*Node.Element {\n    const container = self._proto.getCommonAncestorContainer();\n    if (container.is(Node.Element)) |el| return el;\n    const parent = container.parentNode() orelse return null;\n    return parent.is(Node.Element);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Range);\n\n    pub const Meta = struct {\n        pub const name = \"Range\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(Range.deinit);\n    };\n\n    // Constants for compareBoundaryPoints\n    pub const START_TO_START = bridge.property(0, .{ .template = true });\n    pub const START_TO_END = bridge.property(1, .{ .template = true });\n    pub const END_TO_END = bridge.property(2, .{ .template = true });\n    pub const END_TO_START = bridge.property(3, .{ .template = true });\n\n    pub const constructor = bridge.constructor(Range.init, .{});\n    pub const setStart = bridge.function(Range.setStart, .{ .dom_exception = true });\n    pub const setEnd = bridge.function(Range.setEnd, .{ .dom_exception = true });\n    pub const setStartBefore = bridge.function(Range.setStartBefore, .{ .dom_exception = true });\n    pub const setStartAfter = bridge.function(Range.setStartAfter, .{ .dom_exception = true });\n    pub const setEndBefore = bridge.function(Range.setEndBefore, .{ .dom_exception = true });\n    pub const setEndAfter = bridge.function(Range.setEndAfter, .{ .dom_exception = true });\n    pub const selectNode = bridge.function(Range.selectNode, .{ .dom_exception = true });\n    pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{});\n    pub const collapse = bridge.function(Range.collapse, .{ .dom_exception = true });\n    pub const detach = bridge.function(Range.detach, .{});\n    pub const compareBoundaryPoints = bridge.function(Range.compareBoundaryPoints, .{ .dom_exception = true });\n    pub const comparePoint = bridge.function(Range.comparePoint, .{ .dom_exception = true });\n    pub const isPointInRange = bridge.function(Range.isPointInRange, .{ .dom_exception = true });\n    pub const intersectsNode = bridge.function(Range.intersectsNode, .{});\n    pub const cloneRange = bridge.function(Range.cloneRange, .{ .dom_exception = true });\n    pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true });\n    pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true });\n    pub const cloneContents = bridge.function(Range.cloneContents, .{ .dom_exception = true });\n    pub const extractContents = bridge.function(Range.extractContents, .{ .dom_exception = true });\n    pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true });\n    pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true });\n    pub const toString = bridge.function(Range.toString, .{ .dom_exception = true });\n    pub const getBoundingClientRect = bridge.function(Range.getBoundingClientRect, .{});\n    pub const getClientRects = bridge.function(Range.getClientRects, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Range\" {\n    try testing.htmlRunner(\"range.html\", .{});\n}\ntest \"WebApi: Range mutations\" {\n    try testing.htmlRunner(\"range_mutations.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/ResizeObserver.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst Element = @import(\"Element.zig\");\n\npub const ResizeObserver = @This();\n\n// Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n_pad: bool = false,\n\nfn init(cbk: js.Function) ResizeObserver {\n    _ = cbk;\n    return .{};\n}\n\nconst Options = struct {\n    box: []const u8,\n};\npub fn observe(self: *const ResizeObserver, element: *Element, options_: ?Options) void {\n    _ = self;\n    _ = element;\n    _ = options_;\n    return;\n}\n\npub fn unobserve(self: *const ResizeObserver, element: *Element) void {\n    _ = self;\n    _ = element;\n    return;\n}\n\npub fn disconnect(self: *const ResizeObserver) void {\n    _ = self;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ResizeObserver);\n\n    pub const Meta = struct {\n        pub const name = \"ResizeObserver\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const constructor = bridge.constructor(ResizeObserver.init, .{});\n    pub const observe = bridge.function(ResizeObserver.observe, .{});\n    pub const disconnect = bridge.function(ResizeObserver.disconnect, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/Screen.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\n\npub fn registerTypes() []const type {\n    return &.{\n        Screen,\n        Orientation,\n    };\n}\n\nconst Screen = @This();\n\n_proto: *EventTarget,\n_orientation: ?*Orientation = null,\n\npub fn asEventTarget(self: *Screen) *EventTarget {\n    return self._proto;\n}\n\npub fn getOrientation(self: *Screen, page: *Page) !*Orientation {\n    if (self._orientation) |orientation| {\n        return orientation;\n    }\n    const orientation = try Orientation.init(page);\n    self._orientation = orientation;\n    return orientation;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Screen);\n\n    pub const Meta = struct {\n        pub const name = \"Screen\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const width = bridge.property(1920, .{ .template = false });\n    pub const height = bridge.property(1080, .{ .template = false });\n    pub const availWidth = bridge.property(1920, .{ .template = false });\n    pub const availHeight = bridge.property(1040, .{ .template = false });\n    pub const colorDepth = bridge.property(24, .{ .template = false });\n    pub const pixelDepth = bridge.property(24, .{ .template = false });\n    pub const orientation = bridge.accessor(Screen.getOrientation, null, .{});\n};\n\npub const Orientation = struct {\n    _proto: *EventTarget,\n\n    pub fn init(page: *Page) !*Orientation {\n        return page._factory.eventTarget(Orientation{\n            ._proto = undefined,\n        });\n    }\n\n    pub fn asEventTarget(self: *Orientation) *EventTarget {\n        return self._proto;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(Orientation);\n\n        pub const Meta = struct {\n            pub const name = \"ScreenOrientation\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n\n        pub const angle = bridge.property(0, .{ .template = false });\n        pub const @\"type\" = bridge.property(\"landscape-primary\", .{ .template = false });\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/Selection.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../log.zig\");\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Session = @import(\"../Session.zig\");\n\nconst Range = @import(\"Range.zig\");\nconst AbstractRange = @import(\"AbstractRange.zig\");\nconst Node = @import(\"Node.zig\");\nconst Event = @import(\"Event.zig\");\nconst Document = @import(\"Document.zig\");\n\n/// https://w3c.github.io/selection-api/\nconst Selection = @This();\n\npub const SelectionDirection = enum { backward, forward, none };\n\n_range: ?*Range = null,\n_direction: SelectionDirection = .none,\n\npub const init: Selection = .{};\n\npub fn deinit(self: *Selection, shutdown: bool, session: *Session) void {\n    if (self._range) |r| {\n        r.deinit(shutdown, session);\n        self._range = null;\n    }\n}\n\nfn dispatchSelectionChangeEvent(page: *Page) !void {\n    const event = try Event.init(\"selectionchange\", .{}, page);\n    try page._event_manager.dispatch(page.document.asEventTarget(), event);\n}\n\nfn isInTree(self: *const Selection) bool {\n    if (self._range == null) {\n        return false;\n    }\n    const anchor_node = self.getAnchorNode() orelse return false;\n    const focus_node = self.getFocusNode() orelse return false;\n    return anchor_node.isConnected() and focus_node.isConnected();\n}\n\npub fn getAnchorNode(self: *const Selection) ?*Node {\n    const range = self._range orelse return null;\n\n    const node = switch (self._direction) {\n        .backward => range.asAbstractRange().getEndContainer(),\n        .forward, .none => range.asAbstractRange().getStartContainer(),\n    };\n\n    return if (node.isConnected()) node else null;\n}\n\npub fn getAnchorOffset(self: *const Selection) u32 {\n    const range = self._range orelse return 0;\n\n    const anchor_node = self.getAnchorNode() orelse return 0;\n    if (!anchor_node.isConnected()) return 0;\n\n    return switch (self._direction) {\n        .backward => range.asAbstractRange().getEndOffset(),\n        .forward, .none => range.asAbstractRange().getStartOffset(),\n    };\n}\n\npub fn getDirection(self: *const Selection) []const u8 {\n    return @tagName(self._direction);\n}\n\npub fn getFocusNode(self: *const Selection) ?*Node {\n    const range = self._range orelse return null;\n\n    const node = switch (self._direction) {\n        .backward => range.asAbstractRange().getStartContainer(),\n        .forward, .none => range.asAbstractRange().getEndContainer(),\n    };\n\n    return if (node.isConnected()) node else null;\n}\n\npub fn getFocusOffset(self: *const Selection) u32 {\n    const range = self._range orelse return 0;\n    const focus_node = self.getFocusNode() orelse return 0;\n    if (!focus_node.isConnected()) return 0;\n\n    return switch (self._direction) {\n        .backward => range.asAbstractRange().getStartOffset(),\n        .forward, .none => range.asAbstractRange().getEndOffset(),\n    };\n}\n\npub fn getIsCollapsed(self: *const Selection) bool {\n    const range = self._range orelse return true;\n    return range.asAbstractRange().getCollapsed();\n}\n\npub fn getRangeCount(self: *const Selection) u32 {\n    if (self._range == null) {\n        return 0;\n    }\n    if (!self.isInTree()) {\n        return 0;\n    }\n\n    return 1;\n}\n\npub fn getType(self: *const Selection) []const u8 {\n    if (self._range == null) {\n        return \"None\";\n    }\n    if (!self.isInTree()) {\n        return \"None\";\n    }\n    if (self.getIsCollapsed()) {\n        return \"Caret\";\n    }\n    return \"Range\";\n}\n\npub fn addRange(self: *Selection, range: *Range, page: *Page) !void {\n    if (self._range != null) {\n        return;\n    }\n\n    // Only add the range if its root node is in the document associated with this selection\n    const start_node = range.asAbstractRange().getStartContainer();\n    if (!page.document.asNode().contains(start_node)) {\n        return;\n    }\n\n    self.setRange(range, page);\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn removeRange(self: *Selection, range: *Range, page: *Page) !void {\n    const existing_range = self._range orelse return error.NotFound;\n    if (existing_range != range) {\n        return error.NotFound;\n    }\n    self.setRange(null, page);\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn removeAllRanges(self: *Selection, page: *Page) !void {\n    if (self._range == null) {\n        return;\n    }\n\n    self.setRange(null, page);\n    self._direction = .none;\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn collapseToEnd(self: *Selection, page: *Page) !void {\n    const range = self._range orelse return;\n\n    const abstract = range.asAbstractRange();\n    const last_node = abstract.getEndContainer();\n    const last_offset = abstract.getEndOffset();\n\n    const new_range = try Range.init(page);\n    try new_range.setStart(last_node, last_offset);\n    try new_range.setEnd(last_node, last_offset);\n\n    self.setRange(new_range, page);\n    self._direction = .none;\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn collapseToStart(self: *Selection, page: *Page) !void {\n    const range = self._range orelse return error.InvalidStateError;\n\n    const abstract = range.asAbstractRange();\n    const first_node = abstract.getStartContainer();\n    const first_offset = abstract.getStartOffset();\n\n    const new_range = try Range.init(page);\n    try new_range.setStart(first_node, first_offset);\n    try new_range.setEnd(first_node, first_offset);\n\n    self.setRange(new_range, page);\n    self._direction = .none;\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn containsNode(self: *const Selection, node: *Node, partial: bool) !bool {\n    const range = self._range orelse return false;\n\n    if (partial) {\n        if (range.intersectsNode(node)) {\n            return true;\n        }\n    } else {\n        const abstract = range.asAbstractRange();\n        if (abstract.getStartContainer() == node or abstract.getEndContainer() == node) {\n            return false;\n        }\n\n        const parent = node.parentNode() orelse return false;\n        const offset = parent.getChildIndex(node) orelse return false;\n        const start_cmp = range.comparePoint(parent, offset) catch return false;\n        const end_cmp = range.comparePoint(parent, offset + 1) catch return false;\n\n        if (start_cmp <= 0 and end_cmp >= 0) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\npub fn deleteFromDocument(self: *Selection, page: *Page) !void {\n    const range = self._range orelse return;\n    try range.deleteContents(page);\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {\n    const range = self._range orelse return error.InvalidState;\n    const offset = _offset orelse 0;\n\n    // If the node is not contained in the document, do not change the selection\n    if (!page.document.asNode().contains(node)) {\n        return;\n    }\n\n    if (node._type == .document_type) return error.InvalidNodeType;\n\n    if (offset > node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    const old_anchor = switch (self._direction) {\n        .backward => range.asAbstractRange().getEndContainer(),\n        .forward, .none => range.asAbstractRange().getStartContainer(),\n    };\n    const old_anchor_offset = switch (self._direction) {\n        .backward => range.asAbstractRange().getEndOffset(),\n        .forward, .none => range.asAbstractRange().getStartOffset(),\n    };\n\n    const new_range = try Range.init(page);\n\n    const cmp = AbstractRange.compareBoundaryPoints(node, offset, old_anchor, old_anchor_offset);\n    switch (cmp) {\n        .before => {\n            try new_range.setStart(node, offset);\n            try new_range.setEnd(old_anchor, old_anchor_offset);\n            self._direction = .backward;\n        },\n        .after => {\n            try new_range.setStart(old_anchor, old_anchor_offset);\n            try new_range.setEnd(node, offset);\n            self._direction = .forward;\n        },\n        .equal => {\n            try new_range.setStart(old_anchor, old_anchor_offset);\n            try new_range.setEnd(old_anchor, old_anchor_offset);\n            self._direction = .none;\n        },\n    }\n\n    self.setRange(new_range, page);\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn getRangeAt(self: *Selection, index: u32) !*Range {\n    if (index != 0) return error.IndexSizeError;\n    if (!self.isInTree()) return error.IndexSizeError;\n    const range = self._range orelse return error.IndexSizeError;\n\n    return range;\n}\n\nconst ModifyAlter = enum {\n    move,\n    extend,\n\n    pub fn fromString(str: []const u8) ?ModifyAlter {\n        return std.meta.stringToEnum(ModifyAlter, str);\n    }\n};\n\nconst ModifyDirection = enum {\n    forward,\n    backward,\n    left,\n    right,\n\n    pub fn fromString(str: []const u8) ?ModifyDirection {\n        return std.meta.stringToEnum(ModifyDirection, str);\n    }\n};\n\nconst ModifyGranularity = enum {\n    character,\n    word,\n    // The rest are either:\n    // 1. Layout dependent.\n    // 2. Not widely supported across browsers.\n\n    pub fn fromString(str: []const u8) ?ModifyGranularity {\n        return std.meta.stringToEnum(ModifyGranularity, str);\n    }\n};\n\npub fn modify(\n    self: *Selection,\n    alter_str: []const u8,\n    direction_str: []const u8,\n    granularity_str: []const u8,\n    page: *Page,\n) !void {\n    const alter = ModifyAlter.fromString(alter_str) orelse return;\n    const direction = ModifyDirection.fromString(direction_str) orelse return;\n    const granularity = ModifyGranularity.fromString(granularity_str) orelse return;\n\n    const range = self._range orelse return;\n\n    const is_forward = switch (direction) {\n        .forward, .right => true,\n        .backward, .left => false,\n    };\n\n    switch (granularity) {\n        .character => try self.modifyByCharacter(alter, is_forward, range, page),\n        .word => try self.modifyByWord(alter, is_forward, range, page),\n    }\n}\n\nfn isTextNode(node: *const Node) bool {\n    return switch (node._type) {\n        .cdata => |cd| cd._type == .text,\n        else => false,\n    };\n}\n\nfn nextTextNode(node: *Node) ?*Node {\n    var current = node;\n\n    while (true) {\n        if (current.firstChild()) |child| {\n            current = child;\n        } else if (current.nextSibling()) |sib| {\n            current = sib;\n        } else {\n            while (true) {\n                const parent = current.parentNode() orelse return null;\n                if (parent.nextSibling()) |uncle| {\n                    current = uncle;\n                    break;\n                }\n                current = parent;\n            }\n        }\n\n        if (isTextNode(current)) return current;\n    }\n}\n\nfn nextTextNodeAfter(node: *Node) ?*Node {\n    var current = node;\n    while (true) {\n        if (current.nextSibling()) |sib| {\n            current = sib;\n        } else {\n            while (true) {\n                const parent = current.parentNode() orelse return null;\n                if (parent.nextSibling()) |uncle| {\n                    current = uncle;\n                    break;\n                }\n                current = parent;\n            }\n        }\n\n        var descend = current;\n        while (true) {\n            if (isTextNode(descend)) return descend;\n            descend = descend.firstChild() orelse break;\n        }\n    }\n}\n\nfn prevTextNode(node: *Node) ?*Node {\n    var current = node;\n\n    while (true) {\n        if (current.previousSibling()) |sib| {\n            current = sib;\n            while (current.lastChild()) |child| {\n                current = child;\n            }\n        } else {\n            current = current.parentNode() orelse return null;\n        }\n\n        if (isTextNode(current)) return current;\n    }\n}\n\nfn modifyByCharacter(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, page: *Page) !void {\n    const abstract = range.asAbstractRange();\n\n    const focus_node = switch (self._direction) {\n        .backward => abstract.getStartContainer(),\n        .forward, .none => abstract.getEndContainer(),\n    };\n    const focus_offset = switch (self._direction) {\n        .backward => abstract.getStartOffset(),\n        .forward, .none => abstract.getEndOffset(),\n    };\n\n    var new_node = focus_node;\n    var new_offset = focus_offset;\n\n    if (isTextNode(focus_node)) {\n        if (forward) {\n            const len = focus_node.getLength();\n            if (focus_offset < len) {\n                new_offset += 1;\n            } else if (nextTextNode(focus_node)) |next| {\n                new_node = next;\n                new_offset = 0;\n            }\n        } else {\n            if (focus_offset > 0) {\n                new_offset -= 1;\n            } else if (prevTextNode(focus_node)) |prev| {\n                new_node = prev;\n                new_offset = prev.getLength();\n            }\n        }\n    } else {\n        if (forward) {\n            if (focus_node.getChildAt(focus_offset)) |child| {\n                if (isTextNode(child)) {\n                    new_node = child;\n                    new_offset = 0;\n                } else if (nextTextNode(child)) |t| {\n                    new_node = t;\n                    new_offset = 0;\n                }\n            } else if (nextTextNodeAfter(focus_node)) |next| {\n                new_node = next;\n                new_offset = 1;\n            }\n        } else {\n            // backward element-node case\n            var idx = focus_offset;\n            while (idx > 0) {\n                idx -= 1;\n                const child = focus_node.getChildAt(idx) orelse break;\n                var bottom = child;\n                while (bottom.lastChild()) |c| bottom = c;\n                if (isTextNode(bottom)) {\n                    new_node = bottom;\n                    new_offset = bottom.getLength();\n                    break;\n                }\n            }\n        }\n    }\n\n    try self.applyModify(alter, new_node, new_offset, page);\n}\n\nfn isWordChar(c: u8) bool {\n    return std.ascii.isAlphanumeric(c) or c == '_';\n}\n\nfn nextWordEnd(text: []const u8, offset: u32) u32 {\n    var i = offset;\n    // consumes whitespace till next word\n    while (i < text.len and !isWordChar(text[i])) : (i += 1) {}\n    // consumes next word\n    while (i < text.len and isWordChar(text[i])) : (i += 1) {}\n    return i;\n}\n\nfn prevWordStart(text: []const u8, offset: u32) u32 {\n    var i = offset;\n    if (i > 0) i -= 1;\n    // consumes the white space\n    while (i > 0 and !isWordChar(text[i])) : (i -= 1) {}\n    // consumes the last word\n    while (i > 0 and isWordChar(text[i - 1])) : (i -= 1) {}\n    return i;\n}\n\nfn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, page: *Page) !void {\n    const abstract = range.asAbstractRange();\n\n    const focus_node = switch (self._direction) {\n        .backward => abstract.getStartContainer(),\n        .forward, .none => abstract.getEndContainer(),\n    };\n    const focus_offset = switch (self._direction) {\n        .backward => abstract.getStartOffset(),\n        .forward, .none => abstract.getEndOffset(),\n    };\n\n    var new_node = focus_node;\n    var new_offset = focus_offset;\n\n    if (isTextNode(focus_node)) {\n        if (forward) {\n            const i = nextWordEnd(new_node.getData().str(), new_offset);\n            if (i > new_offset) {\n                new_offset = i;\n            } else if (nextTextNode(focus_node)) |next| {\n                new_node = next;\n                new_offset = nextWordEnd(next.getData().str(), 0);\n            }\n        } else {\n            const i = prevWordStart(new_node.getData().str(), new_offset);\n            if (i < new_offset) {\n                new_offset = i;\n            } else if (prevTextNode(focus_node)) |prev| {\n                new_node = prev;\n                new_offset = prevWordStart(prev.getData().str(), @intCast(prev.getData().len));\n            }\n        }\n    } else {\n        // Search and apply rules on the next Text Node.\n        // This is either next (on forward) or previous (on backward).\n\n        if (forward) {\n            const child = focus_node.getChildAt(focus_offset) orelse {\n                if (nextTextNodeAfter(focus_node)) |next| {\n                    new_node = next;\n                    new_offset = nextWordEnd(next.getData().str(), 0);\n                }\n                return self.applyModify(alter, new_node, new_offset, page);\n            };\n\n            const t = if (isTextNode(child)) child else nextTextNode(child) orelse {\n                return self.applyModify(alter, new_node, new_offset, page);\n            };\n\n            new_node = t;\n            new_offset = nextWordEnd(t.getData().str(), 0);\n        } else {\n            var idx = focus_offset;\n            while (idx > 0) {\n                idx -= 1;\n                const child = focus_node.getChildAt(idx) orelse break;\n                var bottom = child;\n                while (bottom.lastChild()) |c| bottom = c;\n                if (isTextNode(bottom)) {\n                    new_node = bottom;\n                    new_offset = prevWordStart(bottom.getData().str(), bottom.getLength());\n                    break;\n                }\n            }\n        }\n    }\n\n    try self.applyModify(alter, new_node, new_offset, page);\n}\n\nfn applyModify(self: *Selection, alter: ModifyAlter, new_node: *Node, new_offset: u32, page: *Page) !void {\n    switch (alter) {\n        .move => {\n            const new_range = try Range.init(page);\n            try new_range.setStart(new_node, new_offset);\n            try new_range.setEnd(new_node, new_offset);\n\n            self.setRange(new_range, page);\n            self._direction = .none;\n            try dispatchSelectionChangeEvent(page);\n        },\n        .extend => try self.extend(new_node, new_offset, page),\n    }\n}\n\npub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {\n    if (parent._type == .document_type) return error.InvalidNodeType;\n\n    // If the node is not contained in the document, do not change the selection\n    if (!page.document.asNode().contains(parent)) {\n        return;\n    }\n\n    const range = try Range.init(page);\n    try range.setStart(parent, 0);\n\n    const child_count = parent.getChildrenCount();\n    try range.setEnd(parent, @intCast(child_count));\n\n    self.setRange(range, page);\n    self._direction = .forward;\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn setBaseAndExtent(\n    self: *Selection,\n    anchor_node: *Node,\n    anchor_offset: u32,\n    focus_node: *Node,\n    focus_offset: u32,\n    page: *Page,\n) !void {\n    if (anchor_offset > anchor_node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    if (focus_offset > focus_node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    const cmp = AbstractRange.compareBoundaryPoints(\n        anchor_node,\n        anchor_offset,\n        focus_node,\n        focus_offset,\n    );\n\n    const range = try Range.init(page);\n\n    switch (cmp) {\n        .before => {\n            try range.setStart(anchor_node, anchor_offset);\n            try range.setEnd(focus_node, focus_offset);\n            self._direction = .forward;\n        },\n        .after => {\n            try range.setStart(focus_node, focus_offset);\n            try range.setEnd(anchor_node, anchor_offset);\n            self._direction = .backward;\n        },\n        .equal => {\n            try range.setStart(anchor_node, anchor_offset);\n            try range.setEnd(anchor_node, anchor_offset);\n            self._direction = .none;\n        },\n    }\n\n    self.setRange(range, page);\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !void {\n    const node = _node orelse {\n        try self.removeAllRanges(page);\n        return;\n    };\n\n    if (node._type == .document_type) return error.InvalidNodeType;\n\n    const offset = _offset orelse 0;\n    if (offset > node.getLength()) {\n        return error.IndexSizeError;\n    }\n\n    // If the node is not contained in the document, do not change the selection\n    if (!page.document.asNode().contains(node)) {\n        return;\n    }\n\n    const range = try Range.init(page);\n    try range.setStart(node, offset);\n    try range.setEnd(node, offset);\n\n    self.setRange(range, page);\n    self._direction = .none;\n    try dispatchSelectionChangeEvent(page);\n}\n\npub fn toString(self: *const Selection, page: *Page) ![]const u8 {\n    const range = self._range orelse return \"\";\n    return try range.toString(page);\n}\n\nfn setRange(self: *Selection, new_range: ?*Range, page: *Page) void {\n    if (self._range) |existing| {\n        existing.deinit(false, page._session);\n    }\n    if (new_range) |nr| {\n        nr.asAbstractRange().acquireRef();\n    }\n    self._range = new_range;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Selection);\n\n    pub const Meta = struct {\n        pub const name = \"Selection\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const finalizer = bridge.finalizer(Selection.deinit);\n    };\n\n    pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{});\n    pub const anchorOffset = bridge.accessor(Selection.getAnchorOffset, null, .{});\n    pub const direction = bridge.accessor(Selection.getDirection, null, .{});\n    pub const focusNode = bridge.accessor(Selection.getFocusNode, null, .{});\n    pub const focusOffset = bridge.accessor(Selection.getFocusOffset, null, .{});\n    pub const isCollapsed = bridge.accessor(Selection.getIsCollapsed, null, .{});\n    pub const rangeCount = bridge.accessor(Selection.getRangeCount, null, .{});\n    pub const @\"type\" = bridge.accessor(Selection.getType, null, .{});\n\n    pub const addRange = bridge.function(Selection.addRange, .{});\n    pub const collapse = bridge.function(Selection.collapse, .{ .dom_exception = true });\n    pub const collapseToEnd = bridge.function(Selection.collapseToEnd, .{});\n    pub const collapseToStart = bridge.function(Selection.collapseToStart, .{ .dom_exception = true });\n    pub const containsNode = bridge.function(Selection.containsNode, .{});\n    pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{});\n    pub const empty = bridge.function(Selection.removeAllRanges, .{});\n    pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true });\n    // unimplemented: getComposedRanges\n    pub const getRangeAt = bridge.function(Selection.getRangeAt, .{ .dom_exception = true });\n    pub const modify = bridge.function(Selection.modify, .{});\n    pub const removeAllRanges = bridge.function(Selection.removeAllRanges, .{});\n    pub const removeRange = bridge.function(Selection.removeRange, .{ .dom_exception = true });\n    pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{ .dom_exception = true });\n    pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{ .dom_exception = true });\n    pub const setPosition = bridge.function(Selection.collapse, .{ .dom_exception = true });\n    pub const toString = bridge.function(Selection.toString, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Selection\" {\n    try testing.htmlRunner(\"selection.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/ShadowRoot.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Node = @import(\"Node.zig\");\nconst DocumentFragment = @import(\"DocumentFragment.zig\");\nconst Element = @import(\"Element.zig\");\n\nconst ShadowRoot = @This();\n\npub const Mode = enum {\n    open,\n    closed,\n\n    pub fn fromString(str: []const u8) !Mode {\n        return std.meta.stringToEnum(Mode, str) orelse error.InvalidMode;\n    }\n};\n\n_proto: *DocumentFragment,\n_mode: Mode,\n_host: *Element,\n_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},\n_removed_ids: std.StringHashMapUnmanaged(void) = .{},\n_adopted_style_sheets: ?js.Object.Global = null,\n\npub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {\n    return page._factory.documentFragment(ShadowRoot{\n        ._proto = undefined,\n        ._mode = mode,\n        ._host = host,\n    });\n}\n\npub fn asDocumentFragment(self: *ShadowRoot) *DocumentFragment {\n    return self._proto;\n}\n\npub fn asNode(self: *ShadowRoot) *Node {\n    return self._proto.asNode();\n}\n\npub fn asEventTarget(self: *ShadowRoot) *@import(\"EventTarget.zig\") {\n    return self.asNode().asEventTarget();\n}\n\npub fn getMode(self: *const ShadowRoot) []const u8 {\n    return @tagName(self._mode);\n}\n\npub fn getHost(self: *const ShadowRoot) *Element {\n    return self._host;\n}\n\npub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element {\n    if (id.len == 0) {\n        return null;\n    }\n\n    // Fast path: ID is in the map\n    if (self._elements_by_id.get(id)) |element| {\n        return element;\n    }\n\n    // Slow path: ID was removed but might have duplicates\n    if (self._removed_ids.remove(id)) {\n        // Do a tree walk to find another element with this ID\n        var tw = @import(\"TreeWalker.zig\").Full.Elements.init(self.asNode(), .{});\n        while (tw.next()) |el| {\n            const element_id = el.getAttributeSafe(comptime .wrap(\"id\")) orelse continue;\n            if (std.mem.eql(u8, element_id, id)) {\n                // we ignore this error to keep getElementById easy to call\n                // if it really failed, then we're out of memory and nothing's\n                // going to work like it should anyways.\n                const owned_id = page.dupeString(id) catch return null;\n                self._elements_by_id.put(page.arena, owned_id, el) catch return null;\n                return el;\n            }\n        }\n    }\n\n    return null;\n}\n\npub fn getAdoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object.Global {\n    if (self._adopted_style_sheets) |ass| {\n        return ass;\n    }\n    const js_arr = page.js.local.?.newArray(0);\n    const js_obj = js_arr.toObject();\n    self._adopted_style_sheets = try js_obj.persist();\n    return self._adopted_style_sheets.?;\n}\n\npub fn setAdoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {\n    self._adopted_style_sheets = try sheets.persist();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ShadowRoot);\n\n    pub const Meta = struct {\n        pub const name = \"ShadowRoot\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});\n    pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});\n    pub const getElementById = bridge.function(_getElementById, .{});\n    fn _getElementById(self: *ShadowRoot, value_: ?js.Value, page: *Page) !?*Element {\n        const value = value_ orelse return null;\n        if (value.isNull()) {\n            return self.getElementById(\"null\", page);\n        }\n        if (value.isUndefined()) {\n            return self.getElementById(\"undefined\", page);\n        }\n        return self.getElementById(try value.toZig([]const u8), page);\n    }\n    pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: ShadowRoot\" {\n    try testing.htmlRunner(\"shadowroot\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/StorageManager.zig",
    "content": "// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\n\npub fn registerTypes() []const type {\n    return &.{ StorageManager, StorageEstimate };\n}\n\nconst StorageManager = @This();\n\n_pad: bool = false,\n\npub fn estimate(_: *const StorageManager, page: *Page) !js.Promise {\n    const est = try page._factory.create(StorageEstimate{\n        ._usage = 0,\n        ._quota = 1024 * 1024 * 1024, // 1 GiB\n    });\n    return page.js.local.?.resolvePromise(est);\n}\n\nconst StorageEstimate = struct {\n    _quota: u64,\n    _usage: u64,\n\n    fn getUsage(self: *const StorageEstimate) u64 {\n        return self._usage;\n    }\n\n    fn getQuota(self: *const StorageEstimate) u64 {\n        return self._quota;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(StorageEstimate);\n        pub const Meta = struct {\n            pub const name = \"StorageEstimate\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n        pub const quota = bridge.accessor(getQuota, null, .{});\n        pub const usage = bridge.accessor(getUsage, null, .{});\n    };\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(StorageManager);\n    pub const Meta = struct {\n        pub const name = \"StorageManager\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n    pub const estimate = bridge.function(StorageManager.estimate, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/SubtleCrypto.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"../../log.zig\");\n\nconst crypto = @import(\"../../crypto.zig\");\nconst DOMException = @import(\"DOMException.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst js = @import(\"../js/js.zig\");\n\npub fn registerTypes() []const type {\n    return &.{ SubtleCrypto, CryptoKey };\n}\n\n/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level\n/// cryptographic functions.\n/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto\n/// https://w3c.github.io/webcrypto/#subtlecrypto-interface\nconst SubtleCrypto = @This();\n/// Don't optimize away the type.\n_pad: bool = false,\n\nconst Algorithm = union(enum) {\n    /// For RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP: pass an RsaHashedKeyGenParams object.\n    rsa_hashed_key_gen: RsaHashedKeyGen,\n    /// For HMAC: pass an HmacKeyGenParams object.\n    hmac_key_gen: HmacKeyGen,\n    /// Can be Ed25519 or X25519.\n    name: []const u8,\n    /// Can be Ed25519 or X25519.\n    object: struct { name: []const u8 },\n\n    /// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams\n    const RsaHashedKeyGen = struct {\n        name: []const u8,\n        /// This should be at least 2048.\n        /// Some organizations are now recommending that it should be 4096.\n        modulusLength: u32,\n        publicExponent: js.TypedArray(u8),\n        hash: union(enum) {\n            string: []const u8,\n            object: struct { name: []const u8 },\n        },\n    };\n\n    /// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams\n    const HmacKeyGen = struct {\n        /// Always HMAC.\n        name: []const u8,\n        /// Its also possible to pass this in an object.\n        hash: union(enum) {\n            string: []const u8,\n            object: struct { name: []const u8 },\n        },\n        /// If omitted, default is the block size of the chosen hash function.\n        length: ?usize,\n    };\n    /// Alias.\n    const HmacImport = HmacKeyGen;\n\n    const EcdhKeyDeriveParams = struct {\n        /// Can be Ed25519 or X25519.\n        name: []const u8,\n        public: *const CryptoKey,\n    };\n\n    /// Algorithm for deriveBits() and deriveKey().\n    const DeriveBits = union(enum) {\n        ecdh_or_x25519: EcdhKeyDeriveParams,\n    };\n};\n\n/// Generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).\npub fn generateKey(\n    _: *const SubtleCrypto,\n    algorithm: Algorithm,\n    extractable: bool,\n    key_usages: []const []const u8,\n    page: *Page,\n) !js.Promise {\n    const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| {\n        return page.js.local.?.rejectPromise(@errorName(err));\n    };\n\n    return page.js.local.?.resolvePromise(key_or_pair);\n}\n\n/// Exports a key: that is, it takes as input a CryptoKey object and gives you\n/// the key in an external, portable format.\npub fn exportKey(\n    _: *const SubtleCrypto,\n    format: []const u8,\n    key: *CryptoKey,\n    page: *Page,\n) !js.Promise {\n    if (!key.canExportKey()) {\n        return error.InvalidAccessError;\n    }\n\n    if (std.mem.eql(u8, format, \"raw\")) {\n        return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key });\n    }\n\n    const is_unsupported = std.mem.eql(u8, format, \"pkcs8\") or\n        std.mem.eql(u8, format, \"spki\") or std.mem.eql(u8, format, \"jwk\");\n\n    if (is_unsupported) {\n        log.warn(.not_implemented, \"SubtleCrypto.exportKey\", .{ .format = format });\n    }\n\n    return page.js.local.?.rejectPromise(@errorName(error.NotSupported));\n}\n\n/// Derive a secret key from a master key.\npub fn deriveBits(\n    _: *const SubtleCrypto,\n    algorithm: Algorithm.DeriveBits,\n    base_key: *const CryptoKey, // Private key.\n    length: usize,\n    page: *Page,\n) !js.Promise {\n    return switch (algorithm) {\n        .ecdh_or_x25519 => |p| {\n            const name = p.name;\n            if (std.mem.eql(u8, name, \"X25519\")) {\n                return page.js.local.?.resolvePromise(base_key.deriveBitsX25519(p.public, length, page));\n            }\n\n            if (std.mem.eql(u8, name, \"ECDH\")) {\n                log.warn(.not_implemented, \"SubtleCrypto.deriveBits\", .{ .name = name });\n            }\n\n            return page.js.local.?.rejectPromise(@errorName(error.NotSupported));\n        },\n    };\n}\n\nconst SignatureAlgorithm = union(enum) {\n    string: []const u8,\n    object: struct { name: []const u8 },\n\n    pub fn isHMAC(self: SignatureAlgorithm) bool {\n        const name = switch (self) {\n            .string => |string| string,\n            .object => |object| object.name,\n        };\n\n        if (name.len < 4) return false;\n        const hmac: u32 = @bitCast([4]u8{ 'H', 'M', 'A', 'C' });\n        return @as(u32, @bitCast(name[0..4].*)) == hmac;\n    }\n};\n\n/// Generate a digital signature.\npub fn sign(\n    _: *const SubtleCrypto,\n    /// This can either be provided as string or object.\n    /// We can't use the `Algorithm` type defined before though since there\n    /// are couple of changes between the two.\n    /// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm\n    algorithm: SignatureAlgorithm,\n    key: *CryptoKey,\n    data: []const u8, // ArrayBuffer.\n    page: *Page,\n) !js.Promise {\n    return switch (key._type) {\n        .hmac => {\n            // Verify algorithm.\n            if (!algorithm.isHMAC()) {\n                return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));\n            }\n\n            // Call sign for HMAC.\n            const result = key.signHMAC(data, page) catch |err| {\n                return page.js.local.?.rejectPromise(@errorName(err));\n            };\n\n            return page.js.local.?.resolvePromise(result);\n        },\n        else => {\n            log.warn(.not_implemented, \"SubtleCrypto.sign\", .{ .key_type = key._type });\n            return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));\n        },\n    };\n}\n\n/// Verify a digital signature.\npub fn verify(\n    _: *const SubtleCrypto,\n    algorithm: SignatureAlgorithm,\n    key: *const CryptoKey,\n    signature: []const u8, // ArrayBuffer.\n    data: []const u8, // ArrayBuffer.\n    page: *Page,\n) !js.Promise {\n    if (!algorithm.isHMAC()) return error.InvalidAccessError;\n\n    return switch (key._type) {\n        .hmac => key.verifyHMAC(signature, data, page),\n        else => return error.InvalidAccessError,\n    };\n}\n\npub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {\n    const local = page.js.local.?;\n    if (algorithm.len > 10) {\n        return local.rejectPromise(DOMException.fromError(error.NotSupported));\n    }\n    const normalized = std.ascii.lowerString(&page.buf, algorithm);\n    if (std.mem.eql(u8, normalized, \"sha-1\")) {\n        const Sha1 = std.crypto.hash.Sha1;\n        Sha1.hash(data.values, page.buf[0..Sha1.digest_length], .{});\n        return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha1.digest_length] });\n    }\n    if (std.mem.eql(u8, normalized, \"sha-256\")) {\n        const Sha256 = std.crypto.hash.sha2.Sha256;\n        Sha256.hash(data.values, page.buf[0..Sha256.digest_length], .{});\n        return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha256.digest_length] });\n    }\n    if (std.mem.eql(u8, normalized, \"sha-384\")) {\n        const Sha384 = std.crypto.hash.sha2.Sha384;\n        Sha384.hash(data.values, page.buf[0..Sha384.digest_length], .{});\n        return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha384.digest_length] });\n    }\n    if (std.mem.eql(u8, normalized, \"sha-512\")) {\n        const Sha512 = std.crypto.hash.sha2.Sha512;\n        Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});\n        return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });\n    }\n    return local.rejectPromise(DOMException.fromError(error.NotSupported));\n}\n\n/// Returns the desired digest by its name.\nfn findDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {\n    if (std.mem.eql(u8, \"SHA-256\", name)) {\n        return crypto.EVP_sha256();\n    }\n\n    if (std.mem.eql(u8, \"SHA-384\", name)) {\n        return crypto.EVP_sha384();\n    }\n\n    if (std.mem.eql(u8, \"SHA-512\", name)) {\n        return crypto.EVP_sha512();\n    }\n\n    if (std.mem.eql(u8, \"SHA-1\", name)) {\n        return crypto.EVP_sha1();\n    }\n\n    return error.Invalid;\n}\n\nconst KeyOrPair = union(enum) { key: *CryptoKey, pair: CryptoKeyPair };\n\n/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair\nconst CryptoKeyPair = struct {\n    privateKey: *CryptoKey,\n    publicKey: *CryptoKey,\n};\n\n/// Represents a cryptographic key obtained from one of the SubtleCrypto methods\n/// generateKey(), deriveKey(), importKey(), or unwrapKey().\npub const CryptoKey = struct {\n    /// Algorithm being used.\n    _type: Type,\n    /// Whether the key is extractable.\n    _extractable: bool,\n    /// Bit flags of `usages`; see `Usages` type.\n    _usages: u8,\n    /// Raw bytes of key.\n    _key: []const u8,\n    /// Different algorithms may use different data structures;\n    /// this union can be used for such situations. Active field is understood\n    /// from `_type`.\n    _vary: extern union {\n        /// Used by HMAC.\n        digest: *const crypto.EVP_MD,\n        /// Used by asymmetric algorithms (X25519, Ed25519).\n        pkey: *crypto.EVP_PKEY,\n    },\n\n    pub const Type = enum(u8) { hmac, rsa, x25519 };\n\n    /// Changing the names of fields would affect bitmask creation.\n    pub const Usages = struct {\n        // zig fmt: off\n        pub const encrypt    = 0x001;\n        pub const decrypt    = 0x002;\n        pub const sign       = 0x004;\n        pub const verify     = 0x008;\n        pub const deriveKey  = 0x010;\n        pub const deriveBits = 0x020;\n        pub const wrapKey    = 0x040;\n        pub const unwrapKey  = 0x080;\n        // zig fmt: on\n    };\n\n    pub fn init(\n        algorithm: Algorithm,\n        extractable: bool,\n        key_usages: []const []const u8,\n        page: *Page,\n    ) !KeyOrPair {\n        return switch (algorithm) {\n            .hmac_key_gen => |hmac| initHMAC(hmac, extractable, key_usages, page),\n            .name => |name| {\n                if (std.mem.eql(u8, \"X25519\", name)) {\n                    return initX25519(extractable, key_usages, page);\n                }\n                log.warn(.not_implemented, \"CryptoKey.init\", .{ .name = name });\n                return error.NotSupported;\n            },\n            .object => |object| {\n                // Ditto.\n                const name = object.name;\n                if (std.mem.eql(u8, \"X25519\", name)) {\n                    return initX25519(extractable, key_usages, page);\n                }\n                log.warn(.not_implemented, \"CryptoKey.init\", .{ .name = name });\n                return error.NotSupported;\n            },\n            else => {\n                log.warn(.not_implemented, \"CryptoKey.init\", .{ .algorithm = algorithm });\n                return error.NotSupported;\n            },\n        };\n    }\n\n    inline fn canSign(self: *const CryptoKey) bool {\n        return self._usages & Usages.sign != 0;\n    }\n\n    inline fn canVerify(self: *const CryptoKey) bool {\n        return self._usages & Usages.verify != 0;\n    }\n\n    inline fn canDeriveBits(self: *const CryptoKey) bool {\n        return self._usages & Usages.deriveBits != 0;\n    }\n\n    inline fn canExportKey(self: *const CryptoKey) bool {\n        return self._extractable;\n    }\n\n    /// Only valid for HMAC.\n    inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD {\n        return self._vary.digest;\n    }\n\n    /// Only valid for asymmetric algorithms (X25519, Ed25519).\n    inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY {\n        return self._vary.pkey;\n    }\n\n    // HMAC.\n\n    fn initHMAC(\n        algorithm: Algorithm.HmacKeyGen,\n        extractable: bool,\n        key_usages: []const []const u8,\n        page: *Page,\n    ) !KeyOrPair {\n        const hash = switch (algorithm.hash) {\n            .string => |str| str,\n            .object => |obj| obj.name,\n        };\n        // Find digest.\n        const d = try findDigest(hash);\n\n        // We need at least a single usage.\n        if (key_usages.len == 0) {\n            return error.SyntaxError;\n        }\n        // Calculate usages mask.\n        const decls = @typeInfo(Usages).@\"struct\".decls;\n        var usages_mask: u8 = 0;\n        iter_usages: for (key_usages) |usage| {\n            inline for (decls) |decl| {\n                if (std.mem.eql(u8, decl.name, usage)) {\n                    usages_mask |= @field(Usages, decl.name);\n                    continue :iter_usages;\n                }\n            }\n            // Unknown usage if got here.\n            return error.SyntaxError;\n        }\n\n        const block_size: usize = blk: {\n            // Caller provides this in bits, not bytes.\n            if (algorithm.length) |length| {\n                break :blk length / 8;\n            }\n            // Prefer block size of the hash function instead.\n            break :blk crypto.EVP_MD_block_size(d);\n        };\n\n        const key = try page.arena.alloc(u8, block_size);\n        errdefer page.arena.free(key);\n\n        // HMAC is simply CSPRNG.\n        const res = crypto.RAND_bytes(key.ptr, key.len);\n        lp.assert(res == 1, \"SubtleCrypto.initHMAC\", .{ .res = res });\n\n        const crypto_key = try page._factory.create(CryptoKey{\n            ._type = .hmac,\n            ._extractable = extractable,\n            ._usages = usages_mask,\n            ._key = key,\n            ._vary = .{ .digest = d },\n        });\n\n        return .{ .key = crypto_key };\n    }\n\n    fn signHMAC(self: *const CryptoKey, data: []const u8, page: *Page) !js.ArrayBuffer {\n        if (!self.canSign()) {\n            return error.InvalidAccessError;\n        }\n\n        const buffer = try page.call_arena.alloc(u8, crypto.EVP_MD_size(self.getDigest()));\n        errdefer page.call_arena.free(buffer);\n        var out_len: u32 = 0;\n        // Try to sign.\n        const signed = crypto.HMAC(\n            self.getDigest(),\n            @ptrCast(self._key.ptr),\n            self._key.len,\n            data.ptr,\n            data.len,\n            buffer.ptr,\n            &out_len,\n        );\n\n        if (signed != null) {\n            return js.ArrayBuffer{ .values = buffer[0..out_len] };\n        }\n\n        // Not DOM exception, failed on our side.\n        return error.Invalid;\n    }\n\n    fn verifyHMAC(\n        self: *const CryptoKey,\n        signature: []const u8,\n        data: []const u8,\n        page: *Page,\n    ) !js.Promise {\n        if (!self.canVerify()) {\n            return error.InvalidAccessError;\n        }\n\n        var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined;\n        var out_len: u32 = 0;\n        // Try to sign.\n        const signed = crypto.HMAC(\n            self.getDigest(),\n            @ptrCast(self._key.ptr),\n            self._key.len,\n            data.ptr,\n            data.len,\n            &buffer,\n            &out_len,\n        );\n\n        if (signed != null) {\n            // CRYPTO_memcmp compare in constant time so prohibits time-based attacks.\n            const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);\n            return page.js.local.?.resolvePromise(res == 0);\n        }\n\n        return page.js.local.?.resolvePromise(false);\n    }\n\n    // X25519.\n\n    /// Create a pair of X25519.\n    fn initX25519(\n        extractable: bool,\n        key_usages: []const []const u8,\n        page: *Page,\n    ) !KeyOrPair {\n        // This code has too many allocations here and there, might be nice to\n        // gather them together with a single alloc call. Not sure if factory\n        // pattern is suitable for it though.\n\n        // Calculate usages; only matters for private key.\n        // Only deriveKey() and deriveBits() be used for X25519.\n        if (key_usages.len == 0) {\n            return error.SyntaxError;\n        }\n        var mask: u8 = 0;\n        iter_usages: for (key_usages) |usage| {\n            inline for ([_][]const u8{ \"deriveKey\", \"deriveBits\" }) |name| {\n                if (std.mem.eql(u8, name, usage)) {\n                    mask |= @field(Usages, name);\n                    continue :iter_usages;\n                }\n            }\n            // Unknown usage if got here.\n            return error.SyntaxError;\n        }\n\n        const public_value = try page.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);\n        errdefer page.arena.free(public_value);\n\n        const private_key = try page.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);\n        errdefer page.arena.free(private_key);\n\n        // There's no info about whether this can fail; so I assume it cannot.\n        crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key));\n\n        // Create EVP_PKEY for public key.\n        // Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome\n        // prefer not to, yet BoringSSL added it and recommends instead of what\n        // we're doing currently.\n        const public_pkey = crypto.EVP_PKEY_new_raw_public_key(\n            crypto.EVP_PKEY_X25519,\n            null,\n            public_value.ptr,\n            public_value.len,\n        );\n        if (public_pkey == null) {\n            return error.OutOfMemory;\n        }\n\n        // Create EVP_PKEY for private key.\n        // Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome\n        // prefer not to, yet BoringSSL added it and recommends instead of what\n        // we're doing currently.\n        const private_pkey = crypto.EVP_PKEY_new_raw_private_key(\n            crypto.EVP_PKEY_X25519,\n            null,\n            private_key.ptr,\n            private_key.len,\n        );\n        if (private_pkey == null) {\n            return error.OutOfMemory;\n        }\n\n        const private = try page._factory.create(CryptoKey{\n            ._type = .x25519,\n            ._extractable = extractable,\n            ._usages = mask,\n            ._key = private_key,\n            ._vary = .{ .pkey = private_pkey.? },\n        });\n        errdefer page._factory.destroy(private);\n\n        const public = try page._factory.create(CryptoKey{\n            ._type = .x25519,\n            // Public keys are always extractable.\n            ._extractable = true,\n            // Always empty for public key.\n            ._usages = 0,\n            ._key = public_value,\n            ._vary = .{ .pkey = public_pkey.? },\n        });\n        errdefer page._factory.destroy(public);\n\n        return .{ .pair = .{ .privateKey = private, .publicKey = public } };\n    }\n\n    fn deriveBitsX25519(\n        private: *const CryptoKey,\n        public: *const CryptoKey,\n        length_in_bits: usize,\n        page: *Page,\n    ) !js.ArrayBuffer {\n        if (!private.canDeriveBits()) {\n            return error.InvalidAccessError;\n        }\n\n        const maybe_ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null);\n        if (maybe_ctx) |ctx| {\n            // Context is valid, free it on failure.\n            errdefer crypto.EVP_PKEY_CTX_free(ctx);\n\n            // Init derive operation and set public key as peer.\n            if (crypto.EVP_PKEY_derive_init(ctx) != 1 or\n                crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1)\n            {\n                // Failed on our end.\n                return error.Internal;\n            }\n\n            const derived_key = try page.call_arena.alloc(u8, 32);\n            errdefer page.call_arena.free(derived_key);\n\n            var out_key_len: usize = derived_key.len;\n            const result = crypto.EVP_PKEY_derive(ctx, derived_key.ptr, &out_key_len);\n            if (result != 1) {\n                // Failed on our end.\n                return error.Internal;\n            }\n            // Sanity check.\n            lp.assert(derived_key.len == out_key_len, \"SubtleCrypto.deriveBitsX25519\", .{});\n\n            // Length is in bits, convert to byte length.\n            const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8;\n            // Truncate the slice to specified length.\n            // Same as `derived_key`.\n            const tailored = blk: {\n                if (length > derived_key.len) {\n                    return error.LengthTooLong;\n                }\n                break :blk derived_key[0..length];\n            };\n\n            // Zero any \"unused bits\" in the final byte.\n            const remainder_bits: u3 = @intCast(length_in_bits % 8);\n            if (remainder_bits != 0) {\n                tailored[tailored.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits);\n            }\n\n            return js.ArrayBuffer{ .values = tailored };\n        }\n\n        // Failed on our end.\n        return error.Internal;\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(CryptoKey);\n\n        pub const Meta = struct {\n            pub const name = \"CryptoKey\";\n\n            pub var class_id: bridge.ClassId = undefined;\n            pub const prototype_chain = bridge.prototypeChain();\n        };\n    };\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(SubtleCrypto);\n\n    pub const Meta = struct {\n        pub const name = \"SubtleCrypto\";\n\n        pub var class_id: bridge.ClassId = undefined;\n        pub const prototype_chain = bridge.prototypeChain();\n    };\n\n    pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{ .dom_exception = true });\n    pub const exportKey = bridge.function(SubtleCrypto.exportKey, .{ .dom_exception = true });\n    pub const sign = bridge.function(SubtleCrypto.sign, .{ .dom_exception = true });\n    pub const verify = bridge.function(SubtleCrypto.verify, .{ .dom_exception = true });\n    pub const deriveBits = bridge.function(SubtleCrypto.deriveBits, .{ .dom_exception = true });\n    pub const digest = bridge.function(SubtleCrypto.digest, .{ .dom_exception = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/TreeWalker.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst Node = @import(\"Node.zig\");\nconst Element = @import(\"Element.zig\");\n\npub const Full = TreeWalker(.full);\npub const FullExcludeSelf = TreeWalker(.exclude_self);\npub const Children = TreeWalker(.children);\n\nconst Mode = enum {\n    full,\n    children,\n    exclude_self,\n};\n\npub fn TreeWalker(comptime mode: Mode) type {\n    return struct {\n        _current: ?*Node = null,\n        _next: ?*Node,\n        _root: *Node,\n\n        const Self = @This();\n        const Opts = struct {};\n\n        pub fn init(root: *Node, opts: Opts) Self {\n            _ = opts;\n            return .{\n                ._next = firstNext(root),\n                ._root = root,\n            };\n        }\n\n        pub fn next(self: *Self) ?*Node {\n            const node = self._next orelse return null;\n            self._current = node;\n\n            if (comptime mode == .children) {\n                self._next = node.nextSibling();\n                return node;\n            }\n\n            if (node.firstChild()) |child| {\n                self._next = child;\n            } else {\n                var current: *Node = node;\n                while (current != self._root) {\n                    if (current.nextSibling()) |sibling| {\n                        self._next = sibling;\n                        return node;\n                    }\n                    current = current._parent orelse break;\n                }\n                self._next = null;\n            }\n            return node;\n        }\n\n        pub fn skipChildren(self: *Self) void {\n            if (comptime mode == .children) return;\n            const current_node = self._current orelse return;\n\n            var current: *Node = current_node;\n            while (current != self._root) {\n                if (current.nextSibling()) |sibling| {\n                    self._next = sibling;\n                    return;\n                }\n                current = current._parent orelse break;\n            }\n            self._next = null;\n        }\n\n        pub fn reset(self: *Self) void {\n            self._current = null;\n            self._next = firstNext(self._root);\n        }\n\n        pub fn contains(self: *const Self, target: *const Node) bool {\n            const root = self._root;\n\n            if (comptime mode == .children) {\n                var it = root.childrenIterator();\n                while (it.next()) |child| {\n                    if (child == target) {\n                        return true;\n                    }\n                }\n                return false;\n            }\n\n            var node = target;\n            if ((comptime mode == .exclude_self) and node == root) {\n                return false;\n            }\n\n            while (true) {\n                if (node == root) {\n                    return true;\n                }\n                node = node._parent orelse return false;\n            }\n        }\n\n        pub fn clone(self: *const Self) Self {\n            const root = self._root;\n            return .{\n                ._next = firstNext(root),\n                ._root = root,\n            };\n        }\n\n        fn firstNext(root: *Node) ?*Node {\n            return switch (comptime mode) {\n                .full => root,\n                .exclude_self => root.firstChild(),\n                .children => root.firstChild(),\n            };\n        }\n\n        pub const Elements = struct {\n            tw: Self,\n\n            pub fn init(root: *Node, comptime opts: Opts) Elements {\n                return .{\n                    .tw = Self.init(root, opts),\n                };\n            }\n\n            pub fn next(self: *Elements) ?*Element {\n                while (self.tw.next()) |node| {\n                    if (node.is(Element)) |el| {\n                        return el;\n                    }\n                }\n                return null;\n            }\n\n            pub fn reset(self: *Elements) void {\n                self.tw.reset();\n            }\n        };\n    };\n}\n\ntest \"TreeWalker: skipChildren\" {\n    const testing = @import(\"../../testing.zig\");\n    const page = try testing.test_session.createPage();\n    defer testing.test_session.removePage();\n    const doc = page.window._document;\n\n    // <div>\n    //   <span>\n    //     <b>A</b>\n    //   </span>\n    //   <p>B</p>\n    // </div>\n    const div = try doc.createElement(\"div\", null, page);\n    const span = try doc.createElement(\"span\", null, page);\n    const b = try doc.createElement(\"b\", null, page);\n    const p = try doc.createElement(\"p\", null, page);\n    _ = try span.asNode().appendChild(b.asNode(), page);\n    _ = try div.asNode().appendChild(span.asNode(), page);\n    _ = try div.asNode().appendChild(p.asNode(), page);\n\n    var tw = Full.init(div.asNode(), .{});\n\n    // root (div)\n    try testing.expect(tw.next() == div.asNode());\n\n    // span\n    try testing.expect(tw.next() == span.asNode());\n\n    // skip children of span (should jump over <b> to <p>)\n    tw.skipChildren();\n    try testing.expect(tw.next() == p.asNode());\n\n    try testing.expect(tw.next() == null);\n}\n"
  },
  {
    "path": "src/browser/webapi/URL.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst U = @import(\"../URL.zig\");\nconst Page = @import(\"../Page.zig\");\nconst URLSearchParams = @import(\"net/URLSearchParams.zig\");\nconst Blob = @import(\"Blob.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst URL = @This();\n\n_raw: [:0]const u8,\n_arena: ?Allocator = null,\n_search_params: ?*URLSearchParams = null,\n\n// convenience\npub const resolve = @import(\"../URL.zig\").resolve;\npub const eqlDocument = @import(\"../URL.zig\").eqlDocument;\n\npub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL {\n    const url_is_absolute = @import(\"../URL.zig\").isCompleteHTTPUrl(url);\n\n    const base = if (base_) |b| blk: {\n        // If URL is absolute, base is ignored (but we still use page.url internally)\n        if (url_is_absolute) {\n            break :blk page.url;\n        }\n        // For relative URLs, base must be a valid absolute URL\n        if (!@import(\"../URL.zig\").isCompleteHTTPUrl(b)) {\n            return error.TypeError;\n        }\n        break :blk b;\n    } else if (!url_is_absolute) {\n        return error.TypeError;\n    } else page.url;\n\n    const arena = page.arena;\n    const raw = try resolve(arena, base, url, .{ .always_dupe = true });\n\n    return page._factory.create(URL{\n        ._raw = raw,\n        ._arena = arena,\n    });\n}\n\npub fn getUsername(self: *const URL) []const u8 {\n    return U.getUsername(self._raw);\n}\n\npub fn setUsername(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setUsername(self._raw, value, allocator);\n}\n\npub fn getPassword(self: *const URL) []const u8 {\n    return U.getPassword(self._raw);\n}\n\npub fn setPassword(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setPassword(self._raw, value, allocator);\n}\n\npub fn getPathname(self: *const URL) []const u8 {\n    return U.getPathname(self._raw);\n}\n\npub fn getProtocol(self: *const URL) []const u8 {\n    return U.getProtocol(self._raw);\n}\n\npub fn getHostname(self: *const URL) []const u8 {\n    return U.getHostname(self._raw);\n}\n\npub fn getHost(self: *const URL) []const u8 {\n    return U.getHost(self._raw);\n}\n\npub fn getPort(self: *const URL) []const u8 {\n    return U.getPort(self._raw);\n}\n\npub fn getOrigin(self: *const URL, page: *const Page) ![]const u8 {\n    return (try U.getOrigin(page.call_arena, self._raw)) orelse {\n        // yes, a null string, that's what the spec wants\n        return \"null\";\n    };\n}\n\npub fn getSearch(self: *const URL, page: *const Page) ![]const u8 {\n    // If searchParams has been accessed, generate search from it\n    if (self._search_params) |sp| {\n        if (sp.getSize() == 0) {\n            return \"\";\n        }\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try buf.writer.writeByte('?');\n        try sp.toString(&buf.writer);\n        return buf.written();\n    }\n    return U.getSearch(self._raw);\n}\n\npub fn getHash(self: *const URL) []const u8 {\n    return U.getHash(self._raw);\n}\n\npub fn getSearchParams(self: *URL, page: *Page) !*URLSearchParams {\n    if (self._search_params) |sp| {\n        return sp;\n    }\n\n    // Get current search string (without the '?')\n    const search = try self.getSearch(page);\n    const search_value = if (search.len > 0) search[1..] else \"\";\n\n    const params = try URLSearchParams.init(.{ .query_string = search_value }, page);\n    self._search_params = params;\n    return params;\n}\n\npub fn setHref(self: *URL, value: []const u8, page: *Page) !void {\n    const base = if (U.isCompleteHTTPUrl(value)) page.url else self._raw;\n    const raw = try U.resolve(self._arena orelse page.arena, base, value, .{ .always_dupe = true });\n    self._raw = raw;\n\n    // Update existing searchParams if it exists\n    if (self._search_params) |sp| {\n        const search = U.getSearch(raw);\n        const search_value = if (search.len > 0) search[1..] else \"\";\n        try sp.updateFromString(search_value, page);\n    }\n}\n\npub fn setProtocol(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setProtocol(self._raw, value, allocator);\n}\n\npub fn setHost(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setHost(self._raw, value, allocator);\n}\n\npub fn setHostname(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setHostname(self._raw, value, allocator);\n}\n\npub fn setPort(self: *URL, value: ?[]const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setPort(self._raw, value, allocator);\n}\n\npub fn setPathname(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setPathname(self._raw, value, allocator);\n}\n\npub fn setSearch(self: *URL, value: []const u8, page: *Page) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setSearch(self._raw, value, allocator);\n\n    // Update existing searchParams if it exists\n    if (self._search_params) |sp| {\n        const search = U.getSearch(self._raw);\n        const search_value = if (search.len > 0) search[1..] else \"\";\n        try sp.updateFromString(search_value, page);\n    }\n}\n\npub fn setHash(self: *URL, value: []const u8) !void {\n    const allocator = self._arena orelse return error.NoAllocator;\n    self._raw = try U.setHash(self._raw, value, allocator);\n}\n\npub fn toString(self: *const URL, page: *const Page) ![:0]const u8 {\n    const sp = self._search_params orelse {\n        return self._raw;\n    };\n\n    // Rebuild URL from searchParams\n    const raw = self._raw;\n\n    // Find the base (everything before ? or #)\n    const base_end = std.mem.indexOfAnyPos(u8, raw, 0, \"?#\") orelse raw.len;\n    const base = raw[0..base_end];\n\n    // Get the hash if it exists\n    const hash = self.getHash();\n\n    // Build the new URL string\n    var buf = std.Io.Writer.Allocating.init(page.call_arena);\n    try buf.writer.writeAll(base);\n\n    // Add / if missing (e.g., \"https://example.com\" -> \"https://example.com/\")\n    // Only add if pathname is just \"/\" and not already in the base\n    const pathname = U.getPathname(raw);\n    if (std.mem.eql(u8, pathname, \"/\") and !std.mem.endsWith(u8, base, \"/\")) {\n        try buf.writer.writeByte('/');\n    }\n\n    // Only add ? if there are params\n    if (sp.getSize() > 0) {\n        try buf.writer.writeByte('?');\n        try sp.toString(&buf.writer);\n    }\n\n    try buf.writer.writeAll(hash);\n    try buf.writer.writeByte(0);\n\n    return buf.written()[0 .. buf.written().len - 1 :0];\n}\n\npub fn canParse(url: []const u8, base_: ?[]const u8) bool {\n    if (base_) |b| {\n        return U.isCompleteHTTPUrl(b);\n    }\n    return U.isCompleteHTTPUrl(url);\n}\n\npub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {\n    var uuid_buf: [36]u8 = undefined;\n    @import(\"../../id.zig\").uuidv4(&uuid_buf);\n\n    const blob_url = try std.fmt.allocPrint(\n        page.arena,\n        \"blob:{s}/{s}\",\n        .{ page.origin orelse \"null\", uuid_buf },\n    );\n    try page._blob_urls.put(page.arena, blob_url, blob);\n    // prevent GC from cleaning up the blob while it's in the registry\n    page.js.strongRef(blob);\n    return blob_url;\n}\n\npub fn revokeObjectURL(url: []const u8, page: *Page) void {\n    // Per spec: silently ignore non-blob URLs\n    if (!std.mem.startsWith(u8, url, \"blob:\")) {\n        return;\n    }\n\n    // Remove from registry and release strong ref (no-op if not found)\n    if (page._blob_urls.fetchRemove(url)) |entry| {\n        page.js.weakRef(entry.value);\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(URL);\n\n    pub const Meta = struct {\n        pub const name = \"URL\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(URL.init, .{});\n    pub const canParse = bridge.function(URL.canParse, .{ .static = true });\n    pub const createObjectURL = bridge.function(URL.createObjectURL, .{ .static = true });\n    pub const revokeObjectURL = bridge.function(URL.revokeObjectURL, .{ .static = true });\n    pub const toString = bridge.function(URL.toString, .{});\n    pub const toJSON = bridge.function(URL.toString, .{});\n    pub const href = bridge.accessor(URL.toString, URL.setHref, .{});\n    pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{});\n    pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{});\n    pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{});\n    pub const username = bridge.accessor(URL.getUsername, URL.setUsername, .{});\n    pub const password = bridge.accessor(URL.getPassword, URL.setPassword, .{});\n    pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{});\n    pub const host = bridge.accessor(URL.getHost, URL.setHost, .{});\n    pub const port = bridge.accessor(URL.getPort, URL.setPort, .{});\n    pub const origin = bridge.accessor(URL.getOrigin, null, .{});\n    pub const protocol = bridge.accessor(URL.getProtocol, URL.setProtocol, .{});\n    pub const searchParams = bridge.accessor(URL.getSearchParams, null, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: URL\" {\n    try testing.htmlRunner(\"url.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/VisualViewport.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../js/js.zig\");\nconst Page = @import(\"../Page.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\nconst Window = @import(\"Window.zig\");\n\nconst VisualViewport = @This();\n\n_proto: *EventTarget,\n\npub fn asEventTarget(self: *VisualViewport) *EventTarget {\n    return self._proto;\n}\n\npub fn getPageLeft(_: *const VisualViewport, page: *Page) u32 {\n    return page.window.getScrollX();\n}\n\npub fn getPageTop(_: *const VisualViewport, page: *Page) u32 {\n    return page.window.getScrollY();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(VisualViewport);\n\n    pub const Meta = struct {\n        pub const name = \"VisualViewport\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    // Static viewport properties for headless browser\n    // No pinch-zoom or mobile viewport, so values are straightforward\n    pub const offsetLeft = bridge.property(0, .{ .template = false });\n    pub const offsetTop = bridge.property(0, .{ .template = false });\n    pub const pageLeft = bridge.accessor(VisualViewport.getPageLeft, null, .{});\n    pub const pageTop = bridge.accessor(VisualViewport.getPageTop, null, .{});\n    pub const width = bridge.property(1920, .{ .template = false });\n    pub const height = bridge.property(1080, .{ .template = false });\n    pub const scale = bridge.property(1.0, .{ .template = false });\n};\n"
  },
  {
    "path": "src/browser/webapi/Window.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\nconst builtin = @import(\"builtin\");\n\nconst log = @import(\"../../log.zig\");\nconst Page = @import(\"../Page.zig\");\nconst Console = @import(\"Console.zig\");\nconst History = @import(\"History.zig\");\nconst Navigation = @import(\"navigation/Navigation.zig\");\nconst Crypto = @import(\"Crypto.zig\");\nconst CSS = @import(\"CSS.zig\");\nconst Navigator = @import(\"Navigator.zig\");\nconst Screen = @import(\"Screen.zig\");\nconst VisualViewport = @import(\"VisualViewport.zig\");\nconst Performance = @import(\"Performance.zig\");\nconst Document = @import(\"Document.zig\");\nconst Location = @import(\"Location.zig\");\nconst Fetch = @import(\"net/Fetch.zig\");\nconst Event = @import(\"Event.zig\");\nconst EventTarget = @import(\"EventTarget.zig\");\nconst ErrorEvent = @import(\"event/ErrorEvent.zig\");\nconst MessageEvent = @import(\"event/MessageEvent.zig\");\nconst MediaQueryList = @import(\"css/MediaQueryList.zig\");\nconst storage = @import(\"storage/storage.zig\");\nconst Element = @import(\"Element.zig\");\nconst CSSStyleProperties = @import(\"css/CSSStyleProperties.zig\");\nconst CustomElementRegistry = @import(\"CustomElementRegistry.zig\");\nconst Selection = @import(\"Selection.zig\");\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\nconst Allocator = std.mem.Allocator;\n\nconst Window = @This();\n\n_proto: *EventTarget,\n_page: *Page,\n_document: *Document,\n_css: CSS = .init,\n_crypto: Crypto = .init,\n_console: Console = .init,\n_navigator: Navigator = .init,\n_screen: *Screen,\n_visual_viewport: *VisualViewport,\n_performance: Performance,\n_storage_bucket: storage.Bucket = .{},\n_on_load: ?js.Function.Global = null,\n_on_pageshow: ?js.Function.Global = null,\n_on_popstate: ?js.Function.Global = null,\n_on_error: ?js.Function.Global = null,\n_on_message: ?js.Function.Global = null,\n_on_rejection_handled: ?js.Function.Global = null,\n_on_unhandled_rejection: ?js.Function.Global = null,\n_current_event: ?*Event = null,\n_location: *Location,\n_timer_id: u30 = 0,\n_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},\n_custom_elements: CustomElementRegistry = .{},\n_scroll_pos: struct {\n    x: u32,\n    y: u32,\n    state: enum {\n        scroll,\n        end,\n        done,\n    },\n} = .{\n    .x = 0,\n    .y = 0,\n    .state = .done,\n},\n\npub fn asEventTarget(self: *Window) *EventTarget {\n    return self._proto;\n}\n\npub fn getEvent(self: *const Window) ?*Event {\n    return self._current_event;\n}\n\npub fn getSelf(self: *Window) *Window {\n    return self;\n}\n\npub fn getWindow(self: *Window) *Window {\n    return self;\n}\n\npub fn getTop(self: *Window) *Window {\n    var p = self._page;\n    while (p.parent) |parent| {\n        p = parent;\n    }\n    return p.window;\n}\n\npub fn getParent(self: *Window) *Window {\n    if (self._page.parent) |p| {\n        return p.window;\n    }\n    return self;\n}\n\npub fn getDocument(self: *Window) *Document {\n    return self._document;\n}\n\npub fn getConsole(self: *Window) *Console {\n    return &self._console;\n}\n\npub fn getNavigator(self: *Window) *Navigator {\n    return &self._navigator;\n}\n\npub fn getScreen(self: *Window) *Screen {\n    return self._screen;\n}\n\npub fn getVisualViewport(self: *const Window) *VisualViewport {\n    return self._visual_viewport;\n}\n\npub fn getCrypto(self: *Window) *Crypto {\n    return &self._crypto;\n}\n\npub fn getCSS(self: *Window) *CSS {\n    return &self._css;\n}\n\npub fn getPerformance(self: *Window) *Performance {\n    return &self._performance;\n}\n\npub fn getLocalStorage(self: *Window) *storage.Lookup {\n    return &self._storage_bucket.local;\n}\n\npub fn getSessionStorage(self: *Window) *storage.Lookup {\n    return &self._storage_bucket.session;\n}\n\npub fn getLocation(self: *const Window) *Location {\n    return self._location;\n}\n\npub fn getSelection(self: *const Window) *Selection {\n    return &self._document._selection;\n}\n\npub fn setLocation(self: *Window, url: [:0]const u8, page: *Page) !void {\n    return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._page });\n}\n\npub fn getHistory(_: *Window, page: *Page) *History {\n    return &page._session.history;\n}\n\npub fn getNavigation(_: *Window, page: *Page) *Navigation {\n    return &page._session.navigation;\n}\n\npub fn getCustomElements(self: *Window) *CustomElementRegistry {\n    return &self._custom_elements;\n}\n\npub fn getOnLoad(self: *const Window) ?js.Function.Global {\n    return self._on_load;\n}\n\npub fn setOnLoad(self: *Window, setter: ?FunctionSetter) void {\n    self._on_load = getFunctionFromSetter(setter);\n}\n\npub fn getOnPageShow(self: *const Window) ?js.Function.Global {\n    return self._on_pageshow;\n}\n\npub fn setOnPageShow(self: *Window, setter: ?FunctionSetter) void {\n    self._on_pageshow = getFunctionFromSetter(setter);\n}\n\npub fn getOnPopState(self: *const Window) ?js.Function.Global {\n    return self._on_popstate;\n}\n\npub fn setOnPopState(self: *Window, setter: ?FunctionSetter) void {\n    self._on_popstate = getFunctionFromSetter(setter);\n}\n\npub fn getOnError(self: *const Window) ?js.Function.Global {\n    return self._on_error;\n}\n\npub fn setOnError(self: *Window, setter: ?FunctionSetter) void {\n    self._on_error = getFunctionFromSetter(setter);\n}\n\npub fn getOnMessage(self: *const Window) ?js.Function.Global {\n    return self._on_message;\n}\n\npub fn setOnMessage(self: *Window, setter: ?FunctionSetter) void {\n    self._on_message = getFunctionFromSetter(setter);\n}\n\npub fn getOnRejectionHandled(self: *const Window) ?js.Function.Global {\n    return self._on_rejection_handled;\n}\n\npub fn setOnRejectionHandled(self: *Window, setter: ?FunctionSetter) void {\n    self._on_rejection_handled = getFunctionFromSetter(setter);\n}\n\npub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {\n    return self._on_unhandled_rejection;\n}\n\npub fn setOnUnhandledRejection(self: *Window, setter: ?FunctionSetter) void {\n    self._on_unhandled_rejection = getFunctionFromSetter(setter);\n}\n\npub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, page: *Page) !js.Promise {\n    return Fetch.init(input, options, page);\n}\n\npub fn setTimeout(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {\n    return self.scheduleCallback(cb, delay_ms orelse 0, .{\n        .repeat = false,\n        .params = params,\n        .low_priority = false,\n        .name = \"window.setTimeout\",\n    }, page);\n}\n\npub fn setInterval(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {\n    return self.scheduleCallback(cb, delay_ms orelse 0, .{\n        .repeat = true,\n        .params = params,\n        .low_priority = false,\n        .name = \"window.setInterval\",\n    }, page);\n}\n\npub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, page: *Page) !u32 {\n    return self.scheduleCallback(cb, 0, .{\n        .repeat = false,\n        .params = params,\n        .low_priority = false,\n        .name = \"window.setImmediate\",\n    }, page);\n}\n\npub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, page: *Page) !u32 {\n    return self.scheduleCallback(cb, 5, .{\n        .repeat = false,\n        .params = &.{},\n        .low_priority = false,\n        .mode = .animation_frame,\n        .name = \"window.requestAnimationFrame\",\n    }, page);\n}\n\npub fn queueMicrotask(_: *Window, cb: js.Function, page: *Page) void {\n    page.js.queueMicrotaskFunc(cb);\n}\n\npub fn clearTimeout(self: *Window, id: u32) void {\n    var sc = self._timers.get(id) orelse return;\n    sc.removed = true;\n}\n\npub fn clearInterval(self: *Window, id: u32) void {\n    var sc = self._timers.get(id) orelse return;\n    sc.removed = true;\n}\n\npub fn clearImmediate(self: *Window, id: u32) void {\n    var sc = self._timers.get(id) orelse return;\n    sc.removed = true;\n}\n\npub fn cancelAnimationFrame(self: *Window, id: u32) void {\n    var sc = self._timers.get(id) orelse return;\n    sc.removed = true;\n}\n\nconst RequestIdleCallbackOpts = struct {\n    timeout: ?u32 = null,\n};\npub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {\n    const opts = opts_ orelse RequestIdleCallbackOpts{};\n    return self.scheduleCallback(cb, opts.timeout orelse 50, .{\n        .mode = .idle,\n        .repeat = false,\n        .params = &.{},\n        .low_priority = true,\n        .name = \"window.requestIdleCallback\",\n    }, page);\n}\n\npub fn cancelIdleCallback(self: *Window, id: u32) void {\n    var sc = self._timers.get(id) orelse return;\n    sc.removed = true;\n}\n\npub fn reportError(self: *Window, err: js.Value, page: *Page) !void {\n    const error_event = try ErrorEvent.initTrusted(comptime .wrap(\"error\"), .{\n        .@\"error\" = try err.temp(),\n        .message = err.toStringSlice() catch \"Unknown error\",\n        .bubbles = false,\n        .cancelable = true,\n    }, page);\n\n    // Invoke window.onerror callback if set (per WHATWG spec, this is called\n    // with 5 arguments: message, source, lineno, colno, error)\n    // If it returns true, the event is cancelled.\n    var prevent_default = false;\n    if (self._on_error) |on_error| {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        const local_func = ls.toLocal(on_error);\n        const result = local_func.call(js.Value, .{\n            error_event._message,\n            error_event._filename,\n            error_event._line_number,\n            error_event._column_number,\n            err,\n        }) catch null;\n\n        // Per spec: returning true from onerror cancels the event\n        if (result) |r| {\n            prevent_default = r.isTrue();\n        }\n    }\n\n    const event = error_event.asEvent();\n    event._prevent_default = prevent_default;\n    // Pass null as handler: onerror was already called above with 5 args.\n    // We still dispatch so that addEventListener('error', ...) listeners fire.\n    try page._event_manager.dispatchDirect(self.asEventTarget(), event, null, .{\n        .context = \"window.reportError\",\n    });\n\n    if (comptime builtin.is_test == false) {\n        if (!event._prevent_default) {\n            log.warn(.js, \"window.reportError\", .{\n                .message = error_event._message,\n                .filename = error_event._filename,\n                .line_number = error_event._line_number,\n                .column_number = error_event._column_number,\n            });\n        }\n    }\n}\n\npub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQueryList {\n    return page._factory.eventTarget(MediaQueryList{\n        ._proto = undefined,\n        ._media = try page.dupeString(query),\n    });\n}\n\npub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]const u8, page: *Page) !*CSSStyleProperties {\n    if (pseudo_element) |pe| {\n        if (pe.len != 0) {\n            log.warn(.not_implemented, \"window.GetComputedStyle\", .{ .pseudo_element = pe });\n        }\n    }\n    return CSSStyleProperties.init(element, true, page);\n}\n\npub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {\n    // For now, we ignore targetOrigin checking and just dispatch the message\n    // In a full implementation, we would validate the origin\n    _ = target_origin;\n\n    // self = the window that will get the message\n    // page = the context calling postMessage\n    const target_page = self._page;\n    const source_window = target_page.js.getIncumbent().window;\n\n    const arena = try target_page.getArena(.{ .debug = \"Window.postMessage\" });\n    errdefer target_page.releaseArena(arena);\n\n    // Origin should be the source window's origin (where the message came from)\n    const origin = try source_window._location.getOrigin(page);\n    const callback = try arena.create(PostMessageCallback);\n    callback.* = .{\n        .arena = arena,\n        .message = message,\n        .page = target_page,\n        .source = source_window,\n        .origin = try arena.dupe(u8, origin),\n    };\n\n    try target_page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{\n        .name = \"postMessage\",\n        .low_priority = false,\n        .finalizer = PostMessageCallback.cancelled,\n    });\n}\n\npub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {\n    const encoded_len = std.base64.standard.Encoder.calcSize(input.len);\n    const encoded = try page.call_arena.alloc(u8, encoded_len);\n    return std.base64.standard.Encoder.encode(encoded, input);\n}\n\npub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {\n    const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);\n    // Forgiving base64 decode per WHATWG spec:\n    // https://infra.spec.whatwg.org/#forgiving-base64-decode\n    // Remove trailing padding to use standard_no_pad decoder\n    const unpadded = std.mem.trimRight(u8, trimmed, \"=\");\n\n    // Length % 4 == 1 is invalid (can't represent valid base64)\n    if (unpadded.len % 4 == 1) {\n        return error.InvalidCharacterError;\n    }\n\n    const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;\n    const decoded = try page.call_arena.alloc(u8, decoded_len);\n    std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;\n    return decoded;\n}\n\npub fn structuredClone(_: *const Window, value: js.Value) !js.Value {\n    return value.structuredClone();\n}\n\npub fn getFrame(self: *Window, idx: usize) !?*Window {\n    const page = self._page;\n    const frames = page.frames.items;\n    if (idx >= frames.len) {\n        return null;\n    }\n\n    if (page.frames_sorted == false) {\n        std.mem.sort(*Page, frames, {}, struct {\n            fn lessThan(_: void, a: *Page, b: *Page) bool {\n                const iframe_a = a.iframe orelse return false;\n                const iframe_b = b.iframe orelse return true;\n\n                const pos = iframe_a.asNode().compareDocumentPosition(iframe_b.asNode());\n                // Return true if a precedes b (a should come before b in sorted order)\n                return (pos & 0x04) != 0; // FOLLOWING bit: b follows a\n            }\n        }.lessThan);\n        page.frames_sorted = true;\n    }\n    return frames[idx].window;\n}\n\npub fn getFramesLength(self: *const Window) u32 {\n    return @intCast(self._page.frames.items.len);\n}\n\npub fn getScrollX(self: *const Window) u32 {\n    return self._scroll_pos.x;\n}\n\npub fn getScrollY(self: *const Window) u32 {\n    return self._scroll_pos.y;\n}\n\nconst ScrollToOpts = union(enum) {\n    x: i32,\n    opts: Opts,\n\n    const Opts = struct {\n        top: i32,\n        left: i32,\n        behavior: []const u8 = \"\",\n    };\n};\npub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {\n    switch (opts) {\n        .x => |x| {\n            self._scroll_pos.x = @intCast(@max(x, 0));\n            self._scroll_pos.y = @intCast(@max(0, y orelse 0));\n        },\n        .opts => |o| {\n            self._scroll_pos.x = @intCast(@max(0, o.left));\n            self._scroll_pos.y = @intCast(@max(0, o.top));\n        },\n    }\n\n    self._scroll_pos.state = .scroll;\n\n    // We dispatch scroll event asynchronously after 10ms. So we can throttle\n    // them.\n    try page.js.scheduler.add(\n        page,\n        struct {\n            fn dispatch(_page: *anyopaque) anyerror!?u32 {\n                const p: *Page = @ptrCast(@alignCast(_page));\n                const pos = &p.window._scroll_pos;\n                // If the state isn't scroll, we can ignore safely to throttle\n                // the events.\n                if (pos.state != .scroll) {\n                    return null;\n                }\n\n                const event = try Event.initTrusted(comptime .wrap(\"scroll\"), .{ .bubbles = true }, p);\n                try p._event_manager.dispatch(p.document.asEventTarget(), event);\n\n                pos.state = .end;\n\n                return null;\n            }\n        }.dispatch,\n        10,\n        .{ .low_priority = true },\n    );\n    // We dispatch scrollend event asynchronously after 20ms.\n    try page.js.scheduler.add(\n        page,\n        struct {\n            fn dispatch(_page: *anyopaque) anyerror!?u32 {\n                const p: *Page = @ptrCast(@alignCast(_page));\n                const pos = &p.window._scroll_pos;\n                // Dispatch only if the state is .end.\n                // If a scroll is pending, retry in 10ms.\n                // If the state is .end, the event has been dispatched, so\n                // ignore safely.\n                switch (pos.state) {\n                    .scroll => return 10,\n                    .end => {},\n                    .done => return null,\n                }\n                const event = try Event.initTrusted(comptime .wrap(\"scrollend\"), .{ .bubbles = true }, p);\n                try p._event_manager.dispatch(p.document.asEventTarget(), event);\n\n                pos.state = .done;\n\n                return null;\n            }\n        }.dispatch,\n        20,\n        .{ .low_priority = true },\n    );\n}\n\npub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {\n    // The scroll is relative to the current position. So compute to new\n    // absolute position.\n    var absx: i32 = undefined;\n    var absy: i32 = undefined;\n    switch (opts) {\n        .x => |x| {\n            absx = @as(i32, @intCast(self._scroll_pos.x)) + x;\n            absy = @as(i32, @intCast(self._scroll_pos.y)) + (y orelse 0);\n        },\n        .opts => |o| {\n            absx = @as(i32, @intCast(self._scroll_pos.x)) + o.left;\n            absy = @as(i32, @intCast(self._scroll_pos.y)) + o.top;\n        },\n    }\n    return self.scrollTo(.{ .x = absx }, absy, page);\n}\n\npub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {\n    if (comptime IS_DEBUG) {\n        log.debug(.js, \"unhandled rejection\", .{\n            .value = rejection.reason(),\n            .stack = rejection.local.stackTrace() catch |err| @errorName(err) orelse \"???\",\n        });\n    }\n\n    const event_name, const attribute_callback = blk: {\n        if (no_handler) {\n            break :blk .{ \"unhandledrejection\", self._on_unhandled_rejection };\n        }\n        break :blk .{ \"rejectionhandled\", self._on_rejection_handled };\n    };\n\n    const target = self.asEventTarget();\n    if (page._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {\n        const event = (try @import(\"event/PromiseRejectionEvent.zig\").init(event_name, .{\n            .reason = if (rejection.reason()) |r| try r.temp() else null,\n            .promise = try rejection.promise().temp(),\n        }, page)).asEvent();\n        try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = \"window.unhandledrejection\" });\n    }\n}\n\nconst ScheduleOpts = struct {\n    repeat: bool,\n    params: []js.Value.Temp,\n    name: []const u8,\n    low_priority: bool = false,\n    animation_frame: bool = false,\n    mode: ScheduleCallback.Mode = .normal,\n};\nfn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {\n    if (self._timers.count() > 512) {\n        // these are active\n        return error.TooManyTimeout;\n    }\n\n    const arena = try page.getArena(.{ .debug = \"Window.schedule\" });\n    errdefer page.releaseArena(arena);\n\n    const timer_id = self._timer_id +% 1;\n    self._timer_id = timer_id;\n\n    const params = opts.params;\n    var persisted_params: []js.Value.Temp = &.{};\n    if (params.len > 0) {\n        persisted_params = try arena.dupe(js.Value.Temp, params);\n    }\n\n    const gop = try self._timers.getOrPut(page.arena, timer_id);\n    if (gop.found_existing) {\n        // 2^31 would have to wrap for this to happen.\n        return error.TooManyTimeout;\n    }\n    errdefer _ = self._timers.remove(timer_id);\n\n    const callback = try arena.create(ScheduleCallback);\n    callback.* = .{\n        .cb = cb,\n        .page = page,\n        .arena = arena,\n        .mode = opts.mode,\n        .name = opts.name,\n        .timer_id = timer_id,\n        .params = persisted_params,\n        .repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null,\n    };\n    gop.value_ptr.* = callback;\n\n    try page.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{\n        .name = opts.name,\n        .low_priority = opts.low_priority,\n        .finalizer = ScheduleCallback.cancelled,\n    });\n\n    return timer_id;\n}\n\nconst ScheduleCallback = struct {\n    // for debugging\n    name: []const u8,\n\n    // window._timers key\n    timer_id: u31,\n\n    // delay, in ms, to repeat. When null, will be removed after the first time\n    repeat_ms: ?u32,\n\n    cb: js.Function.Temp,\n\n    mode: Mode,\n    page: *Page,\n    arena: Allocator,\n    removed: bool = false,\n    params: []const js.Value.Temp,\n\n    const Mode = enum {\n        idle,\n        normal,\n        animation_frame,\n    };\n\n    fn cancelled(ctx: *anyopaque) void {\n        var self: *ScheduleCallback = @ptrCast(@alignCast(ctx));\n        self.deinit();\n    }\n\n    fn deinit(self: *ScheduleCallback) void {\n        self.cb.release();\n        for (self.params) |param| {\n            param.release();\n        }\n        self.page.releaseArena(self.arena);\n    }\n\n    fn run(ctx: *anyopaque) !?u32 {\n        const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));\n        const page = self.page;\n        const window = page.window;\n\n        if (self.removed) {\n            _ = window._timers.remove(self.timer_id);\n            self.deinit();\n            return null;\n        }\n\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        switch (self.mode) {\n            .idle => {\n                const IdleDeadline = @import(\"IdleDeadline.zig\");\n                ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| {\n                    log.warn(.js, \"window.idleCallback\", .{ .name = self.name, .err = err });\n                };\n            },\n            .animation_frame => {\n                ls.toLocal(self.cb).call(void, .{window._performance.now()}) catch |err| {\n                    log.warn(.js, \"window.RAF\", .{ .name = self.name, .err = err });\n                };\n            },\n            .normal => {\n                ls.toLocal(self.cb).call(void, self.params) catch |err| {\n                    log.warn(.js, \"window.timer\", .{ .name = self.name, .err = err });\n                };\n            },\n        }\n        ls.local.runMicrotasks();\n        if (self.repeat_ms) |ms| {\n            return ms;\n        }\n        defer self.deinit();\n        _ = window._timers.remove(self.timer_id);\n        return null;\n    }\n};\n\nconst PostMessageCallback = struct {\n    page: *Page,\n    source: *Window,\n    arena: Allocator,\n    origin: []const u8,\n    message: js.Value.Temp,\n\n    fn deinit(self: *PostMessageCallback) void {\n        self.page.releaseArena(self.arena);\n    }\n\n    fn cancelled(ctx: *anyopaque) void {\n        const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));\n        self.deinit();\n    }\n\n    fn run(ctx: *anyopaque) !?u32 {\n        const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));\n        defer self.deinit();\n\n        const page = self.page;\n        const window = page.window;\n\n        const event_target = window.asEventTarget();\n        if (page._event_manager.hasDirectListeners(event_target, \"message\", window._on_message)) {\n            const event = (try MessageEvent.initTrusted(comptime .wrap(\"message\"), .{\n                .data = self.message,\n                .origin = self.origin,\n                .source = self.source,\n                .bubbles = false,\n                .cancelable = false,\n            }, page)).asEvent();\n            try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = \"window.postMessage\" });\n        }\n\n        return null;\n    }\n};\n\nconst FunctionSetter = union(enum) {\n    func: js.Function.Global,\n    anything: js.Value,\n};\n\n// window.onload = {}; doesn't fail, but it doesn't do anything.\n// seems like setting to null is ok (though, at least on Firefix, it preserves\n// the original value, which we could do, but why?)\nfn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {\n    const setter = setter_ orelse return null;\n    return switch (setter) {\n        .func => |func| func, // Already a Global from bridge auto-conversion\n        .anything => null,\n    };\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Window);\n\n    pub const Meta = struct {\n        pub const name = \"Window\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } });\n    pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });\n\n    pub const top = bridge.accessor(Window.getTop, null, .{});\n    pub const self = bridge.accessor(Window.getWindow, null, .{});\n    pub const window = bridge.accessor(Window.getWindow, null, .{});\n    pub const parent = bridge.accessor(Window.getParent, null, .{});\n    pub const navigator = bridge.accessor(Window.getNavigator, null, .{});\n    pub const screen = bridge.accessor(Window.getScreen, null, .{});\n    pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});\n    pub const performance = bridge.accessor(Window.getPerformance, null, .{});\n    pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});\n    pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});\n    pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});\n    pub const history = bridge.accessor(Window.getHistory, null, .{});\n    pub const navigation = bridge.accessor(Window.getNavigation, null, .{});\n    pub const crypto = bridge.accessor(Window.getCrypto, null, .{});\n    pub const CSS = bridge.accessor(Window.getCSS, null, .{});\n    pub const customElements = bridge.accessor(Window.getCustomElements, null, .{});\n    pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});\n    pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});\n    pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});\n    pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});\n    pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});\n    pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});\n    pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});\n    pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });\n    pub const fetch = bridge.function(Window.fetch, .{});\n    pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});\n    pub const setTimeout = bridge.function(Window.setTimeout, .{});\n    pub const clearTimeout = bridge.function(Window.clearTimeout, .{});\n    pub const setInterval = bridge.function(Window.setInterval, .{});\n    pub const clearInterval = bridge.function(Window.clearInterval, .{});\n    pub const setImmediate = bridge.function(Window.setImmediate, .{});\n    pub const clearImmediate = bridge.function(Window.clearImmediate, .{});\n    pub const requestAnimationFrame = bridge.function(Window.requestAnimationFrame, .{});\n    pub const cancelAnimationFrame = bridge.function(Window.cancelAnimationFrame, .{});\n    pub const requestIdleCallback = bridge.function(Window.requestIdleCallback, .{});\n    pub const cancelIdleCallback = bridge.function(Window.cancelIdleCallback, .{});\n    pub const matchMedia = bridge.function(Window.matchMedia, .{});\n    pub const postMessage = bridge.function(Window.postMessage, .{});\n    pub const btoa = bridge.function(Window.btoa, .{});\n    pub const atob = bridge.function(Window.atob, .{ .dom_exception = true });\n    pub const reportError = bridge.function(Window.reportError, .{});\n    pub const structuredClone = bridge.function(Window.structuredClone, .{});\n    pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});\n    pub const getSelection = bridge.function(Window.getSelection, .{});\n\n    pub const frames = bridge.accessor(Window.getWindow, null, .{});\n    pub const index = bridge.indexed(Window.getFrame, null, .{ .null_as_undefined = true });\n    pub const length = bridge.accessor(Window.getFramesLength, null, .{});\n    pub const scrollX = bridge.accessor(Window.getScrollX, null, .{});\n    pub const scrollY = bridge.accessor(Window.getScrollY, null, .{});\n    pub const pageXOffset = bridge.accessor(Window.getScrollX, null, .{});\n    pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{});\n    pub const scrollTo = bridge.function(Window.scrollTo, .{});\n    pub const scroll = bridge.function(Window.scrollTo, .{});\n    pub const scrollBy = bridge.function(Window.scrollBy, .{});\n\n    // Return false since we don't have secure-context-only APIs implemented\n    // (webcam, geolocation, clipboard, etc.)\n    // This is safer and could help avoid processing errors by hinting at\n    // sites not to try to access those features\n    pub const isSecureContext = bridge.property(false, .{ .template = false });\n\n    pub const innerWidth = bridge.property(1920, .{ .template = false });\n    pub const innerHeight = bridge.property(1080, .{ .template = false });\n    pub const devicePixelRatio = bridge.property(1, .{ .template = false });\n\n    // This should return a window-like object in specific conditions. Would be\n    // pretty complicated to properly support I think.\n    pub const opener = bridge.property(null, .{ .template = false });\n\n    pub const alert = bridge.function(struct {\n        fn alert(_: *const Window, _: ?[]const u8) void {}\n    }.alert, .{ .noop = true });\n    pub const confirm = bridge.function(struct {\n        fn confirm(_: *const Window, _: ?[]const u8) bool {\n            return false;\n        }\n    }.confirm, .{});\n    pub const prompt = bridge.function(struct {\n        fn prompt(_: *const Window, _: ?[]const u8, _: ?[]const u8) ?[]const u8 {\n            return null;\n        }\n    }.prompt, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: Window\" {\n    try testing.htmlRunner(\"window\", .{});\n}\n\ntest \"WebApi: Window scroll\" {\n    try testing.htmlRunner(\"window_scroll.html\", .{});\n}\n\ntest \"WebApi: Window.onerror\" {\n    try testing.htmlRunner(\"event/report_error.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/XMLDocument.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../js/js.zig\");\n\nconst Document = @import(\"Document.zig\");\nconst Node = @import(\"Node.zig\");\n\nconst XMLDocument = @This();\n\n_proto: *Document,\n\npub fn asDocument(self: *XMLDocument) *Document {\n    return self._proto;\n}\n\npub fn asNode(self: *XMLDocument) *Node {\n    return self._proto.asNode();\n}\n\npub fn asEventTarget(self: *XMLDocument) *@import(\"EventTarget.zig\") {\n    return self._proto.asEventTarget();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(XMLDocument);\n\n    pub const Meta = struct {\n        pub const name = \"XMLDocument\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/XMLSerializer.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../js/js.zig\");\n\nconst Page = @import(\"../Page.zig\");\nconst Node = @import(\"Node.zig\");\nconst dump = @import(\"../dump.zig\");\n\nconst XMLSerializer = @This();\n\n// Padding to avoid zero-size struct, which causes identity_map pointer collisions.\n_pad: bool = false,\n\npub fn init() XMLSerializer {\n    return .{};\n}\n\npub fn serializeToString(self: *const XMLSerializer, node: *Node, page: *Page) ![]const u8 {\n    _ = self;\n    var buf = std.Io.Writer.Allocating.init(page.call_arena);\n    if (node.is(Node.Document)) |doc| {\n        try dump.root(doc, .{ .shadow = .skip }, &buf.writer, page);\n    } else {\n        try dump.deep(node, .{ .shadow = .skip }, &buf.writer, page);\n    }\n    // Not sure about this trim. But `dump` is meant to display relatively\n    // pretty HTML, so it does include newlines, which can result in a trailing\n    // newline. XMLSerializer is a bit more strict.\n    return std.mem.trim(u8, buf.written(), &std.ascii.whitespace);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(XMLSerializer);\n\n    pub const Meta = struct {\n        pub const name = \"XMLSerializer\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const constructor = bridge.constructor(XMLSerializer.init, .{});\n    pub const serializeToString = bridge.function(XMLSerializer.serializeToString, .{});\n};\n\nconst testing = @import(\"../../testing.zig\");\ntest \"WebApi: XMLSerializer\" {\n    try testing.htmlRunner(\"xmlserializer.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/animation/Animation.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../../log.zig\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst Animation = @This();\n\nconst PlayState = enum {\n    idle,\n    running,\n    paused,\n    finished,\n};\n\n_page: *Page,\n_arena: Allocator,\n\n_effect: ?js.Object.Global = null,\n_timeline: ?js.Object.Global = null,\n_ready_resolver: ?js.PromiseResolver.Global = null,\n_finished_resolver: ?js.PromiseResolver.Global = null,\n_startTime: ?f64 = null,\n_onFinish: ?js.Function.Temp = null,\n_playState: PlayState = .idle,\n\n// Fake the animation by passing the states:\n// .idle => .running once play() is called.\n// .running => .finished after 10ms when update() is callback.\n//\n// TODO add support for effect and timeline\npub fn init(page: *Page) !*Animation {\n    const arena = try page.getArena(.{ .debug = \"Animation\" });\n    errdefer page.releaseArena(arena);\n\n    const self = try arena.create(Animation);\n    self.* = .{\n        ._page = page,\n        ._arena = arena,\n    };\n\n    return self;\n}\n\npub fn deinit(self: *Animation, _: bool, session: *Session) void {\n    session.releaseArena(self._arena);\n}\n\npub fn play(self: *Animation, page: *Page) !void {\n    if (self._playState == .running) {\n        return;\n    }\n\n    // transition to running.\n    self._playState = .running;\n\n    // Schedule the transition from .running => .finished in 10ms.\n    page.js.strongRef(self);\n    try page.js.scheduler.add(\n        self,\n        Animation.update,\n        10,\n        .{ .name = \"animation.update\" },\n    );\n}\n\npub fn pause(self: *Animation) void {\n    self._playState = .paused;\n}\n\npub fn cancel(self: *Animation) void {\n    // Transition to idle. If the animation was .running, the already-scheduled\n    // update() callback will fire but see .idle state, skip the finish\n    // transition, and release the strong ref via weakRef() as normal.\n    self._playState = .idle;\n}\n\npub fn finish(self: *Animation, page: *Page) void {\n    if (self._playState == .finished) {\n        return;\n    }\n\n    self._playState = .finished;\n\n    // resolve finished\n    if (self._finished_resolver) |resolver| {\n        page.js.local.?.toLocal(resolver).resolve(\"Animation.getFinished\", self);\n    }\n    // call onfinish\n    if (self._onFinish) |func| {\n        page.js.local.?.toLocal(func).call(void, .{}) catch |err| {\n            log.warn(.js, \"Animation._onFinish\", .{ .err = err });\n        };\n    }\n}\n\npub fn reverse(_: *Animation) void {\n    log.warn(.not_implemented, \"Animation.reverse\", .{});\n}\n\npub fn getFinished(self: *Animation, page: *Page) !js.Promise {\n    if (self._finished_resolver == null) {\n        const resolver = page.js.local.?.createPromiseResolver();\n        self._finished_resolver = try resolver.persist();\n        return resolver.promise();\n    }\n    return page.js.toLocal(self._finished_resolver).?.promise();\n}\n\n// The ready promise is immediately resolved.\npub fn getReady(self: *Animation, page: *Page) !js.Promise {\n    if (self._ready_resolver == null) {\n        const resolver = page.js.local.?.createPromiseResolver();\n        resolver.resolve(\"Animation.getReady\", self);\n        self._ready_resolver = try resolver.persist();\n        return resolver.promise();\n    }\n    return page.js.toLocal(self._ready_resolver).?.promise();\n}\n\npub fn getEffect(self: *const Animation) ?js.Object.Global {\n    return self._effect;\n}\n\npub fn setEffect(self: *Animation, effect: ?js.Object.Global) !void {\n    self._effect = effect;\n}\n\npub fn getTimeline(self: *const Animation) ?js.Object.Global {\n    return self._timeline;\n}\n\npub fn setTimeline(self: *Animation, timeline: ?js.Object.Global) !void {\n    self._timeline = timeline;\n}\n\npub fn getStartTime(self: *const Animation) ?f64 {\n    return self._startTime;\n}\n\npub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void {\n    self._startTime = value;\n\n    // if the startTime is null, don't play the animation.\n    if (value == null) {\n        return;\n    }\n\n    return self.play(page);\n}\n\npub fn getOnFinish(self: *const Animation) ?js.Function.Temp {\n    return self._onFinish;\n}\n\n// callback function transitionning from a state to another\nfn update(ctx: *anyopaque) !?u32 {\n    const self: *Animation = @ptrCast(@alignCast(ctx));\n\n    switch (self._playState) {\n        .running => {\n            // transition to finished.\n            self._playState = .finished;\n\n            var ls: js.Local.Scope = undefined;\n            self._page.js.localScope(&ls);\n            defer ls.deinit();\n\n            // resolve finished\n            if (self._finished_resolver) |resolver| {\n                ls.toLocal(resolver).resolve(\"Animation.getFinished\", self);\n            }\n            // call onfinish\n            if (self._onFinish) |func| {\n                ls.toLocal(func).call(void, .{}) catch |err| {\n                    log.warn(.js, \"Animation._onFinish\", .{ .err = err });\n                };\n            }\n        },\n        .idle, .paused, .finished => {},\n    }\n\n    // No future change scheduled, set the object weak for garbage collection.\n    self._page.js.weakRef(self);\n    return null;\n}\n\npub fn setOnFinish(self: *Animation, cb: ?js.Function.Temp) !void {\n    self._onFinish = cb;\n}\n\npub fn playState(self: *const Animation) []const u8 {\n    return @tagName(self._playState);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Animation);\n\n    pub const Meta = struct {\n        pub const name = \"Animation\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(Animation.deinit);\n    };\n\n    pub const play = bridge.function(Animation.play, .{});\n    pub const pause = bridge.function(Animation.pause, .{});\n    pub const cancel = bridge.function(Animation.cancel, .{});\n    pub const finish = bridge.function(Animation.finish, .{});\n    pub const reverse = bridge.function(Animation.reverse, .{});\n    pub const playState = bridge.accessor(Animation.playState, null, .{});\n    pub const pending = bridge.property(false, .{ .template = false });\n    pub const finished = bridge.accessor(Animation.getFinished, null, .{});\n    pub const ready = bridge.accessor(Animation.getReady, null, .{});\n    pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{});\n    pub const timeline = bridge.accessor(Animation.getTimeline, Animation.setTimeline, .{});\n    pub const startTime = bridge.accessor(Animation.getStartTime, Animation.setStartTime, .{});\n    pub const onfinish = bridge.accessor(Animation.getOnFinish, Animation.setOnFinish, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: Animation\" {\n    try testing.htmlRunner(\"animation/animation.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/canvas/CanvasRenderingContext2D.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\n\nconst color = @import(\"../../color.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ImageData = @import(\"../ImageData.zig\");\n\n/// This class doesn't implement a `constructor`.\n/// It can be obtained with a call to `HTMLCanvasElement#getContext`.\n/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D\nconst CanvasRenderingContext2D = @This();\n/// Fill color.\n/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.\n_fill_style: color.RGBA = color.RGBA.Named.black,\n\npub fn getFillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {\n    var w = std.Io.Writer.Allocating.init(page.call_arena);\n    try self._fill_style.format(&w.writer);\n    return w.written();\n}\n\npub fn setFillStyle(\n    self: *CanvasRenderingContext2D,\n    value: []const u8,\n) !void {\n    // Prefer the same fill_style if fails.\n    self._fill_style = color.RGBA.parse(value) catch self._fill_style;\n}\n\nconst WidthOrImageData = union(enum) {\n    width: u32,\n    image_data: *ImageData,\n};\n\npub fn createImageData(\n    _: *const CanvasRenderingContext2D,\n    width_or_image_data: WidthOrImageData,\n    /// If `ImageData` variant preferred, this is null.\n    maybe_height: ?u32,\n    /// Can be used if width and height provided.\n    maybe_settings: ?ImageData.ConstructorSettings,\n    page: *Page,\n) !*ImageData {\n    switch (width_or_image_data) {\n        .width => |width| {\n            const height = maybe_height orelse return error.TypeError;\n            return ImageData.init(width, height, maybe_settings, page);\n        },\n        .image_data => |image_data| {\n            return ImageData.init(image_data._width, image_data._height, null, page);\n        },\n    }\n}\n\npub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}\n\npub fn getImageData(\n    _: *const CanvasRenderingContext2D,\n    _: i32, // sx\n    _: i32, // sy\n    sw: i32,\n    sh: i32,\n    page: *Page,\n) !*ImageData {\n    if (sw <= 0 or sh <= 0) {\n        return error.IndexSizeError;\n    }\n    return ImageData.init(@intCast(sw), @intCast(sh), null, page);\n}\n\npub fn save(_: *CanvasRenderingContext2D) void {}\npub fn restore(_: *CanvasRenderingContext2D) void {}\npub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn rotate(_: *CanvasRenderingContext2D, _: f64) void {}\npub fn translate(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn transform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn setTransform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn resetTransform(_: *CanvasRenderingContext2D) void {}\npub fn setStrokeStyle(_: *CanvasRenderingContext2D, _: []const u8) void {}\npub fn clearRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn fillRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn strokeRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn beginPath(_: *CanvasRenderingContext2D) void {}\npub fn closePath(_: *CanvasRenderingContext2D) void {}\npub fn moveTo(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn lineTo(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn quadraticCurveTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn bezierCurveTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn arc(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}\npub fn arcTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn rect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn fill(_: *CanvasRenderingContext2D) void {}\npub fn stroke(_: *CanvasRenderingContext2D) void {}\npub fn clip(_: *CanvasRenderingContext2D) void {}\npub fn fillText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}\npub fn strokeText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CanvasRenderingContext2D);\n\n    pub const Meta = struct {\n        pub const name = \"CanvasRenderingContext2D\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const font = bridge.property(\"10px sans-serif\", .{ .template = false, .readonly = false });\n    pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });\n    pub const globalCompositeOperation = bridge.property(\"source-over\", .{ .template = false, .readonly = false });\n    pub const strokeStyle = bridge.property(\"#000000\", .{ .template = false, .readonly = false });\n    pub const lineWidth = bridge.property(1.0, .{ .template = false, .readonly = false });\n    pub const lineCap = bridge.property(\"butt\", .{ .template = false, .readonly = false });\n    pub const lineJoin = bridge.property(\"miter\", .{ .template = false, .readonly = false });\n    pub const miterLimit = bridge.property(10.0, .{ .template = false, .readonly = false });\n    pub const textAlign = bridge.property(\"start\", .{ .template = false, .readonly = false });\n    pub const textBaseline = bridge.property(\"alphabetic\", .{ .template = false, .readonly = false });\n\n    pub const fillStyle = bridge.accessor(CanvasRenderingContext2D.getFillStyle, CanvasRenderingContext2D.setFillStyle, .{});\n    pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });\n\n    pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });\n    pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true });\n    pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });\n    pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });\n    pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true });\n    pub const rotate = bridge.function(CanvasRenderingContext2D.rotate, .{ .noop = true });\n    pub const translate = bridge.function(CanvasRenderingContext2D.translate, .{ .noop = true });\n    pub const transform = bridge.function(CanvasRenderingContext2D.transform, .{ .noop = true });\n    pub const setTransform = bridge.function(CanvasRenderingContext2D.setTransform, .{ .noop = true });\n    pub const resetTransform = bridge.function(CanvasRenderingContext2D.resetTransform, .{ .noop = true });\n    pub const clearRect = bridge.function(CanvasRenderingContext2D.clearRect, .{ .noop = true });\n    pub const fillRect = bridge.function(CanvasRenderingContext2D.fillRect, .{ .noop = true });\n    pub const strokeRect = bridge.function(CanvasRenderingContext2D.strokeRect, .{ .noop = true });\n    pub const beginPath = bridge.function(CanvasRenderingContext2D.beginPath, .{ .noop = true });\n    pub const closePath = bridge.function(CanvasRenderingContext2D.closePath, .{ .noop = true });\n    pub const moveTo = bridge.function(CanvasRenderingContext2D.moveTo, .{ .noop = true });\n    pub const lineTo = bridge.function(CanvasRenderingContext2D.lineTo, .{ .noop = true });\n    pub const quadraticCurveTo = bridge.function(CanvasRenderingContext2D.quadraticCurveTo, .{ .noop = true });\n    pub const bezierCurveTo = bridge.function(CanvasRenderingContext2D.bezierCurveTo, .{ .noop = true });\n    pub const arc = bridge.function(CanvasRenderingContext2D.arc, .{ .noop = true });\n    pub const arcTo = bridge.function(CanvasRenderingContext2D.arcTo, .{ .noop = true });\n    pub const rect = bridge.function(CanvasRenderingContext2D.rect, .{ .noop = true });\n    pub const fill = bridge.function(CanvasRenderingContext2D.fill, .{ .noop = true });\n    pub const stroke = bridge.function(CanvasRenderingContext2D.stroke, .{ .noop = true });\n    pub const clip = bridge.function(CanvasRenderingContext2D.clip, .{ .noop = true });\n    pub const fillText = bridge.function(CanvasRenderingContext2D.fillText, .{ .noop = true });\n    pub const strokeText = bridge.function(CanvasRenderingContext2D.strokeText, .{ .noop = true });\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: CanvasRenderingContext2D\" {\n    try testing.htmlRunner(\"canvas/canvas_rendering_context_2d.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/canvas/OffscreenCanvas.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst Blob = @import(\"../Blob.zig\");\nconst OffscreenCanvasRenderingContext2D = @import(\"OffscreenCanvasRenderingContext2D.zig\");\n\n/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas\nconst OffscreenCanvas = @This();\n\npub const _prototype_root = true;\n\n_width: u32,\n_height: u32,\n\n/// Since there's no base class rendering contextes inherit from,\n/// we're using tagged union.\nconst DrawingContext = union(enum) {\n    @\"2d\": *OffscreenCanvasRenderingContext2D,\n};\n\npub fn constructor(width: u32, height: u32, page: *Page) !*OffscreenCanvas {\n    return page._factory.create(OffscreenCanvas{\n        ._width = width,\n        ._height = height,\n    });\n}\n\npub fn getWidth(self: *const OffscreenCanvas) u32 {\n    return self._width;\n}\n\npub fn setWidth(self: *OffscreenCanvas, value: u32) void {\n    self._width = value;\n}\n\npub fn getHeight(self: *const OffscreenCanvas) u32 {\n    return self._height;\n}\n\npub fn setHeight(self: *OffscreenCanvas, value: u32) void {\n    self._height = value;\n}\n\npub fn getContext(_: *OffscreenCanvas, context_type: []const u8, page: *Page) !?DrawingContext {\n    if (std.mem.eql(u8, context_type, \"2d\")) {\n        const ctx = try page._factory.create(OffscreenCanvasRenderingContext2D{});\n        return .{ .@\"2d\" = ctx };\n    }\n\n    return null;\n}\n\n/// Returns a Promise that resolves to a Blob containing the image.\n/// Since we have no actual rendering, this returns an empty blob.\npub fn convertToBlob(_: *OffscreenCanvas, page: *Page) !js.Promise {\n    const blob = try Blob.init(null, null, page);\n    return page.js.local.?.resolvePromise(blob);\n}\n\n/// Returns an ImageBitmap with the rendered content (stub).\npub fn transferToImageBitmap(_: *OffscreenCanvas) ?void {\n    // ImageBitmap not implemented yet, return null\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(OffscreenCanvas);\n\n    pub const Meta = struct {\n        pub const name = \"OffscreenCanvas\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(OffscreenCanvas.constructor, .{});\n    pub const width = bridge.accessor(OffscreenCanvas.getWidth, OffscreenCanvas.setWidth, .{});\n    pub const height = bridge.accessor(OffscreenCanvas.getHeight, OffscreenCanvas.setHeight, .{});\n    pub const getContext = bridge.function(OffscreenCanvas.getContext, .{});\n    pub const convertToBlob = bridge.function(OffscreenCanvas.convertToBlob, .{});\n    pub const transferToImageBitmap = bridge.function(OffscreenCanvas.transferToImageBitmap, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: OffscreenCanvas\" {\n    try testing.htmlRunner(\"canvas/offscreen_canvas.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\nconst color = @import(\"../../color.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ImageData = @import(\"../ImageData.zig\");\n\n/// This class doesn't implement a `constructor`.\n/// It can be obtained with a call to `OffscreenCanvas#getContext`.\n/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D\nconst OffscreenCanvasRenderingContext2D = @This();\n/// Fill color.\n/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.\n_fill_style: color.RGBA = color.RGBA.Named.black,\n\npub fn getFillStyle(self: *const OffscreenCanvasRenderingContext2D, page: *Page) ![]const u8 {\n    var w = std.Io.Writer.Allocating.init(page.call_arena);\n    try self._fill_style.format(&w.writer);\n    return w.written();\n}\n\npub fn setFillStyle(\n    self: *OffscreenCanvasRenderingContext2D,\n    value: []const u8,\n) !void {\n    // Prefer the same fill_style if fails.\n    self._fill_style = color.RGBA.parse(value) catch self._fill_style;\n}\n\nconst WidthOrImageData = union(enum) {\n    width: u32,\n    image_data: *ImageData,\n};\n\npub fn createImageData(\n    _: *const OffscreenCanvasRenderingContext2D,\n    width_or_image_data: WidthOrImageData,\n    /// If `ImageData` variant preferred, this is null.\n    maybe_height: ?u32,\n    /// Can be used if width and height provided.\n    maybe_settings: ?ImageData.ConstructorSettings,\n    page: *Page,\n) !*ImageData {\n    switch (width_or_image_data) {\n        .width => |width| {\n            const height = maybe_height orelse return error.TypeError;\n            return ImageData.init(width, height, maybe_settings, page);\n        },\n        .image_data => |image_data| {\n            return ImageData.init(image_data._width, image_data._height, null, page);\n        },\n    }\n}\n\npub fn putImageData(_: *const OffscreenCanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}\n\npub fn getImageData(\n    _: *const OffscreenCanvasRenderingContext2D,\n    _: i32, // sx\n    _: i32, // sy\n    sw: i32,\n    sh: i32,\n    page: *Page,\n) !*ImageData {\n    if (sw <= 0 or sh <= 0) {\n        return error.IndexSizeError;\n    }\n    return ImageData.init(@intCast(sw), @intCast(sh), null, page);\n}\n\npub fn save(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn restore(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn scale(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn rotate(_: *OffscreenCanvasRenderingContext2D, _: f64) void {}\npub fn translate(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn transform(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn setTransform(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn resetTransform(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn setStrokeStyle(_: *OffscreenCanvasRenderingContext2D, _: []const u8) void {}\npub fn clearRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn fillRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn strokeRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn beginPath(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn closePath(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn moveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn lineTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}\npub fn quadraticCurveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn bezierCurveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn arc(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}\npub fn arcTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}\npub fn rect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}\npub fn fill(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn stroke(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn clip(_: *OffscreenCanvasRenderingContext2D) void {}\npub fn fillText(_: *OffscreenCanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}\npub fn strokeText(_: *OffscreenCanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(OffscreenCanvasRenderingContext2D);\n\n    pub const Meta = struct {\n        pub const name = \"OffscreenCanvasRenderingContext2D\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const font = bridge.property(\"10px sans-serif\", .{ .template = false, .readonly = false });\n    pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });\n    pub const globalCompositeOperation = bridge.property(\"source-over\", .{ .template = false, .readonly = false });\n    pub const strokeStyle = bridge.property(\"#000000\", .{ .template = false, .readonly = false });\n    pub const lineWidth = bridge.property(1.0, .{ .template = false, .readonly = false });\n    pub const lineCap = bridge.property(\"butt\", .{ .template = false, .readonly = false });\n    pub const lineJoin = bridge.property(\"miter\", .{ .template = false, .readonly = false });\n    pub const miterLimit = bridge.property(10.0, .{ .template = false, .readonly = false });\n    pub const textAlign = bridge.property(\"start\", .{ .template = false, .readonly = false });\n    pub const textBaseline = bridge.property(\"alphabetic\", .{ .template = false, .readonly = false });\n\n    pub const fillStyle = bridge.accessor(OffscreenCanvasRenderingContext2D.getFillStyle, OffscreenCanvasRenderingContext2D.setFillStyle, .{});\n    pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true });\n\n    pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = true });\n    pub const getImageData = bridge.function(OffscreenCanvasRenderingContext2D.getImageData, .{ .dom_exception = true });\n    pub const save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true });\n    pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true });\n    pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true });\n    pub const rotate = bridge.function(OffscreenCanvasRenderingContext2D.rotate, .{ .noop = true });\n    pub const translate = bridge.function(OffscreenCanvasRenderingContext2D.translate, .{ .noop = true });\n    pub const transform = bridge.function(OffscreenCanvasRenderingContext2D.transform, .{ .noop = true });\n    pub const setTransform = bridge.function(OffscreenCanvasRenderingContext2D.setTransform, .{ .noop = true });\n    pub const resetTransform = bridge.function(OffscreenCanvasRenderingContext2D.resetTransform, .{ .noop = true });\n    pub const clearRect = bridge.function(OffscreenCanvasRenderingContext2D.clearRect, .{ .noop = true });\n    pub const fillRect = bridge.function(OffscreenCanvasRenderingContext2D.fillRect, .{ .noop = true });\n    pub const strokeRect = bridge.function(OffscreenCanvasRenderingContext2D.strokeRect, .{ .noop = true });\n    pub const beginPath = bridge.function(OffscreenCanvasRenderingContext2D.beginPath, .{ .noop = true });\n    pub const closePath = bridge.function(OffscreenCanvasRenderingContext2D.closePath, .{ .noop = true });\n    pub const moveTo = bridge.function(OffscreenCanvasRenderingContext2D.moveTo, .{ .noop = true });\n    pub const lineTo = bridge.function(OffscreenCanvasRenderingContext2D.lineTo, .{ .noop = true });\n    pub const quadraticCurveTo = bridge.function(OffscreenCanvasRenderingContext2D.quadraticCurveTo, .{ .noop = true });\n    pub const bezierCurveTo = bridge.function(OffscreenCanvasRenderingContext2D.bezierCurveTo, .{ .noop = true });\n    pub const arc = bridge.function(OffscreenCanvasRenderingContext2D.arc, .{ .noop = true });\n    pub const arcTo = bridge.function(OffscreenCanvasRenderingContext2D.arcTo, .{ .noop = true });\n    pub const rect = bridge.function(OffscreenCanvasRenderingContext2D.rect, .{ .noop = true });\n    pub const fill = bridge.function(OffscreenCanvasRenderingContext2D.fill, .{ .noop = true });\n    pub const stroke = bridge.function(OffscreenCanvasRenderingContext2D.stroke, .{ .noop = true });\n    pub const clip = bridge.function(OffscreenCanvasRenderingContext2D.clip, .{ .noop = true });\n    pub const fillText = bridge.function(OffscreenCanvasRenderingContext2D.fillText, .{ .noop = true });\n    pub const strokeText = bridge.function(OffscreenCanvasRenderingContext2D.strokeText, .{ .noop = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/canvas/WebGLRenderingContext.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\npub fn registerTypes() []const type {\n    return &.{\n        WebGLRenderingContext,\n        // Extension types should be runtime generated. We might want\n        // to revisit this.\n        Extension.Type.WEBGL_debug_renderer_info,\n        Extension.Type.WEBGL_lose_context,\n    };\n}\n\nconst WebGLRenderingContext = @This();\n\n/// On Chrome and Safari, a call to `getSupportedExtensions` returns total of 39.\n/// The reference for it lists lesser number of extensions:\n/// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions#extension_list\npub const Extension = union(enum) {\n    ANGLE_instanced_arrays: void,\n    EXT_blend_minmax: void,\n    EXT_clip_control: void,\n    EXT_color_buffer_half_float: void,\n    EXT_depth_clamp: void,\n    EXT_disjoint_timer_query: void,\n    EXT_float_blend: void,\n    EXT_frag_depth: void,\n    EXT_polygon_offset_clamp: void,\n    EXT_shader_texture_lod: void,\n    EXT_texture_compression_bptc: void,\n    EXT_texture_compression_rgtc: void,\n    EXT_texture_filter_anisotropic: void,\n    EXT_texture_mirror_clamp_to_edge: void,\n    EXT_sRGB: void,\n    KHR_parallel_shader_compile: void,\n    OES_element_index_uint: void,\n    OES_fbo_render_mipmap: void,\n    OES_standard_derivatives: void,\n    OES_texture_float: void,\n    OES_texture_float_linear: void,\n    OES_texture_half_float: void,\n    OES_texture_half_float_linear: void,\n    OES_vertex_array_object: void,\n    WEBGL_blend_func_extended: void,\n    WEBGL_color_buffer_float: void,\n    WEBGL_compressed_texture_astc: void,\n    WEBGL_compressed_texture_etc: void,\n    WEBGL_compressed_texture_etc1: void,\n    WEBGL_compressed_texture_pvrtc: void,\n    WEBGL_compressed_texture_s3tc: void,\n    WEBGL_compressed_texture_s3tc_srgb: void,\n    WEBGL_debug_renderer_info: *Type.WEBGL_debug_renderer_info,\n    WEBGL_debug_shaders: void,\n    WEBGL_depth_texture: void,\n    WEBGL_draw_buffers: void,\n    WEBGL_lose_context: *Type.WEBGL_lose_context,\n    WEBGL_multi_draw: void,\n    WEBGL_polygon_mode: void,\n\n    /// Reified enum type from the fields of this union.\n    const Kind = blk: {\n        const info = @typeInfo(Extension).@\"union\";\n        const fields = info.fields;\n        var items: [fields.len]std.builtin.Type.EnumField = undefined;\n        for (fields, 0..) |field, i| {\n            items[i] = .{ .name = field.name, .value = i };\n        }\n\n        break :blk @Type(.{\n            .@\"enum\" = .{\n                .tag_type = std.math.IntFittingRange(0, if (fields.len == 0) 0 else fields.len - 1),\n                .fields = &items,\n                .decls = &.{},\n                .is_exhaustive = true,\n            },\n        });\n    };\n\n    /// Returns the `Extension.Kind` by its name.\n    fn find(name: []const u8) ?Kind {\n        // Just to make you really sad, this function has to be case-insensitive.\n        // So here we copy what's being done in `std.meta.stringToEnum` but replace\n        // the comparison function.\n        const kvs = comptime build_kvs: {\n            const T = Extension.Kind;\n            const EnumKV = struct { []const u8, T };\n            var kvs_array: [@typeInfo(T).@\"enum\".fields.len]EnumKV = undefined;\n            for (@typeInfo(T).@\"enum\".fields, 0..) |enumField, i| {\n                kvs_array[i] = .{ enumField.name, @field(T, enumField.name) };\n            }\n            break :build_kvs kvs_array[0..];\n        };\n        const Map = std.StaticStringMapWithEql(Extension.Kind, std.static_string_map.eqlAsciiIgnoreCase);\n        const map = Map.initComptime(kvs);\n        return map.get(name);\n    }\n\n    /// Extension types.\n    pub const Type = struct {\n        pub const WEBGL_debug_renderer_info = struct {\n            _: u8 = 0,\n            pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;\n            pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;\n\n            pub const JsApi = struct {\n                pub const bridge = js.Bridge(WEBGL_debug_renderer_info);\n\n                pub const Meta = struct {\n                    pub const name = \"WEBGL_debug_renderer_info\";\n\n                    pub const prototype_chain = bridge.prototypeChain();\n                    pub var class_id: bridge.ClassId = undefined;\n                };\n\n                pub const UNMASKED_VENDOR_WEBGL = bridge.property(WEBGL_debug_renderer_info.UNMASKED_VENDOR_WEBGL, .{ .template = false, .readonly = true });\n                pub const UNMASKED_RENDERER_WEBGL = bridge.property(WEBGL_debug_renderer_info.UNMASKED_RENDERER_WEBGL, .{ .template = false, .readonly = true });\n            };\n        };\n\n        pub const WEBGL_lose_context = struct {\n            _: u8 = 0,\n            pub fn loseContext(_: *const WEBGL_lose_context) void {}\n            pub fn restoreContext(_: *const WEBGL_lose_context) void {}\n\n            pub const JsApi = struct {\n                pub const bridge = js.Bridge(WEBGL_lose_context);\n\n                pub const Meta = struct {\n                    pub const name = \"WEBGL_lose_context\";\n\n                    pub const prototype_chain = bridge.prototypeChain();\n                    pub var class_id: bridge.ClassId = undefined;\n                };\n\n                pub const loseContext = bridge.function(WEBGL_lose_context.loseContext, .{ .noop = true });\n                pub const restoreContext = bridge.function(WEBGL_lose_context.restoreContext, .{ .noop = true });\n            };\n        };\n    };\n};\n\n/// This actually takes \"GLenum\" which, in fact, is a fancy way to say number.\n/// Return value also depends on what's being passed as `pname`; we don't really\n/// support any though.\npub fn getParameter(_: *const WebGLRenderingContext, pname: u32) []const u8 {\n    _ = pname;\n    return \"\";\n}\n\n/// Enables a WebGL extension.\npub fn getExtension(_: *const WebGLRenderingContext, name: []const u8, page: *Page) !?Extension {\n    const tag = Extension.find(name) orelse return null;\n\n    return switch (tag) {\n        .WEBGL_debug_renderer_info => {\n            const info = try page._factory.create(Extension.Type.WEBGL_debug_renderer_info{});\n            return .{ .WEBGL_debug_renderer_info = info };\n        },\n        .WEBGL_lose_context => {\n            const ctx = try page._factory.create(Extension.Type.WEBGL_lose_context{});\n            return .{ .WEBGL_lose_context = ctx };\n        },\n        inline else => |comptime_enum| @unionInit(Extension, @tagName(comptime_enum), {}),\n    };\n}\n\n/// Returns a list of all the supported WebGL extensions.\npub fn getSupportedExtensions(_: *const WebGLRenderingContext) []const []const u8 {\n    return std.meta.fieldNames(Extension.Kind);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(WebGLRenderingContext);\n\n    pub const Meta = struct {\n        pub const name = \"WebGLRenderingContext\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const getParameter = bridge.function(WebGLRenderingContext.getParameter, .{});\n    pub const getExtension = bridge.function(WebGLRenderingContext.getExtension, .{});\n    pub const getSupportedExtensions = bridge.function(WebGLRenderingContext.getSupportedExtensions, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: WebGLRenderingContext\" {\n    try testing.htmlRunner(\"canvas/webgl_rendering_context.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/cdata/CDATASection.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\n\nconst Text = @import(\"Text.zig\");\n\nconst CDATASection = @This();\n\n_proto: *Text,\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CDATASection);\n\n    pub const Meta = struct {\n        pub const name = \"CDATASection\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/cdata/Comment.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst CData = @import(\"../CData.zig\");\n\nconst Comment = @This();\n\n_proto: *CData,\n\npub fn init(str: ?js.NullableString, page: *Page) !*Comment {\n    const node = try page.createComment(if (str) |s| s.value else \"\");\n    return node.as(Comment);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Comment);\n\n    pub const Meta = struct {\n        pub const name = \"Comment\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(Comment.init, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: CData.Text\" {\n    try testing.htmlRunner(\"cdata/comment.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/cdata/ProcessingInstruction.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\n\nconst CData = @import(\"../CData.zig\");\n\nconst ProcessingInstruction = @This();\n\n_proto: *CData,\n_target: []const u8,\n\npub fn getTarget(self: *const ProcessingInstruction) []const u8 {\n    return self._target;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ProcessingInstruction);\n\n    pub const Meta = struct {\n        pub const name = \"ProcessingInstruction\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const target = bridge.accessor(ProcessingInstruction.getTarget, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: ProcessingInstruction\" {\n    try testing.htmlRunner(\"processing_instruction.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/cdata/Text.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../string.zig\").String;\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst CData = @import(\"../CData.zig\");\n\nconst Text = @This();\n\n_proto: *CData,\n\npub fn init(str: ?js.NullableString, page: *Page) !*Text {\n    const node = try page.createTextNode(if (str) |s| s.value else \"\");\n    return node.as(Text);\n}\n\npub fn getWholeText(self: *Text) []const u8 {\n    return self._proto._data.str();\n}\n\npub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {\n    const data = self._proto._data.str();\n\n    const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;\n\n    const new_data = data[byte_offset..];\n    const new_node = try page.createTextNode(new_data);\n    const new_text = new_node.as(Text);\n\n    const node = self._proto.asNode();\n\n    // Per DOM spec splitText: insert first (step 7a), then update ranges (7b-7e),\n    // then truncate original node (step 8).\n    if (node.parentNode()) |parent| {\n        const next_sibling = node.nextSibling();\n        _ = try parent.insertBefore(new_node, next_sibling, page);\n\n        // splitText-specific range updates (steps 7b-7e)\n        if (parent.getChildIndex(node)) |node_index| {\n            page.updateRangesForSplitText(node, new_node, @intCast(offset), parent, node_index);\n        }\n    }\n\n    // Step 8: truncate original node via replaceData(offset, count, \"\").\n    // Use replaceData instead of setData so live range updates fire\n    // (matters for detached text nodes where steps 7b-7e were skipped).\n    const length = self._proto.getLength();\n    try self._proto.replaceData(offset, length - offset, \"\", page);\n\n    return new_text;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Text);\n\n    pub const Meta = struct {\n        pub const name = \"Text\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(Text.init, .{});\n    pub const wholeText = bridge.accessor(Text.getWholeText, null, .{});\n    pub const splitText = bridge.function(Text.splitText, .{ .dom_exception = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/children.zig",
    "content": "const std = @import(\"std\");\n\nconst Node = @import(\"Node.zig\");\n\nconst LinkedList = std.DoublyLinkedList;\n\n// Our node._chilren is of type ?*NodeList. The extra (extra) indirection is to\n// keep memory size down.\n// First, a lot of nodes have no children. For these nodes, `?*NodeList = null`\n// will take 8 bytes and require no allocations (because an optional pointer in\n// Zig uses the address 0 to represent null, rather than a separate field).\n// Second, a lot of nodes will have one child. For these nodes, we'll also only\n// use 8 bytes, because @sizeOf(NodeList) == 8. This is the reason the\n// list: *LinkedList is behind a pointer.\npub const Children = union(enum) {\n    one: *Node,\n    list: *LinkedList,\n\n    pub fn first(self: *const Children) *Node {\n        return switch (self.*) {\n            .one => |n| n,\n            .list => |list| Node.linkToNode(list.first.?),\n        };\n    }\n\n    pub fn last(self: *const Children) *Node {\n        return switch (self.*) {\n            .one => |n| n,\n            .list => |list| Node.linkToNode(list.last.?),\n        };\n    }\n\n    pub fn len(self: *const Children) u32 {\n        return switch (self.*) {\n            .one => 1,\n            .list => |list| @intCast(list.len()),\n        };\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/ChildNodes.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Node = @import(\"../Node.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst GenericIterator = @import(\"iterator.zig\").Entry;\n\n// Optimized for node.childNodes, which has to be a live list.\n// No need to go through a TreeWalker or add any filtering.\nconst ChildNodes = @This();\n\n_arena: std.mem.Allocator,\n_last_index: usize,\n_last_length: ?u32,\n_last_node: ?*std.DoublyLinkedList.Node,\n_cached_version: usize,\n_node: *Node,\n\npub const KeyIterator = GenericIterator(Iterator, \"0\");\npub const ValueIterator = GenericIterator(Iterator, \"1\");\npub const EntryIterator = GenericIterator(Iterator, null);\n\npub fn init(node: *Node, page: *Page) !*ChildNodes {\n    const arena = try page.getArena(.{ .debug = \"ChildNodes\" });\n    errdefer page.releaseArena(arena);\n\n    const self = try arena.create(ChildNodes);\n    self.* = .{\n        ._node = node,\n        ._arena = arena,\n        ._last_index = 0,\n        ._last_node = null,\n        ._last_length = null,\n        ._cached_version = page.version,\n    };\n    return self;\n}\n\npub fn deinit(self: *const ChildNodes, session: *Session) void {\n    session.releaseArena(self._arena);\n}\n\npub fn length(self: *ChildNodes, page: *Page) !u32 {\n    if (self.versionCheck(page)) {\n        if (self._last_length) |cached_length| {\n            return cached_length;\n        }\n    }\n    const children = self._node._children orelse return 0;\n\n    // O(N)\n    const len = children.len();\n    self._last_length = len;\n    return len;\n}\n\npub fn getAtIndex(self: *ChildNodes, index: usize, page: *Page) !?*Node {\n    _ = self.versionCheck(page);\n\n    var current = self._last_index;\n    var node: ?*std.DoublyLinkedList.Node = null;\n    if (index < current) {\n        current = 0;\n        node = self.first() orelse return null;\n    } else {\n        node = self._last_node orelse self.first() orelse return null;\n    }\n    defer self._last_index = current;\n\n    while (node) |n| {\n        if (index == current) {\n            self._last_node = n;\n            return Node.linkToNode(n);\n        }\n        current += 1;\n        node = n.next;\n    }\n    self._last_node = null;\n    return null;\n}\n\npub fn first(self: *const ChildNodes) ?*std.DoublyLinkedList.Node {\n    return &(self._node._children orelse return null).first()._child_link;\n}\n\npub fn keys(self: *ChildNodes, page: *Page) !*KeyIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn values(self: *ChildNodes, page: *Page) !*ValueIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn entries(self: *ChildNodes, page: *Page) !*EntryIterator {\n    return .init(.{ .list = self }, page);\n}\n\nfn versionCheck(self: *ChildNodes, page: *Page) bool {\n    const current = page.version;\n    if (current == self._cached_version) {\n        return true;\n    }\n    self._last_index = 0;\n    self._last_node = null;\n    self._last_length = null;\n    self._cached_version = current;\n    return false;\n}\n\nconst NodeList = @import(\"NodeList.zig\");\npub fn runtimeGenericWrap(self: *ChildNodes, page: *Page) !*NodeList {\n    return page._factory.create(NodeList{ ._data = .{ .child_nodes = self } });\n}\n\nconst Iterator = struct {\n    index: u32 = 0,\n    list: *ChildNodes,\n\n    const Entry = struct { u32, *Node };\n\n    pub fn next(self: *Iterator, page: *Page) !?Entry {\n        const index = self.index;\n        const node = try self.list.getAtIndex(index, page) orelse return null;\n        self.index = index + 1;\n        return .{ index, node };\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/DOMTokenList.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../../log.zig\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Element = @import(\"../Element.zig\");\nconst GenericIterator = @import(\"iterator.zig\").Entry;\n\npub const DOMTokenList = @This();\n\n// There are a lot of inefficiencies in this code because the list is meant to\n// be live, e.g. reflect changes to the underlying attribute. The only good news\n// is that lists tend to be very short (often just 1 item).\n\n_element: *Element,\n_attribute_name: String,\n\npub const KeyIterator = GenericIterator(Iterator, \"0\");\npub const ValueIterator = GenericIterator(Iterator, \"1\");\npub const EntryIterator = GenericIterator(Iterator, null);\n\nconst Lookup = std.StringArrayHashMapUnmanaged(void);\n\nconst WHITESPACE = \" \\t\\n\\r\\x0C\";\n\npub fn length(self: *const DOMTokenList, page: *Page) !u32 {\n    const tokens = try self.getTokens(page);\n    return @intCast(tokens.count());\n}\n\n// TODO: soooo..inefficient\npub fn item(self: *const DOMTokenList, index: usize, page: *Page) !?[]const u8 {\n    var i: usize = 0;\n\n    const allocator = page.call_arena;\n    var seen: std.StringArrayHashMapUnmanaged(void) = .empty;\n\n    var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);\n    while (it.next()) |token| {\n        const gop = try seen.getOrPut(allocator, token);\n        if (!gop.found_existing) {\n            if (i == index) {\n                return token;\n            }\n            i += 1;\n        }\n    }\n    return null;\n}\n\npub fn contains(self: *const DOMTokenList, search: []const u8) !bool {\n    var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);\n    while (it.next()) |token| {\n        if (std.mem.eql(u8, search, token)) {\n            return true;\n        }\n    }\n    return false;\n}\n\npub fn add(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {\n    for (tokens) |token| {\n        try validateToken(token);\n    }\n\n    var lookup = try self.getTokens(page);\n    const allocator = page.call_arena;\n    try lookup.ensureUnusedCapacity(allocator, tokens.len);\n\n    for (tokens) |token| {\n        try lookup.put(allocator, token, {});\n    }\n\n    try self.updateAttribute(lookup, page);\n}\n\npub fn remove(self: *DOMTokenList, tokens: []const []const u8, page: *Page) !void {\n    for (tokens) |token| {\n        try validateToken(token);\n    }\n\n    var lookup = try self.getTokens(page);\n    for (tokens) |token| {\n        _ = lookup.orderedRemove(token);\n    }\n    try self.updateAttribute(lookup, page);\n}\n\npub fn toggle(self: *DOMTokenList, token: []const u8, force: ?bool, page: *Page) !bool {\n    try validateToken(token);\n\n    const has_token = try self.contains(token);\n\n    if (force) |f| {\n        if (f) {\n            if (!has_token) {\n                const tokens_to_add = [_][]const u8{token};\n                try self.add(&tokens_to_add, page);\n            }\n            return true;\n        } else {\n            if (has_token) {\n                const tokens_to_remove = [_][]const u8{token};\n                try self.remove(&tokens_to_remove, page);\n            }\n            return false;\n        }\n    } else {\n        if (has_token) {\n            const tokens_to_remove = [_][]const u8{token};\n            try self.remove(tokens_to_remove[0..], page);\n            return false;\n        } else {\n            const tokens_to_add = [_][]const u8{token};\n            try self.add(tokens_to_add[0..], page);\n            return true;\n        }\n    }\n}\n\npub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8, page: *Page) !bool {\n    // Validate in spec order: both empty first, then both whitespace\n    if (old_token.len == 0 or new_token.len == 0) {\n        return error.SyntaxError;\n    }\n    if (std.mem.indexOfAny(u8, old_token, WHITESPACE) != null) {\n        return error.InvalidCharacterError;\n    }\n    if (std.mem.indexOfAny(u8, new_token, WHITESPACE) != null) {\n        return error.InvalidCharacterError;\n    }\n\n    var lookup = try self.getTokens(page);\n\n    // Check if old_token exists\n    if (!lookup.contains(old_token)) {\n        return false;\n    }\n\n    // If replacing with the same token, still need to trigger mutation\n    if (std.mem.eql(u8, new_token, old_token)) {\n        try self.updateAttribute(lookup, page);\n        return true;\n    }\n\n    const allocator = page.call_arena;\n    // Build new token list preserving order but replacing old with new\n    var new_tokens = try std.ArrayList([]const u8).initCapacity(allocator, lookup.count());\n    var replaced_old = false;\n\n    for (lookup.keys()) |token| {\n        if (std.mem.eql(u8, token, old_token) and !replaced_old) {\n            new_tokens.appendAssumeCapacity(new_token);\n            replaced_old = true;\n        } else if (std.mem.eql(u8, token, old_token)) {\n            // Subsequent occurrences of old_token: skip (remove duplicates)\n            continue;\n        } else if (std.mem.eql(u8, token, new_token) and replaced_old) {\n            // Occurrence of new_token AFTER replacement: skip (remove duplicate)\n            continue;\n        } else {\n            // Any other token (including new_token before replacement): keep it\n            new_tokens.appendAssumeCapacity(token);\n        }\n    }\n\n    // Rebuild lookup\n    var new_lookup: Lookup = .empty;\n    try new_lookup.ensureTotalCapacity(allocator, new_tokens.items.len);\n    for (new_tokens.items) |token| {\n        try new_lookup.put(allocator, token, {});\n    }\n\n    try self.updateAttribute(new_lookup, page);\n    return true;\n}\n\npub fn getValue(self: *const DOMTokenList) []const u8 {\n    return self._element.getAttributeSafe(self._attribute_name) orelse \"\";\n}\n\npub fn setValue(self: *DOMTokenList, value: String, page: *Page) !void {\n    try self._element.setAttribute(self._attribute_name, value, page);\n}\n\npub fn keys(self: *DOMTokenList, page: *Page) !*KeyIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn values(self: *DOMTokenList, page: *Page) !*ValueIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn entries(self: *DOMTokenList, page: *Page) !*EntryIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, page: *Page) !void {\n    const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;\n\n    const allocator = page.call_arena;\n\n    var i: i32 = 0;\n    var seen: std.StringArrayHashMapUnmanaged(void) = .empty;\n\n    var it = std.mem.tokenizeAny(u8, self.getValue(), WHITESPACE);\n    while (it.next()) |token| {\n        const gop = try seen.getOrPut(allocator, token);\n        if (gop.found_existing) {\n            continue;\n        }\n        var caught: js.TryCatch.Caught = undefined;\n        cb.tryCall(void, .{ token, i, self }, &caught) catch {\n            log.debug(.js, \"forEach callback\", .{ .caught = caught, .source = \"DOMTokenList\" });\n            return;\n        };\n        i += 1;\n    }\n}\n\nfn getTokens(self: *const DOMTokenList, page: *Page) !Lookup {\n    const value = self.getValue();\n    if (value.len == 0) {\n        return .empty;\n    }\n\n    var list: Lookup = .empty;\n    const allocator = page.call_arena;\n    try list.ensureTotalCapacity(allocator, 4);\n\n    var it = std.mem.tokenizeAny(u8, value, WHITESPACE);\n    while (it.next()) |token| {\n        try list.put(allocator, token, {});\n    }\n    return list;\n}\n\nfn validateToken(token: []const u8) !void {\n    if (token.len == 0) {\n        return error.SyntaxError;\n    }\n    if (std.mem.indexOfAny(u8, token, &std.ascii.whitespace) != null) {\n        return error.InvalidCharacterError;\n    }\n}\n\nfn updateAttribute(self: *DOMTokenList, tokens: Lookup, page: *Page) !void {\n    if (tokens.count() > 0) {\n        const joined = try std.mem.join(page.call_arena, \" \", tokens.keys());\n        return self._element.setAttribute(self._attribute_name, .wrap(joined), page);\n    }\n\n    // Only remove attribute if it didn't exist before (was null)\n    // If it existed (even as \"\"), set it to \"\" to preserve its existence\n    if (self._element.hasAttributeSafe(self._attribute_name)) {\n        try self._element.setAttribute(self._attribute_name, .wrap(\"\"), page);\n    }\n}\n\nconst Iterator = struct {\n    index: u32 = 0,\n    list: *DOMTokenList,\n\n    const Entry = struct { u32, []const u8 };\n\n    pub fn next(self: *Iterator, page: *Page) !?Entry {\n        const index = self.index;\n        const node = try self.list.item(index, page) orelse return null;\n        self.index = index + 1;\n        return .{ index, node };\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMTokenList);\n\n    pub const Meta = struct {\n        pub const name = \"DOMTokenList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const length = bridge.accessor(DOMTokenList.length, null, .{});\n    pub const item = bridge.function(_item, .{});\n    fn _item(self: *const DOMTokenList, index: i32, page: *Page) !?[]const u8 {\n        if (index < 0) {\n            return null;\n        }\n        return self.item(@intCast(index), page);\n    }\n\n    pub const contains = bridge.function(DOMTokenList.contains, .{ .dom_exception = true });\n    pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true });\n    pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true });\n    pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true });\n    pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true });\n    pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{});\n    pub const toString = bridge.function(DOMTokenList.getValue, .{});\n    pub const keys = bridge.function(DOMTokenList.keys, .{});\n    pub const values = bridge.function(DOMTokenList.values, .{});\n    pub const entries = bridge.function(DOMTokenList.entries, .{});\n    pub const symbol_iterator = bridge.iterator(DOMTokenList.values, .{});\n    pub const forEach = bridge.function(DOMTokenList.forEach, .{});\n    pub const @\"[]\" = bridge.indexed(DOMTokenList.item, null, .{ .null_as_undefined = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/HTMLAllCollection.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\nconst TreeWalker = @import(\"../TreeWalker.zig\");\n\nconst HTMLAllCollection = @This();\n\n_tw: TreeWalker.FullExcludeSelf,\n_last_index: usize,\n_last_length: ?u32,\n_cached_version: usize,\n\npub fn init(root: *Node, page: *Page) HTMLAllCollection {\n    return .{\n        ._last_index = 0,\n        ._last_length = null,\n        ._tw = TreeWalker.FullExcludeSelf.init(root, .{}),\n        ._cached_version = page.version,\n    };\n}\n\nfn versionCheck(self: *HTMLAllCollection, page: *const Page) bool {\n    if (self._cached_version != page.version) {\n        self._cached_version = page.version;\n        self._last_index = 0;\n        self._last_length = null;\n        self._tw.reset();\n        return false;\n    }\n    return true;\n}\n\npub fn length(self: *HTMLAllCollection, page: *const Page) u32 {\n    if (self.versionCheck(page)) {\n        if (self._last_length) |cached_length| {\n            return cached_length;\n        }\n    }\n\n    lp.assert(self._last_index == 0, \"HTMLAllCollection.length\", .{ .last_index = self._last_index });\n\n    var tw = &self._tw;\n    defer tw.reset();\n\n    var l: u32 = 0;\n    while (tw.next()) |node| {\n        if (node.is(Element) != null) {\n            l += 1;\n        }\n    }\n\n    self._last_length = l;\n    return l;\n}\n\npub fn getAtIndex(self: *HTMLAllCollection, index: usize, page: *const Page) ?*Element {\n    _ = self.versionCheck(page);\n    var current = self._last_index;\n    if (index <= current) {\n        current = 0;\n        self._tw.reset();\n    }\n    defer self._last_index = current + 1;\n\n    const tw = &self._tw;\n    while (tw.next()) |node| {\n        if (node.is(Element)) |el| {\n            if (index == current) {\n                return el;\n            }\n            current += 1;\n        }\n    }\n\n    return null;\n}\n\npub fn getByName(self: *HTMLAllCollection, name: []const u8, page: *Page) ?*Element {\n    // First, try fast ID lookup using the document's element map\n    if (page.document._elements_by_id.get(name)) |el| {\n        return el;\n    }\n\n    // Fall back to searching by name attribute\n    // Clone the tree walker to preserve _last_index optimization\n    _ = self.versionCheck(page);\n    var tw = self._tw.clone();\n    tw.reset();\n\n    while (tw.next()) |node| {\n        if (node.is(Element)) |el| {\n            if (el.getAttributeSafe(comptime .wrap(\"name\"))) |attr_name| {\n                if (std.mem.eql(u8, attr_name, name)) {\n                    return el;\n                }\n            }\n        }\n    }\n\n    return null;\n}\n\nconst CAllAsFunctionArg = union(enum) {\n    index: u32,\n    id: []const u8,\n};\npub fn callable(self: *HTMLAllCollection, arg: CAllAsFunctionArg, page: *Page) ?*Element {\n    return switch (arg) {\n        .index => |i| self.getAtIndex(i, page),\n        .id => |id| self.getByName(id, page),\n    };\n}\n\npub fn iterator(self: *HTMLAllCollection, page: *Page) !*Iterator {\n    return Iterator.init(.{\n        .list = self,\n        .tw = self._tw.clone(),\n    }, page);\n}\n\nconst GenericIterator = @import(\"iterator.zig\").Entry;\npub const Iterator = GenericIterator(struct {\n    list: *HTMLAllCollection,\n    tw: TreeWalker.FullExcludeSelf,\n\n    pub fn next(self: *@This(), _: *Page) ?*Element {\n        while (self.tw.next()) |node| {\n            if (node.is(Element)) |el| {\n                return el;\n            }\n        }\n        return null;\n    }\n}, null);\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HTMLAllCollection);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLAllCollection\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n\n        // This is a very weird class that requires special JavaScript behavior\n        // this htmldda and callable are only used here..\n        pub const htmldda = true;\n        pub const callable = JsApi.callable;\n    };\n\n    pub const length = bridge.accessor(HTMLAllCollection.length, null, .{});\n    pub const @\"[int]\" = bridge.indexed(HTMLAllCollection.getAtIndex, null, .{ .null_as_undefined = true });\n    pub const @\"[str]\" = bridge.namedIndexed(HTMLAllCollection.getByName, null, null, .{ .null_as_undefined = true });\n\n    pub const item = bridge.function(_item, .{});\n    fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element {\n        if (index < 0) {\n            return null;\n        }\n        return self.getAtIndex(@intCast(index), page);\n    }\n\n    pub const namedItem = bridge.function(HTMLAllCollection.getByName, .{});\n    pub const symbol_iterator = bridge.iterator(HTMLAllCollection.iterator, .{});\n\n    pub const callable = bridge.callable(HTMLAllCollection.callable, .{ .null_as_undefined = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/HTMLCollection.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Element = @import(\"../Element.zig\");\nconst TreeWalker = @import(\"../TreeWalker.zig\");\nconst NodeLive = @import(\"node_live.zig\").NodeLive;\n\nconst Mode = enum {\n    tag,\n    tag_name,\n    tag_name_ns,\n    class_name,\n    all_elements,\n    child_elements,\n    child_tag,\n    selected_options,\n    links,\n    anchors,\n    form,\n    empty,\n};\n\nconst HTMLCollection = @This();\n\n_data: union(Mode) {\n    tag: NodeLive(.tag),\n    tag_name: NodeLive(.tag_name),\n    tag_name_ns: NodeLive(.tag_name_ns),\n    class_name: NodeLive(.class_name),\n    all_elements: NodeLive(.all_elements),\n    child_elements: NodeLive(.child_elements),\n    child_tag: NodeLive(.child_tag),\n    selected_options: NodeLive(.selected_options),\n    links: NodeLive(.links),\n    anchors: NodeLive(.anchors),\n    form: NodeLive(.form),\n    empty: void,\n},\n\npub fn length(self: *HTMLCollection, page: *const Page) u32 {\n    return switch (self._data) {\n        .empty => 0,\n        inline else => |*impl| impl.length(page),\n    };\n}\n\npub fn getAtIndex(self: *HTMLCollection, index: usize, page: *const Page) ?*Element {\n    return switch (self._data) {\n        .empty => null,\n        inline else => |*impl| impl.getAtIndex(index, page),\n    };\n}\n\npub fn getByName(self: *HTMLCollection, name: []const u8, page: *Page) ?*Element {\n    return switch (self._data) {\n        .empty => null,\n        inline else => |*impl| impl.getByName(name, page),\n    };\n}\n\npub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {\n    return Iterator.init(.{\n        .list = self,\n        .tw = switch (self._data) {\n            .tag => |*impl| .{ .tag = impl._tw.clone() },\n            .tag_name => |*impl| .{ .tag_name = impl._tw.clone() },\n            .tag_name_ns => |*impl| .{ .tag_name_ns = impl._tw.clone() },\n            .class_name => |*impl| .{ .class_name = impl._tw.clone() },\n            .all_elements => |*impl| .{ .all_elements = impl._tw.clone() },\n            .child_elements => |*impl| .{ .child_elements = impl._tw.clone() },\n            .child_tag => |*impl| .{ .child_tag = impl._tw.clone() },\n            .selected_options => |*impl| .{ .selected_options = impl._tw.clone() },\n            .links => |*impl| .{ .links = impl._tw.clone() },\n            .anchors => |*impl| .{ .anchors = impl._tw.clone() },\n            .form => |*impl| .{ .form = impl._tw.clone() },\n            .empty => .empty,\n        },\n    }, page);\n}\n\nconst GenericIterator = @import(\"iterator.zig\").Entry;\npub const Iterator = GenericIterator(struct {\n    list: *HTMLCollection,\n    tw: union(Mode) {\n        tag: TreeWalker.FullExcludeSelf,\n        tag_name: TreeWalker.FullExcludeSelf,\n        tag_name_ns: TreeWalker.FullExcludeSelf,\n        class_name: TreeWalker.FullExcludeSelf,\n        all_elements: TreeWalker.FullExcludeSelf,\n        child_elements: TreeWalker.Children,\n        child_tag: TreeWalker.Children,\n        selected_options: TreeWalker.Children,\n        links: TreeWalker.FullExcludeSelf,\n        anchors: TreeWalker.FullExcludeSelf,\n        form: TreeWalker.FullExcludeSelf,\n        empty: void,\n    },\n\n    pub fn next(self: *@This(), _: *Page) ?*Element {\n        return switch (self.list._data) {\n            .tag => |*impl| impl.nextTw(&self.tw.tag),\n            .tag_name => |*impl| impl.nextTw(&self.tw.tag_name),\n            .tag_name_ns => |*impl| impl.nextTw(&self.tw.tag_name_ns),\n            .class_name => |*impl| impl.nextTw(&self.tw.class_name),\n            .all_elements => |*impl| impl.nextTw(&self.tw.all_elements),\n            .child_elements => |*impl| impl.nextTw(&self.tw.child_elements),\n            .child_tag => |*impl| impl.nextTw(&self.tw.child_tag),\n            .selected_options => |*impl| impl.nextTw(&self.tw.selected_options),\n            .links => |*impl| impl.nextTw(&self.tw.links),\n            .anchors => |*impl| impl.nextTw(&self.tw.anchors),\n            .form => |*impl| impl.nextTw(&self.tw.form),\n            .empty => return null,\n        };\n    }\n}, null);\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HTMLCollection);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLCollection\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const length = bridge.accessor(HTMLCollection.length, null, .{});\n    pub const @\"[int]\" = bridge.indexed(HTMLCollection.getAtIndex, null, .{ .null_as_undefined = true });\n    pub const @\"[str]\" = bridge.namedIndexed(HTMLCollection.getByName, null, null, .{ .null_as_undefined = true });\n\n    pub const item = bridge.function(_item, .{});\n    fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element {\n        if (index < 0) {\n            return null;\n        }\n        return self.getAtIndex(@intCast(index), page);\n    }\n\n    pub const namedItem = bridge.function(HTMLCollection.getByName, .{});\n    pub const symbol_iterator = bridge.iterator(HTMLCollection.iterator, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/HTMLFormControlsCollection.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Element = @import(\"../Element.zig\");\n\nconst NodeList = @import(\"NodeList.zig\");\nconst RadioNodeList = @import(\"RadioNodeList.zig\");\nconst HTMLCollection = @import(\"HTMLCollection.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst HTMLFormControlsCollection = @This();\n\n_proto: *HTMLCollection,\n\npub const NamedItemResult = union(enum) {\n    element: *Element,\n    radio_node_list: *RadioNodeList,\n};\n\npub fn length(self: *HTMLFormControlsCollection, page: *Page) u32 {\n    return self._proto.length(page);\n}\n\npub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, page: *Page) ?*Element {\n    return self._proto.getAtIndex(index, page);\n}\n\npub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Page) !?NamedItemResult {\n    if (name.len == 0) {\n        return null;\n    }\n\n    // We need special handling for radio, where multiple inputs can have the\n    // same name, but we also need to handle the [incorrect] case where non-\n    // radios share names.\n\n    var count: u32 = 0;\n    var first_element: ?*Element = null;\n\n    var it = try self.iterator();\n    while (it.next()) |element| {\n        const is_match = blk: {\n            if (element.getAttributeSafe(comptime .wrap(\"id\"))) |id| {\n                if (std.mem.eql(u8, id, name)) {\n                    break :blk true;\n                }\n            }\n            if (element.getAttributeSafe(comptime .wrap(\"name\"))) |elem_name| {\n                if (std.mem.eql(u8, elem_name, name)) {\n                    break :blk true;\n                }\n            }\n            break :blk false;\n        };\n\n        if (is_match) {\n            if (first_element == null) {\n                first_element = element;\n            }\n            count += 1;\n\n            if (count == 2) {\n                const radio_node_list = try page._factory.create(RadioNodeList{\n                    ._proto = undefined,\n                    ._form_collection = self,\n                    ._name = try page.dupeString(name),\n                });\n\n                radio_node_list._proto = try page._factory.create(NodeList{ ._data = .{ .radio_node_list = radio_node_list } });\n\n                return .{ .radio_node_list = radio_node_list };\n            }\n        }\n    }\n\n    if (count == 0) {\n        return null;\n    }\n\n    // case == 2 was handled inside the loop\n    if (comptime IS_DEBUG) {\n        std.debug.assert(count == 1);\n    }\n\n    return .{ .element = first_element.? };\n}\n\n// used internally, by HTMLFormControlsCollection and RadioNodeList\npub fn iterator(self: *HTMLFormControlsCollection) !Iterator {\n    const form_collection = self._proto._data.form;\n    return .{\n        .tw = form_collection._tw.clone(),\n        .nodes = form_collection,\n    };\n}\n\n// Used internally. Presents a nicer (more zig-like) iterator and strips away\n// some of the abstraction.\npub const Iterator = struct {\n    tw: TreeWalker,\n    nodes: NodeLive,\n\n    const NodeLive = @import(\"node_live.zig\").NodeLive(.form);\n    const TreeWalker = @import(\"../TreeWalker.zig\").FullExcludeSelf;\n\n    pub fn next(self: *Iterator) ?*Element {\n        return self.nodes.nextTw(&self.tw);\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HTMLFormControlsCollection);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLFormControlsCollection\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const manage = false;\n    };\n\n    pub const length = bridge.accessor(HTMLFormControlsCollection.length, null, .{});\n    pub const @\"[int]\" = bridge.indexed(HTMLFormControlsCollection.getAtIndex, null, .{ .null_as_undefined = true });\n    pub const @\"[str]\" = bridge.namedIndexed(HTMLFormControlsCollection.namedItem, null, null, .{ .null_as_undefined = true });\n    pub const namedItem = bridge.function(HTMLFormControlsCollection.namedItem, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/HTMLOptionsCollection.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\nconst HTMLCollection = @import(\"HTMLCollection.zig\");\n\nconst HTMLOptionsCollection = @This();\n\n_proto: *HTMLCollection,\n_select: *@import(\"../element/html/Select.zig\"),\n\n// Forward length to HTMLCollection\npub fn length(self: *HTMLOptionsCollection, page: *Page) u32 {\n    return self._proto.length(page);\n}\n\n// Forward indexed access to HTMLCollection\npub fn getAtIndex(self: *HTMLOptionsCollection, index: usize, page: *Page) ?*Element {\n    return self._proto.getAtIndex(index, page);\n}\n\npub fn getByName(self: *HTMLOptionsCollection, name: []const u8, page: *Page) ?*Element {\n    return self._proto.getByName(name, page);\n}\n\n// Forward selectedIndex to the owning select element\npub fn getSelectedIndex(self: *const HTMLOptionsCollection) i32 {\n    return self._select.getSelectedIndex();\n}\n\npub fn setSelectedIndex(self: *HTMLOptionsCollection, index: i32) !void {\n    return self._select.setSelectedIndex(index);\n}\n\nconst Option = @import(\"../element/html/Option.zig\");\n\nconst AddBeforeOption = union(enum) {\n    option: *Option,\n    index: u32,\n};\n\n// Add a new option element\npub fn add(self: *HTMLOptionsCollection, element: *Option, before_: ?AddBeforeOption, page: *Page) !void {\n    const select_node = self._select.asNode();\n    const element_node = element.asElement().asNode();\n\n    var before_node: ?*Node = null;\n    if (before_) |before| {\n        switch (before) {\n            .index => |idx| {\n                if (self.getAtIndex(idx, page)) |el| {\n                    before_node = el.asNode();\n                }\n            },\n            .option => |before_option| before_node = before_option.asNode(),\n        }\n    }\n    _ = try select_node.insertBefore(element_node, before_node, page);\n}\n\n// Remove an option element by index\npub fn remove(self: *HTMLOptionsCollection, index: i32, page: *Page) void {\n    if (index < 0) {\n        return;\n    }\n\n    if (self._proto.getAtIndex(@intCast(index), page)) |element| {\n        element.remove(page);\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HTMLOptionsCollection);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLOptionsCollection\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const manage = false;\n    };\n\n    pub const length = bridge.accessor(HTMLOptionsCollection.length, null, .{});\n\n    // Indexed access\n    pub const @\"[int]\" = bridge.indexed(HTMLOptionsCollection.getAtIndex, null, .{ .null_as_undefined = true });\n    pub const @\"[str]\" = bridge.namedIndexed(HTMLOptionsCollection.getByName, null, null, .{ .null_as_undefined = true });\n\n    pub const selectedIndex = bridge.accessor(HTMLOptionsCollection.getSelectedIndex, HTMLOptionsCollection.setSelectedIndex, .{});\n    pub const add = bridge.function(HTMLOptionsCollection.add, .{});\n    pub const remove = bridge.function(HTMLOptionsCollection.remove, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/NodeList.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst log = @import(\"../../../log.zig\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Node = @import(\"../Node.zig\");\n\nconst ChildNodes = @import(\"ChildNodes.zig\");\nconst RadioNodeList = @import(\"RadioNodeList.zig\");\nconst SelectorList = @import(\"../selector/List.zig\");\nconst NodeLive = @import(\"node_live.zig\").NodeLive;\n\nconst NodeList = @This();\n\n_data: union(enum) {\n    child_nodes: *ChildNodes,\n    selector_list: *SelectorList,\n    radio_node_list: *RadioNodeList,\n    name: NodeLive(.name),\n},\n_rc: usize = 0,\n\npub fn deinit(self: *NodeList, _: bool, session: *Session) void {\n    const rc = self._rc;\n    if (rc > 1) {\n        self._rc = rc - 1;\n        return;\n    }\n\n    switch (self._data) {\n        .selector_list => |list| list.deinit(session),\n        .child_nodes => |cn| cn.deinit(session),\n        else => {},\n    }\n}\n\npub fn acquireRef(self: *NodeList) void {\n    self._rc += 1;\n}\n\npub fn length(self: *NodeList, page: *Page) !u32 {\n    return switch (self._data) {\n        .child_nodes => |impl| impl.length(page),\n        .selector_list => |impl| @intCast(impl.getLength()),\n        .radio_node_list => |impl| impl.getLength(),\n        .name => |*impl| impl.length(page),\n    };\n}\n\npub fn indexedGet(self: *NodeList, index: usize, page: *Page) !*Node {\n    return try self.getAtIndex(index, page) orelse return error.NotHandled;\n}\n\npub fn getAtIndex(self: *NodeList, index: usize, page: *Page) !?*Node {\n    return switch (self._data) {\n        .child_nodes => |impl| impl.getAtIndex(index, page),\n        .selector_list => |impl| impl.getAtIndex(index),\n        .radio_node_list => |impl| impl.getAtIndex(index, page),\n        .name => |*impl| if (impl.getAtIndex(index, page)) |el| el.asNode() else null,\n    };\n}\n\npub fn keys(self: *NodeList, page: *Page) !*KeyIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn values(self: *NodeList, page: *Page) !*ValueIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn entries(self: *NodeList, page: *Page) !*EntryIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {\n    var i: i32 = 0;\n    var it = try self.values(page);\n    while (true) : (i += 1) {\n        const next = try it.next(page);\n        if (next.done) {\n            return;\n        }\n\n        var caught: js.TryCatch.Caught = undefined;\n        cb.tryCall(void, .{ next.value, i, self }, &caught) catch {\n            log.debug(.js, \"forEach callback\", .{ .caught = caught, .source = \"nodelist\" });\n            return;\n        };\n    }\n}\n\nconst GenericIterator = @import(\"iterator.zig\").Entry;\npub const KeyIterator = GenericIterator(Iterator, \"0\");\npub const ValueIterator = GenericIterator(Iterator, \"1\");\npub const EntryIterator = GenericIterator(Iterator, null);\n\nconst Iterator = struct {\n    index: u32 = 0,\n    list: *NodeList,\n\n    const Entry = struct { u32, *Node };\n\n    pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void {\n        self.list.deinit(shutdown, session);\n    }\n\n    pub fn acquireRef(self: *Iterator) void {\n        self.list.acquireRef();\n    }\n\n    pub fn next(self: *Iterator, page: *Page) !?Entry {\n        const index = self.index;\n        const node = try self.list.getAtIndex(index, page) orelse return null;\n        self.index = index + 1;\n        return .{ index, node };\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(NodeList);\n\n    pub const Meta = struct {\n        pub const name = \"NodeList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(NodeList.deinit);\n    };\n\n    pub const length = bridge.accessor(NodeList.length, null, .{});\n    pub const @\"[]\" = bridge.indexed(NodeList.indexedGet, getIndexes, .{ .null_as_undefined = true });\n    pub const item = bridge.function(NodeList.getAtIndex, .{});\n    pub const keys = bridge.function(NodeList.keys, .{});\n    pub const values = bridge.function(NodeList.values, .{});\n    pub const entries = bridge.function(NodeList.entries, .{});\n    pub const forEach = bridge.function(NodeList.forEach, .{});\n    pub const symbol_iterator = bridge.iterator(NodeList.values, .{});\n\n    fn getIndexes(self: *NodeList, page: *Page) !js.Array {\n        const len = try self.length(page);\n        var arr = page.js.local.?.newArray(len);\n        for (0..len) |i| {\n            _ = try arr.set(@intCast(i), i, .{});\n        }\n        return arr;\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/collections/RadioNodeList.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\nconst Input = @import(\"../element/html/Input.zig\");\n\nconst NodeList = @import(\"NodeList.zig\");\nconst HTMLFormControlsCollection = @import(\"HTMLFormControlsCollection.zig\");\n\nconst RadioNodeList = @This();\n\n_proto: *NodeList,\n_name: []const u8,\n_form_collection: *HTMLFormControlsCollection,\n\npub fn getLength(self: *RadioNodeList) !u32 {\n    var i: u32 = 0;\n    var it = try self._form_collection.iterator();\n    while (it.next()) |element| {\n        if (self.matches(element)) {\n            i += 1;\n        }\n    }\n    return i;\n}\n\npub fn getAtIndex(self: *RadioNodeList, index: usize, page: *Page) !?*Node {\n    var i: usize = 0;\n    var current: usize = 0;\n    while (self._form_collection.getAtIndex(i, page)) |element| : (i += 1) {\n        if (!self.matches(element)) {\n            continue;\n        }\n        if (current == index) {\n            return element.asNode();\n        }\n        current += 1;\n    }\n    return null;\n}\n\npub fn getValue(self: *RadioNodeList) ![]const u8 {\n    var it = try self._form_collection.iterator();\n    while (it.next()) |element| {\n        const input = element.is(Input) orelse continue;\n        if (input._input_type != .radio) {\n            continue;\n        }\n        if (!input.getChecked()) {\n            continue;\n        }\n        return element.getAttributeSafe(comptime .wrap(\"value\")) orelse \"on\";\n    }\n    return \"\";\n}\n\npub fn setValue(self: *RadioNodeList, value: []const u8, page: *Page) !void {\n    var it = try self._form_collection.iterator();\n    while (it.next()) |element| {\n        const input = element.is(Input) orelse continue;\n        if (input._input_type != .radio) {\n            continue;\n        }\n\n        const input_value = element.getAttributeSafe(comptime .wrap(\"value\"));\n        const matches_value = blk: {\n            if (std.mem.eql(u8, value, \"on\")) {\n                break :blk input_value == null or (input_value != null and std.mem.eql(u8, input_value.?, \"on\"));\n            } else {\n                break :blk input_value != null and std.mem.eql(u8, input_value.?, value);\n            }\n        };\n\n        if (matches_value) {\n            try input.setChecked(true, page);\n            return;\n        }\n    }\n}\n\nfn matches(self: *const RadioNodeList, element: *Element) bool {\n    if (element.getAttributeSafe(comptime .wrap(\"id\"))) |id| {\n        if (std.mem.eql(u8, id, self._name)) {\n            return true;\n        }\n    }\n    if (element.getAttributeSafe(comptime .wrap(\"name\"))) |elem_name| {\n        if (std.mem.eql(u8, elem_name, self._name)) {\n            return true;\n        }\n    }\n    return false;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(RadioNodeList);\n\n    pub const Meta = struct {\n        pub const name = \"RadioNodeList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const length = bridge.accessor(RadioNodeList.getLength, null, .{});\n    pub const @\"[]\" = bridge.indexed(RadioNodeList.getAtIndex, null, .{ .null_as_undefined = true });\n    pub const item = bridge.function(RadioNodeList.getAtIndex, .{});\n    pub const value = bridge.accessor(RadioNodeList.getValue, RadioNodeList.setValue, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: RadioNodeList\" {\n    try testing.htmlRunner(\"collections/radio_node_list.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/collections/iterator.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\npub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {\n    const R = reflect(Inner, field);\n\n    return struct {\n        inner: Inner,\n\n        const Self = @This();\n\n        const Result = struct {\n            done: bool,\n            value: ?R.ValueType,\n\n            pub const js_as_object = true;\n        };\n\n        pub fn init(inner: Inner, page: *Page) !*Self {\n            return page._factory.create(Self{ .inner = inner });\n        }\n\n        pub fn deinit(self: *Self, shutdown: bool, session: *Session) void {\n            if (@hasDecl(Inner, \"deinit\")) {\n                self.inner.deinit(shutdown, session);\n            }\n        }\n\n        pub fn acquireRef(self: *Self) void {\n            if (@hasDecl(Inner, \"acquireRef\")) {\n                self.inner.acquireRef();\n            }\n        }\n\n        pub fn next(self: *Self, page: *Page) if (R.has_error_return) anyerror!Result else Result {\n            const entry = (if (comptime R.has_error_return) try self.inner.next(page) else self.inner.next(page)) orelse {\n                return .{ .done = true, .value = null };\n            };\n\n            if (comptime field == null) {\n                return .{ .done = false, .value = entry };\n            }\n\n            return .{\n                .done = false,\n                .value = @field(entry, field.?),\n            };\n        }\n\n        pub const JsApi = struct {\n            pub const bridge = js.Bridge(Self);\n\n            pub const Meta = struct {\n                pub const prototype_chain = bridge.prototypeChain();\n                pub var class_id: bridge.ClassId = undefined;\n                pub const weak = true;\n                pub const finalizer = bridge.finalizer(Self.deinit);\n            };\n\n            pub const next = bridge.function(Self.next, .{ .null_as_undefined = true });\n            pub const symbol_iterator = bridge.iterator(Self, .{});\n        };\n    };\n}\n\nfn reflect(comptime Inner: type, comptime field: ?[]const u8) Reflect {\n    const R = @typeInfo(@TypeOf(Inner.next)).@\"fn\".return_type.?;\n    const has_error_return = @typeInfo(R) == .error_union;\n    return .{\n        .has_error_return = has_error_return,\n        .ValueType = ValueType(unwrapOptional(unwrapError(R)), field),\n    };\n}\n\nconst Reflect = struct {\n    has_error_return: bool,\n    ValueType: type,\n};\n\nfn unwrapError(comptime T: type) type {\n    if (@typeInfo(T) == .error_union) {\n        return @typeInfo(T).error_union.payload;\n    }\n    return T;\n}\n\nfn unwrapOptional(comptime T: type) type {\n    return @typeInfo(T).optional.child;\n}\n\nfn ValueType(comptime R: type, comptime field_: ?[]const u8) type {\n    const field = field_ orelse return R;\n    inline for (@typeInfo(R).@\"struct\".fields) |f| {\n        if (comptime std.mem.eql(u8, f.name, field)) {\n            return f.type;\n        }\n    }\n    @compileError(\"Unknown EntryIterator field \" ++ @typeName(R) ++ \".\" ++ field);\n}\n"
  },
  {
    "path": "src/browser/webapi/collections/node_live.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\nconst TreeWalker = @import(\"../TreeWalker.zig\");\nconst Selector = @import(\"../selector/Selector.zig\");\nconst Form = @import(\"../element/html/Form.zig\");\n\nconst Mode = enum {\n    tag,\n    tag_name,\n    tag_name_ns,\n    class_name,\n    name,\n    all_elements,\n    child_elements,\n    child_tag,\n    selected_options,\n    links,\n    anchors,\n    form,\n};\n\npub const TagNameNsFilter = struct {\n    namespace: ?Element.Namespace, // null means wildcard \"*\"\n    local_name: String,\n};\n\nconst Filters = union(Mode) {\n    tag: Element.Tag,\n    tag_name: String,\n    tag_name_ns: TagNameNsFilter,\n    class_name: [][]const u8,\n    name: []const u8,\n    all_elements,\n    child_elements,\n    child_tag: Element.Tag,\n    selected_options,\n    links,\n    anchors,\n    form: *Form,\n\n    fn TypeOf(comptime mode: Mode) type {\n        @setEvalBranchQuota(2000);\n        return std.meta.fieldInfo(Filters, mode).type;\n    }\n};\n\n// Operations on the live DOM can be inefficient. Do we really have to walk\n// through the entire tree, filtering out elements we don't care about, every\n// time .length is called?\n// To improve this, we track the \"version\" of the DOM (root.version). If the\n// version changes between operations, than we have to restart and pay the full\n// price.\n// But, if the version hasn't changed, then we can leverage other stateful data\n// to improve performance. For example, we cache the length property. So once\n// we've walked the tree to figure the length, we can re-use the cached property\n// if the DOM is unchanged (i.e. if our _cached_version == page.version).\n//\n// We do something similar for indexed getter (e.g. coll[4]), by preserving the\n// last node visited in the tree (implicitly by not resetting the TreeWalker).\n// If the DOM version is unchanged and the new index >= the last one, we can do\n// not have to reset our TreeWalker. This optimizes the common case of accessing\n// the collection via incrementing indexes.\n\npub fn NodeLive(comptime mode: Mode) type {\n    const Filter = Filters.TypeOf(mode);\n    const TW = switch (mode) {\n        .tag, .tag_name, .tag_name_ns, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,\n        .child_elements, .child_tag, .selected_options => TreeWalker.Children,\n    };\n    return struct {\n        _tw: TW,\n        _filter: Filter,\n        _last_index: usize,\n        _last_length: ?u32,\n        _cached_version: usize,\n\n        const Self = @This();\n\n        pub fn init(root: *Node, filter: Filter, page: *Page) Self {\n            return .{\n                ._last_index = 0,\n                ._last_length = null,\n                ._filter = filter,\n                ._tw = TW.init(root, .{}),\n                ._cached_version = page.version,\n            };\n        }\n\n        pub fn length(self: *Self, page: *const Page) u32 {\n            if (self.versionCheck(page)) {\n                // the DOM version hasn't changed, use the cached version if\n                // we have one\n                if (self._last_length) |cached_length| {\n                    return cached_length;\n                }\n                // not ideal, but this can happen if list[x] is called followed\n                // by list.length.\n                self._tw.reset();\n                self._last_index = 0;\n            }\n            // If we're here, it means it's either the first time we're called\n            // or the DOM version has changed. Either way, the _tw should be\n            // at the start position. It's important that self._last_index == 0\n            // (which it always should be in these cases), because we're going to\n            // reset _tw at the end of this, _last_index should always be 0 when\n            // _tw is reset. Again, this should always be the case, but we're\n            // asserting to make sure, else we'll have weird behavior, namely\n            // the wrong item being returned for the wrong index.\n            lp.assert(self._last_index == 0, \"NodeLives.length\", .{ .last_index = self._last_index });\n\n            var tw = &self._tw;\n            defer tw.reset();\n\n            var l: u32 = 0;\n            while (self.nextTw(tw)) |_| {\n                l += 1;\n            }\n\n            self._last_length = l;\n            return l;\n        }\n\n        // This API supports indexing by both numeric index and id/name\n        // i.e. a combination of getAtIndex and getByName\n        pub fn getIndexed(self: *Self, value: js.Atom, page: *Page) !?*Element {\n            if (value.isUint()) |n| {\n                return self.getAtIndex(n, page);\n            }\n\n            const name = value.toString();\n            defer value.freeString(name);\n\n            return self.getByName(name, page) orelse return error.NotHandled;\n        }\n\n        pub fn getAtIndex(self: *Self, index: usize, page: *const Page) ?*Element {\n            _ = self.versionCheck(page);\n            var current = self._last_index;\n            if (index <= current) {\n                current = 0;\n                self._tw.reset();\n            }\n            defer self._last_index = current + 1;\n\n            const tw = &self._tw;\n            while (self.nextTw(tw)) |el| {\n                if (index == current) {\n                    return el;\n                }\n                current += 1;\n            }\n            return null;\n        }\n\n        pub fn getByName(self: *Self, name: []const u8, page: *Page) ?*Element {\n            if (page.document.getElementById(name, page)) |element| {\n                const node = element.asNode();\n                if (self._tw.contains(node) and self.matches(node)) {\n                    return element;\n                }\n            }\n\n            // Element not found by id, fallback to search by name. This isn't\n            // efficient!\n\n            // Gives us a TreeWalker based on the original, but reset to the\n            // root. Doing this preserves any cache data we have for other calls\n            // (like length or getAtIndex)\n            var tw = self._tw.clone();\n            while (self.nextTw(&tw)) |element| {\n                const element_name = element.getAttributeSafe(comptime .wrap(\"name\")) orelse continue;\n                if (std.mem.eql(u8, element_name, name)) {\n                    return element;\n                }\n            }\n            return null;\n        }\n\n        pub fn next(self: *Self) ?*Element {\n            return self.nextTw(&self._tw);\n        }\n\n        pub fn nextTw(self: *Self, tw: *TW) ?*Element {\n            while (tw.next()) |node| {\n                if (self.matches(node)) {\n                    return node.as(Element);\n                }\n            }\n            return null;\n        }\n\n        fn matches(self: *const Self, node: *Node) bool {\n            switch (mode) {\n                .tag => {\n                    const el = node.is(Element) orelse return false;\n                    // For HTML namespace elements, we can use the optimized tag comparison.\n                    // For other namespaces (XML, SVG custom elements, etc.), fall back to string comparison.\n                    if (el._namespace == .html) {\n                        return el.getTag() == self._filter;\n                    }\n                    // For non-HTML elements, compare by tag name string\n                    const element_tag = el.getTagNameLower();\n                    return std.mem.eql(u8, element_tag, @tagName(self._filter));\n                },\n                .tag_name => {\n                    // If we're in `tag_name` mode, then the tag_name isn't\n                    // a known tag. It could be a custom element, heading, or\n                    // any generic element. Compare against the element's tag name.\n                    // Per spec, getElementsByTagName is case-insensitive for HTML\n                    // namespace elements, case-sensitive for others.\n                    const el = node.is(Element) orelse return false;\n                    const element_tag = el.getTagNameLower();\n                    if (el._namespace == .html) {\n                        return std.ascii.eqlIgnoreCase(element_tag, self._filter.str());\n                    }\n                    return std.mem.eql(u8, element_tag, self._filter.str());\n                },\n                .tag_name_ns => {\n                    const el = node.is(Element) orelse return false;\n                    if (self._filter.namespace) |ns| {\n                        if (el._namespace != ns) return false;\n                    }\n                    // ok, namespace matches, check local name\n                    if (self._filter.local_name.eql(comptime .wrap(\"*\"))) {\n                        // wildcard, match-all\n                        return true;\n                    }\n                    return self._filter.local_name.eqlSlice(el.getLocalName());\n                },\n                .class_name => {\n                    if (self._filter.len == 0) {\n                        return false;\n                    }\n\n                    const el = node.is(Element) orelse return false;\n                    const class_attr = el.getAttributeSafe(comptime .wrap(\"class\")) orelse return false;\n                    for (self._filter) |class_name| {\n                        if (!Selector.classAttributeContains(class_attr, class_name)) {\n                            return false;\n                        }\n                    }\n                    return true;\n                },\n                .name => {\n                    const el = node.is(Element) orelse return false;\n                    const name_attr = el.getAttributeSafe(comptime .wrap(\"name\")) orelse return false;\n                    return std.mem.eql(u8, name_attr, self._filter);\n                },\n                .all_elements => return node._type == .element,\n                .child_elements => return node._type == .element,\n                .child_tag => {\n                    const el = node.is(Element) orelse return false;\n                    return el.getTag() == self._filter;\n                },\n                .selected_options => {\n                    const el = node.is(Element) orelse return false;\n                    const Option = Element.Html.Option;\n                    const opt = el.is(Option) orelse return false;\n                    return opt.getSelected();\n                },\n                .links => {\n                    // Links are <a> elements with href attribute (TODO: also <area> when implemented)\n                    const el = node.is(Element) orelse return false;\n                    const Anchor = Element.Html.Anchor;\n                    if (el.is(Anchor) == null) return false;\n                    return el.hasAttributeSafe(comptime .wrap(\"href\"));\n                },\n                .anchors => {\n                    // Anchors are <a> elements with name attribute\n                    const el = node.is(Element) orelse return false;\n                    const Anchor = Element.Html.Anchor;\n                    if (el.is(Anchor) == null) return false;\n                    return el.hasAttributeSafe(comptime .wrap(\"name\"));\n                },\n                .form => {\n                    const el = node.is(Element) orelse return false;\n                    if (!isFormControl(el)) {\n                        return false;\n                    }\n\n                    if (el.getAttributeSafe(comptime .wrap(\"form\"))) |form_attr| {\n                        const form_id = self._filter.asElement().getAttributeSafe(comptime .wrap(\"id\")) orelse return false;\n                        return std.mem.eql(u8, form_attr, form_id);\n                    }\n\n                    // No form attribute - match if descendant of our form\n                    // This does an O(depth) ancestor walk for each control in the form.\n                    //\n                    // TODO: If profiling shows this is a bottleneck:\n                    // When we first encounter the form element during tree walk, we could\n                    // do a one-time reverse walk to find the LAST control that belongs to\n                    // this form (checking both form controls and their form= attributes).\n                    // Store that element in a new FormState. Then as we traverse\n                    // forward:\n                    //   - Set is_within_form = true when we enter the form element\n                    //   - Return true immediately for any control while is_within_form\n                    //   - Set is_within_form = false when we reach that last element\n                    // This trades one O(form_size) reverse walk for N O(depth) ancestor\n                    // checks, where N = number of controls. For forms with many nested\n                    // controls, this could be significantly faster.\n                    return self._filter.asNode().contains(node);\n                },\n            }\n        }\n\n        fn isFormControl(el: *Element) bool {\n            if (el._type != .html) return false;\n            const html = el._type.html;\n            return switch (html._type) {\n                .input, .button, .select, .textarea => true,\n                else => false,\n            };\n        }\n\n        fn versionCheck(self: *Self, page: *const Page) bool {\n            const current = page.version;\n            if (current == self._cached_version) {\n                return true;\n            }\n\n            self._tw.reset();\n            self._last_index = 0;\n            self._last_length = null;\n            self._cached_version = current;\n            return false;\n        }\n\n        const HTMLCollection = @import(\"HTMLCollection.zig\");\n        const NodeList = @import(\"NodeList.zig\");\n\n        pub fn runtimeGenericWrap(self: Self, page: *Page) !if (mode == .name) *NodeList else *HTMLCollection {\n            const collection = switch (mode) {\n                .name => return page._factory.create(NodeList{ ._data = .{ .name = self } }),\n                .tag => HTMLCollection{ ._data = .{ .tag = self } },\n                .tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },\n                .tag_name_ns => HTMLCollection{ ._data = .{ .tag_name_ns = self } },\n                .class_name => HTMLCollection{ ._data = .{ .class_name = self } },\n                .all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },\n                .child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },\n                .child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },\n                .selected_options => HTMLCollection{ ._data = .{ .selected_options = self } },\n                .links => HTMLCollection{ ._data = .{ .links = self } },\n                .anchors => HTMLCollection{ ._data = .{ .anchors = self } },\n                .form => HTMLCollection{ ._data = .{ .form = self } },\n            };\n            return page._factory.create(collection);\n        }\n    };\n}\n"
  },
  {
    "path": "src/browser/webapi/collections.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\npub const NodeLive = @import(\"collections/node_live.zig\").NodeLive;\npub const ChildNodes = @import(\"collections/ChildNodes.zig\");\npub const DOMTokenList = @import(\"collections/DOMTokenList.zig\");\npub const RadioNodeList = @import(\"collections/RadioNodeList.zig\");\npub const HTMLCollection = @import(\"collections/HTMLCollection.zig\");\npub const HTMLAllCollection = @import(\"collections/HTMLAllCollection.zig\");\npub const HTMLOptionsCollection = @import(\"collections/HTMLOptionsCollection.zig\");\npub const HTMLFormControlsCollection = @import(\"collections/HTMLFormControlsCollection.zig\");\n\npub fn registerTypes() []const type {\n    return &.{\n        HTMLCollection,\n        HTMLCollection.Iterator,\n        @import(\"collections/NodeList.zig\"),\n        @import(\"collections/NodeList.zig\").KeyIterator,\n        @import(\"collections/NodeList.zig\").ValueIterator,\n        @import(\"collections/NodeList.zig\").EntryIterator,\n        @import(\"collections/HTMLAllCollection.zig\"),\n        @import(\"collections/HTMLAllCollection.zig\").Iterator,\n        HTMLOptionsCollection,\n        HTMLFormControlsCollection,\n        RadioNodeList,\n        DOMTokenList,\n        DOMTokenList.KeyIterator,\n        DOMTokenList.ValueIterator,\n        DOMTokenList.EntryIterator,\n    };\n}\n"
  },
  {
    "path": "src/browser/webapi/css/CSSRule.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst CSSRule = @This();\n\npub const Type = enum(u16) {\n    style = 1,\n    charset = 2,\n    import = 3,\n    media = 4,\n    font_face = 5,\n    page = 6,\n    keyframes = 7,\n    keyframe = 8,\n    margin = 9,\n    namespace = 10,\n    counter_style = 11,\n    supports = 12,\n    document = 13,\n    font_feature_values = 14,\n    viewport = 15,\n    region_style = 16,\n};\n\n_type: Type,\n\npub fn init(rule_type: Type, page: *Page) !*CSSRule {\n    return page._factory.create(CSSRule{\n        ._type = rule_type,\n    });\n}\n\npub fn getType(self: *const CSSRule) u16 {\n    return @intFromEnum(self._type);\n}\n\npub fn getCssText(self: *const CSSRule, page: *Page) []const u8 {\n    _ = self;\n    _ = page;\n    return \"\";\n}\n\npub fn setCssText(self: *CSSRule, text: []const u8, page: *Page) !void {\n    _ = self;\n    _ = text;\n    _ = page;\n}\n\npub fn getParentRule(self: *const CSSRule) ?*CSSRule {\n    _ = self;\n    return null;\n}\n\npub fn getParentStyleSheet(self: *const CSSRule) ?*CSSRule {\n    _ = self;\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSSRule);\n\n    pub const Meta = struct {\n        pub const name = \"CSSRule\";\n        pub var class_id: bridge.ClassId = undefined;\n        pub const prototype_chain = bridge.prototypeChain();\n    };\n\n    pub const STYLE_RULE = 1;\n    pub const CHARSET_RULE = 2;\n    pub const IMPORT_RULE = 3;\n    pub const MEDIA_RULE = 4;\n    pub const FONT_FACE_RULE = 5;\n    pub const PAGE_RULE = 6;\n    pub const KEYFRAMES_RULE = 7;\n    pub const KEYFRAME_RULE = 8;\n    pub const MARGIN_RULE = 9;\n    pub const NAMESPACE_RULE = 10;\n    pub const COUNTER_STYLE_RULE = 11;\n    pub const SUPPORTS_RULE = 12;\n    pub const DOCUMENT_RULE = 13;\n    pub const FONT_FEATURE_VALUES_RULE = 14;\n    pub const VIEWPORT_RULE = 15;\n    pub const REGION_STYLE_RULE = 16;\n\n    pub const @\"type\" = bridge.accessor(CSSRule.getType, null, .{});\n    pub const cssText = bridge.accessor(CSSRule.getCssText, CSSRule.setCssText, .{});\n    pub const parentRule = bridge.accessor(CSSRule.getParentRule, null, .{});\n    pub const parentStyleSheet = bridge.accessor(CSSRule.getParentStyleSheet, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/css/CSSRuleList.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst CSSRule = @import(\"CSSRule.zig\");\n\nconst CSSRuleList = @This();\n\n_rules: []*CSSRule = &.{},\n\npub fn init(page: *Page) !*CSSRuleList {\n    return page._factory.create(CSSRuleList{});\n}\n\npub fn length(self: *const CSSRuleList) u32 {\n    return @intCast(self._rules.len);\n}\n\npub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule {\n    if (index >= self._rules.len) {\n        return null;\n    }\n    return self._rules[index];\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSSRuleList);\n\n    pub const Meta = struct {\n        pub const name = \"CSSRuleList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const length = bridge.accessor(CSSRuleList.length, null, .{});\n    pub const @\"[]\" = bridge.indexed(CSSRuleList.item, null, .{ .null_as_undefined = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/css/CSSStyleDeclaration.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../../log.zig\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst CssParser = @import(\"../../css/Parser.zig\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Element = @import(\"../Element.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst CSSStyleDeclaration = @This();\n\n_element: ?*Element = null,\n_properties: std.DoublyLinkedList = .{},\n_is_computed: bool = false,\n\npub fn init(element: ?*Element, is_computed: bool, page: *Page) !*CSSStyleDeclaration {\n    const self = try page._factory.create(CSSStyleDeclaration{\n        ._element = element,\n        ._is_computed = is_computed,\n    });\n\n    // Parse the element's existing style attribute into _properties so that\n    // subsequent JS reads and writes see all CSS properties, not just newly\n    // added ones.  Computed styles have no inline attribute to parse.\n    if (!is_computed) {\n        if (element) |el| {\n            if (el.getAttributeSafe(comptime .wrap(\"style\"))) |attr_value| {\n                var it = CssParser.parseDeclarationsList(attr_value);\n                while (it.next()) |declaration| {\n                    try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, page);\n                }\n            }\n        }\n    }\n\n    return self;\n}\n\npub fn length(self: *const CSSStyleDeclaration) u32 {\n    return @intCast(self._properties.len());\n}\n\npub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {\n    var i: u32 = 0;\n    var node = self._properties.first;\n    while (node) |n| {\n        if (i == index) {\n            const prop = Property.fromNodeLink(n);\n            return prop._name.str();\n        }\n        i += 1;\n        node = n.next;\n    }\n    return \"\";\n}\n\npub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {\n    const normalized = normalizePropertyName(property_name, &page.buf);\n    const prop = self.findProperty(normalized) orelse {\n        // Only return default values for computed styles\n        if (self._is_computed) {\n            return getDefaultPropertyValue(self, normalized);\n        }\n        return \"\";\n    };\n    return prop._value.str();\n}\n\npub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {\n    const normalized = normalizePropertyName(property_name, &page.buf);\n    const prop = self.findProperty(normalized) orelse return \"\";\n    return if (prop._important) \"important\" else \"\";\n}\n\npub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, priority_: ?[]const u8, page: *Page) !void {\n    // Validate priority\n    const priority = priority_ orelse \"\";\n    const important = if (priority.len > 0) blk: {\n        if (!std.ascii.eqlIgnoreCase(priority, \"important\")) {\n            return;\n        }\n        break :blk true;\n    } else false;\n\n    try self.setPropertyImpl(property_name, value, important, page);\n\n    try self.syncStyleAttribute(page);\n}\n\nfn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: []const u8, important: bool, page: *Page) !void {\n    if (value.len == 0) {\n        _ = try self.removePropertyImpl(property_name, page);\n        return;\n    }\n\n    const normalized = normalizePropertyName(property_name, &page.buf);\n\n    // Normalize the value for canonical serialization\n    const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);\n\n    // Find existing property\n    if (self.findProperty(normalized)) |existing| {\n        existing._value = try String.init(page.arena, normalized_value, .{});\n        existing._important = important;\n        return;\n    }\n\n    // Create new property\n    const prop = try page._factory.create(Property{\n        ._node = .{},\n        ._name = try String.init(page.arena, normalized, .{}),\n        ._value = try String.init(page.arena, normalized_value, .{}),\n        ._important = important,\n    });\n    self._properties.append(&prop._node);\n}\n\npub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {\n    const result = try self.removePropertyImpl(property_name, page);\n    try self.syncStyleAttribute(page);\n    return result;\n}\n\nfn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {\n    const normalized = normalizePropertyName(property_name, &page.buf);\n    const prop = self.findProperty(normalized) orelse return \"\";\n\n    // the value might not be on the heap (it could be inlined in the small string\n    // optimization), so we need to dupe it.\n    const old_value = try page.call_arena.dupe(u8, prop._value.str());\n    self._properties.remove(&prop._node);\n    page._factory.destroy(prop);\n    return old_value;\n}\n\n// Serialize current properties back to the element's style attribute so that\n// DOM serialization (outerHTML, getAttribute) reflects JS-modified styles.\nfn syncStyleAttribute(self: *CSSStyleDeclaration, page: *Page) !void {\n    const element = self._element orelse return;\n    const css_text = try self.getCssText(page);\n    try element.setAttributeSafe(comptime .wrap(\"style\"), .wrap(css_text), page);\n}\n\npub fn getFloat(self: *const CSSStyleDeclaration, page: *Page) []const u8 {\n    return self.getPropertyValue(\"float\", page);\n}\n\npub fn setFloat(self: *CSSStyleDeclaration, value_: ?[]const u8, page: *Page) !void {\n    try self.setPropertyImpl(\"float\", value_ orelse \"\", false, page);\n    try self.syncStyleAttribute(page);\n}\n\npub fn getCssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {\n    if (self._element == null) return \"\";\n\n    var buf = std.Io.Writer.Allocating.init(page.call_arena);\n    try self.format(&buf.writer);\n    return buf.written();\n}\n\npub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {\n    if (self._element == null) return;\n\n    // Clear existing properties\n    var node = self._properties.first;\n    while (node) |n| {\n        const next = n.next;\n        const prop = Property.fromNodeLink(n);\n        self._properties.remove(n);\n        page._factory.destroy(prop);\n        node = next;\n    }\n\n    // Parse and set new properties\n    var it = CssParser.parseDeclarationsList(text);\n    while (it.next()) |declaration| {\n        try self.setPropertyImpl(declaration.name, declaration.value, declaration.important, page);\n    }\n    try self.syncStyleAttribute(page);\n}\n\npub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {\n    const node = self._properties.first orelse return;\n    try Property.fromNodeLink(node).format(writer);\n\n    var next = node.next;\n    while (next) |n| {\n        try writer.writeByte(' ');\n        try Property.fromNodeLink(n).format(writer);\n        next = n.next;\n    }\n}\n\nfn findProperty(self: *const CSSStyleDeclaration, name: []const u8) ?*Property {\n    var node = self._properties.first;\n    while (node) |n| {\n        const prop = Property.fromNodeLink(n);\n        if (prop._name.eqlSlice(name)) {\n            return prop;\n        }\n        node = n.next;\n    }\n    return null;\n}\n\nfn normalizePropertyName(name: []const u8, buf: []u8) []const u8 {\n    if (name.len > buf.len) {\n        log.info(.dom, \"css.long.name\", .{ .name = name });\n        return name;\n    }\n    return std.ascii.lowerString(buf, name);\n}\n\n// Normalize CSS property values for canonical serialization\nfn normalizePropertyValue(arena: Allocator, property_name: []const u8, value: []const u8) ![]const u8 {\n    // Per CSSOM spec, unitless zero in length properties should serialize as \"0px\"\n    if (std.mem.eql(u8, value, \"0\") and isLengthProperty(property_name)) {\n        return \"0px\";\n    }\n\n    // \"first baseline\" serializes canonically as \"baseline\" (first is the default)\n    if (std.ascii.startsWithIgnoreCase(value, \"first baseline\")) {\n        if (value.len == 14) {\n            // Exact match \"first baseline\"\n            return \"baseline\";\n        }\n        if (value.len > 14 and value[14] == ' ') {\n            // \"first baseline X\" -> \"baseline X\"\n            return try std.mem.concat(arena, u8, &.{ \"baseline\", value[14..] });\n        }\n    }\n\n    // For 2-value shorthand properties, collapse \"X X\" to \"X\"\n    if (isTwoValueShorthand(property_name)) {\n        if (collapseDuplicateValue(value)) |single| {\n            return single;\n        }\n    }\n\n    // Canonicalize anchor-size() function: anchor name (dashed ident) comes before size keyword\n    if (std.mem.indexOf(u8, value, \"anchor-size(\") != null) {\n        return try canonicalizeAnchorSize(arena, value);\n    }\n\n    return value;\n}\n\n// Canonicalize anchor-size() so that the dashed ident (anchor name) comes before the size keyword.\n// e.g. \"anchor-size(width --foo)\" -> \"anchor-size(--foo width)\"\nfn canonicalizeAnchorSize(arena: Allocator, value: []const u8) ![]const u8 {\n    var buf = std.Io.Writer.Allocating.init(arena);\n    var i: usize = 0;\n\n    while (i < value.len) {\n        // Look for \"anchor-size(\"\n        if (std.mem.startsWith(u8, value[i..], \"anchor-size(\")) {\n            try buf.writer.writeAll(\"anchor-size(\");\n            i += \"anchor-size(\".len;\n\n            // Parse and canonicalize the arguments\n            i = try canonicalizeAnchorSizeArgs(value, i, &buf.writer);\n        } else {\n            try buf.writer.writeByte(value[i]);\n            i += 1;\n        }\n    }\n\n    return buf.written();\n}\n\n// Parse anchor-size arguments and write them in canonical order\nfn canonicalizeAnchorSizeArgs(value: []const u8, start: usize, writer: *std.Io.Writer) !usize {\n    var i = start;\n    var depth: usize = 1;\n\n    // Skip leading whitespace\n    while (i < value.len and value[i] == ' ') : (i += 1) {}\n\n    // Collect tokens before the comma or close paren\n    var first_token_start: ?usize = null;\n    var first_token_end: usize = 0;\n    var second_token_start: ?usize = null;\n    var second_token_end: usize = 0;\n    var comma_pos: ?usize = null;\n    var token_count: usize = 0;\n\n    const args_start = i;\n    var in_token = false;\n\n    // First pass: find the structure of arguments before comma/closing paren at depth 1\n    while (i < value.len and depth > 0) {\n        const c = value[i];\n\n        if (c == '(') {\n            depth += 1;\n            in_token = true;\n            i += 1;\n        } else if (c == ')') {\n            depth -= 1;\n            if (depth == 0) {\n                if (in_token) {\n                    if (token_count == 0) {\n                        first_token_end = i;\n                    } else if (token_count == 1) {\n                        second_token_end = i;\n                    }\n                }\n                break;\n            }\n            i += 1;\n        } else if (c == ',' and depth == 1) {\n            if (in_token) {\n                if (token_count == 0) {\n                    first_token_end = i;\n                } else if (token_count == 1) {\n                    second_token_end = i;\n                }\n            }\n            comma_pos = i;\n            break;\n        } else if (c == ' ') {\n            if (in_token and depth == 1) {\n                if (token_count == 0) {\n                    first_token_end = i;\n                    token_count = 1;\n                } else if (token_count == 1 and second_token_start != null) {\n                    second_token_end = i;\n                    token_count = 2;\n                }\n                in_token = false;\n            }\n            i += 1;\n        } else {\n            if (!in_token and depth == 1) {\n                if (token_count == 0) {\n                    first_token_start = i;\n                } else if (token_count == 1) {\n                    second_token_start = i;\n                }\n                in_token = true;\n            }\n            i += 1;\n        }\n    }\n\n    // Handle end of tokens\n    if (in_token and token_count == 1 and second_token_start != null) {\n        second_token_end = i;\n        token_count = 2;\n    } else if (in_token and token_count == 0) {\n        first_token_end = i;\n        token_count = 1;\n    }\n\n    // Check if we have exactly two tokens that need reordering\n    if (token_count == 2) {\n        const first_start = first_token_start orelse args_start;\n        const second_start = second_token_start orelse first_token_end;\n\n        const first_token = value[first_start..first_token_end];\n        const second_token = value[second_start..second_token_end];\n\n        // If second token is a dashed ident and first is a size keyword, swap them\n        if (std.mem.startsWith(u8, second_token, \"--\") and isAnchorSizeKeyword(first_token)) {\n            try writer.writeAll(second_token);\n            try writer.writeByte(' ');\n            try writer.writeAll(first_token);\n        } else {\n            // Keep original order\n            try writer.writeAll(first_token);\n            try writer.writeByte(' ');\n            try writer.writeAll(second_token);\n        }\n    } else if (first_token_start) |fts| {\n        // Single token, just copy it\n        try writer.writeAll(value[fts..first_token_end]);\n    }\n\n    // Handle comma and fallback value (may contain nested anchor-size)\n    if (comma_pos) |cp| {\n        try writer.writeAll(\", \");\n        i = cp + 1;\n        // Skip whitespace after comma\n        while (i < value.len and value[i] == ' ') : (i += 1) {}\n\n        // Copy the fallback, recursively handling nested anchor-size\n        while (i < value.len and depth > 0) {\n            if (std.mem.startsWith(u8, value[i..], \"anchor-size(\")) {\n                try writer.writeAll(\"anchor-size(\");\n                i += \"anchor-size(\".len;\n                depth += 1;\n                i = try canonicalizeAnchorSizeArgs(value, i, writer);\n                depth -= 1;\n            } else if (value[i] == '(') {\n                depth += 1;\n                try writer.writeByte(value[i]);\n                i += 1;\n            } else if (value[i] == ')') {\n                depth -= 1;\n                if (depth == 0) break;\n                try writer.writeByte(value[i]);\n                i += 1;\n            } else {\n                try writer.writeByte(value[i]);\n                i += 1;\n            }\n        }\n    }\n\n    // Write closing paren\n    try writer.writeByte(')');\n\n    return i + 1; // Skip past the closing paren\n}\n\nfn isAnchorSizeKeyword(token: []const u8) bool {\n    const keywords = std.StaticStringMap(void).initComptime(.{\n        .{ \"width\", {} },\n        .{ \"height\", {} },\n        .{ \"block\", {} },\n        .{ \"inline\", {} },\n        .{ \"self-block\", {} },\n        .{ \"self-inline\", {} },\n    });\n    return keywords.has(token);\n}\n\n// Check if a value is \"X X\" (duplicate) and return just \"X\"\nfn collapseDuplicateValue(value: []const u8) ?[]const u8 {\n    const space_idx = std.mem.indexOfScalar(u8, value, ' ') orelse return null;\n    if (space_idx == 0 or space_idx >= value.len - 1) return null;\n\n    const first = value[0..space_idx];\n    const rest = std.mem.trimLeft(u8, value[space_idx + 1 ..], \" \");\n\n    // Check if there's only one more value (no additional spaces)\n    if (std.mem.indexOfScalar(u8, rest, ' ') != null) return null;\n\n    if (std.mem.eql(u8, first, rest)) {\n        return first;\n    }\n    return null;\n}\n\nfn isTwoValueShorthand(name: []const u8) bool {\n    const shorthands = std.StaticStringMap(void).initComptime(.{\n        .{ \"place-content\", {} },\n        .{ \"place-items\", {} },\n        .{ \"place-self\", {} },\n        .{ \"margin-block\", {} },\n        .{ \"margin-inline\", {} },\n        .{ \"padding-block\", {} },\n        .{ \"padding-inline\", {} },\n        .{ \"inset-block\", {} },\n        .{ \"inset-inline\", {} },\n        .{ \"border-block-style\", {} },\n        .{ \"border-inline-style\", {} },\n        .{ \"border-block-width\", {} },\n        .{ \"border-inline-width\", {} },\n        .{ \"border-block-color\", {} },\n        .{ \"border-inline-color\", {} },\n        .{ \"overflow\", {} },\n        .{ \"overscroll-behavior\", {} },\n        .{ \"gap\", {} },\n        .{ \"grid-gap\", {} },\n        // Scroll\n        .{ \"scroll-padding-block\", {} },\n        .{ \"scroll-padding-inline\", {} },\n        .{ \"scroll-snap-align\", {} },\n        // Background/Mask\n        .{ \"background-size\", {} },\n        .{ \"border-image-repeat\", {} },\n        .{ \"mask-repeat\", {} },\n        .{ \"mask-size\", {} },\n    });\n    return shorthands.has(name);\n}\n\nfn isLengthProperty(name: []const u8) bool {\n    // Properties that accept <length> or <length-percentage> values\n    const length_properties = std.StaticStringMap(void).initComptime(.{\n        // Sizing\n        .{ \"width\", {} },\n        .{ \"height\", {} },\n        .{ \"min-width\", {} },\n        .{ \"min-height\", {} },\n        .{ \"max-width\", {} },\n        .{ \"max-height\", {} },\n        // Margins\n        .{ \"margin\", {} },\n        .{ \"margin-top\", {} },\n        .{ \"margin-right\", {} },\n        .{ \"margin-bottom\", {} },\n        .{ \"margin-left\", {} },\n        .{ \"margin-block\", {} },\n        .{ \"margin-block-start\", {} },\n        .{ \"margin-block-end\", {} },\n        .{ \"margin-inline\", {} },\n        .{ \"margin-inline-start\", {} },\n        .{ \"margin-inline-end\", {} },\n        // Padding\n        .{ \"padding\", {} },\n        .{ \"padding-top\", {} },\n        .{ \"padding-right\", {} },\n        .{ \"padding-bottom\", {} },\n        .{ \"padding-left\", {} },\n        .{ \"padding-block\", {} },\n        .{ \"padding-block-start\", {} },\n        .{ \"padding-block-end\", {} },\n        .{ \"padding-inline\", {} },\n        .{ \"padding-inline-start\", {} },\n        .{ \"padding-inline-end\", {} },\n        // Positioning\n        .{ \"top\", {} },\n        .{ \"right\", {} },\n        .{ \"bottom\", {} },\n        .{ \"left\", {} },\n        .{ \"inset\", {} },\n        .{ \"inset-block\", {} },\n        .{ \"inset-block-start\", {} },\n        .{ \"inset-block-end\", {} },\n        .{ \"inset-inline\", {} },\n        .{ \"inset-inline-start\", {} },\n        .{ \"inset-inline-end\", {} },\n        // Border\n        .{ \"border-width\", {} },\n        .{ \"border-top-width\", {} },\n        .{ \"border-right-width\", {} },\n        .{ \"border-bottom-width\", {} },\n        .{ \"border-left-width\", {} },\n        .{ \"border-block-width\", {} },\n        .{ \"border-block-start-width\", {} },\n        .{ \"border-block-end-width\", {} },\n        .{ \"border-inline-width\", {} },\n        .{ \"border-inline-start-width\", {} },\n        .{ \"border-inline-end-width\", {} },\n        .{ \"border-radius\", {} },\n        .{ \"border-top-left-radius\", {} },\n        .{ \"border-top-right-radius\", {} },\n        .{ \"border-bottom-left-radius\", {} },\n        .{ \"border-bottom-right-radius\", {} },\n        // Text\n        .{ \"font-size\", {} },\n        .{ \"letter-spacing\", {} },\n        .{ \"word-spacing\", {} },\n        .{ \"text-indent\", {} },\n        // Flexbox/Grid\n        .{ \"gap\", {} },\n        .{ \"row-gap\", {} },\n        .{ \"column-gap\", {} },\n        .{ \"flex-basis\", {} },\n        // Legacy grid aliases\n        .{ \"grid-column-gap\", {} },\n        .{ \"grid-row-gap\", {} },\n        // Outline\n        .{ \"outline\", {} },\n        .{ \"outline-width\", {} },\n        .{ \"outline-offset\", {} },\n        // Multi-column\n        .{ \"column-rule-width\", {} },\n        .{ \"column-width\", {} },\n        // Scroll\n        .{ \"scroll-margin\", {} },\n        .{ \"scroll-margin-top\", {} },\n        .{ \"scroll-margin-right\", {} },\n        .{ \"scroll-margin-bottom\", {} },\n        .{ \"scroll-margin-left\", {} },\n        .{ \"scroll-padding\", {} },\n        .{ \"scroll-padding-top\", {} },\n        .{ \"scroll-padding-right\", {} },\n        .{ \"scroll-padding-bottom\", {} },\n        .{ \"scroll-padding-left\", {} },\n        // Shapes\n        .{ \"shape-margin\", {} },\n        // Motion path\n        .{ \"offset-distance\", {} },\n        // Transforms\n        .{ \"translate\", {} },\n        // Animations\n        .{ \"animation-range-end\", {} },\n        .{ \"animation-range-start\", {} },\n        // Other\n        .{ \"border-spacing\", {} },\n        .{ \"text-shadow\", {} },\n        .{ \"box-shadow\", {} },\n        .{ \"baseline-shift\", {} },\n        .{ \"vertical-align\", {} },\n        .{ \"text-decoration-inset\", {} },\n        .{ \"block-step-size\", {} },\n        // Grid lanes\n        .{ \"flow-tolerance\", {} },\n        .{ \"column-rule-edge-inset\", {} },\n        .{ \"column-rule-interior-inset\", {} },\n        .{ \"row-rule-edge-inset\", {} },\n        .{ \"row-rule-interior-inset\", {} },\n        .{ \"rule-edge-inset\", {} },\n        .{ \"rule-interior-inset\", {} },\n    });\n\n    return length_properties.has(name);\n}\n\nfn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {\n    if (std.mem.eql(u8, normalized_name, \"visibility\")) {\n        return \"visible\";\n    }\n    if (std.mem.eql(u8, normalized_name, \"opacity\")) {\n        return \"1\";\n    }\n    if (std.mem.eql(u8, normalized_name, \"display\")) {\n        const element = self._element orelse return \"\";\n        return getDefaultDisplay(element);\n    }\n    if (std.mem.eql(u8, normalized_name, \"color\")) {\n        const element = self._element orelse return \"\";\n        return getDefaultColor(element);\n    }\n    if (std.mem.eql(u8, normalized_name, \"background-color\")) {\n        // transparent\n        return \"rgba(0, 0, 0, 0)\";\n    }\n\n    return \"\";\n}\n\nfn getDefaultDisplay(element: *const Element) []const u8 {\n    switch (element._type) {\n        .html => |html| {\n            return switch (html._type) {\n                .anchor, .br, .span, .label, .time, .font, .mod, .quote => \"inline\",\n                .body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => \"block\",\n                .generic, .custom, .unknown, .data => blk: {\n                    const tag = element.getTagNameLower();\n                    if (isInlineTag(tag)) break :blk \"inline\";\n                    break :blk \"block\";\n                },\n            };\n        },\n        .svg => return \"inline\",\n    }\n}\n\nfn isInlineTag(tag_name: []const u8) bool {\n    const inline_tags = [_][]const u8{\n        \"abbr\",  \"b\",    \"bdi\",    \"bdo\",  \"cite\", \"code\", \"dfn\",\n        \"em\",    \"i\",    \"kbd\",    \"mark\", \"q\",    \"s\",    \"samp\",\n        \"small\", \"span\", \"strong\", \"sub\",  \"sup\",  \"time\", \"u\",\n        \"var\",   \"wbr\",\n    };\n\n    for (inline_tags) |inline_tag| {\n        if (std.mem.eql(u8, tag_name, inline_tag)) {\n            return true;\n        }\n    }\n    return false;\n}\n\nfn getDefaultColor(element: *const Element) []const u8 {\n    switch (element._type) {\n        .html => |html| {\n            return switch (html._type) {\n                .anchor => \"rgb(0, 0, 238)\", // blue\n                else => \"rgb(0, 0, 0)\",\n            };\n        },\n        .svg => return \"rgb(0, 0, 0)\",\n    }\n}\n\npub const Property = struct {\n    _name: String,\n    _value: String,\n    _important: bool = false,\n    _node: std.DoublyLinkedList.Node,\n\n    fn fromNodeLink(n: *std.DoublyLinkedList.Node) *Property {\n        return @alignCast(@fieldParentPtr(\"_node\", n));\n    }\n\n    pub fn format(self: *const Property, writer: *std.Io.Writer) !void {\n        try self._name.format(writer);\n        try writer.writeAll(\": \");\n        try self._value.format(writer);\n\n        if (self._important) {\n            try writer.writeAll(\" !important\");\n        }\n        try writer.writeByte(';');\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSSStyleDeclaration);\n\n    pub const Meta = struct {\n        pub const name = \"CSSStyleDeclaration\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const cssText = bridge.accessor(CSSStyleDeclaration.getCssText, CSSStyleDeclaration.setCssText, .{});\n    pub const length = bridge.accessor(CSSStyleDeclaration.length, null, .{});\n    pub const item = bridge.function(_item, .{});\n\n    fn _item(self: *const CSSStyleDeclaration, index: i32) []const u8 {\n        if (index < 0) {\n            return \"\";\n        }\n        return self.item(@intCast(index));\n    }\n\n    pub const getPropertyValue = bridge.function(CSSStyleDeclaration.getPropertyValue, .{});\n    pub const getPropertyPriority = bridge.function(CSSStyleDeclaration.getPropertyPriority, .{});\n    pub const setProperty = bridge.function(CSSStyleDeclaration.setProperty, .{});\n    pub const removeProperty = bridge.function(CSSStyleDeclaration.removeProperty, .{});\n    pub const cssFloat = bridge.accessor(CSSStyleDeclaration.getFloat, CSSStyleDeclaration.setFloat, .{});\n};\n\nconst testing = @import(\"std\").testing;\n\ntest \"normalizePropertyValue: unitless zero to 0px\" {\n    const cases = .{\n        .{ \"width\", \"0\", \"0px\" },\n        .{ \"height\", \"0\", \"0px\" },\n        .{ \"scroll-margin-top\", \"0\", \"0px\" },\n        .{ \"scroll-padding-bottom\", \"0\", \"0px\" },\n        .{ \"column-width\", \"0\", \"0px\" },\n        .{ \"column-rule-width\", \"0\", \"0px\" },\n        .{ \"outline\", \"0\", \"0px\" },\n        .{ \"shape-margin\", \"0\", \"0px\" },\n        .{ \"offset-distance\", \"0\", \"0px\" },\n        .{ \"translate\", \"0\", \"0px\" },\n        .{ \"grid-column-gap\", \"0\", \"0px\" },\n        .{ \"grid-row-gap\", \"0\", \"0px\" },\n        // Non-length properties should NOT normalize\n        .{ \"opacity\", \"0\", \"0\" },\n        .{ \"z-index\", \"0\", \"0\" },\n    };\n    inline for (cases) |case| {\n        const result = try normalizePropertyValue(testing.allocator, case[0], case[1]);\n        try testing.expectEqualStrings(case[2], result);\n    }\n}\n\ntest \"normalizePropertyValue: first baseline to baseline\" {\n    const result = try normalizePropertyValue(testing.allocator, \"align-items\", \"first baseline\");\n    try testing.expectEqualStrings(\"baseline\", result);\n\n    const result2 = try normalizePropertyValue(testing.allocator, \"align-self\", \"last baseline\");\n    try testing.expectEqualStrings(\"last baseline\", result2);\n}\n\ntest \"normalizePropertyValue: collapse duplicate two-value shorthands\" {\n    const cases = .{\n        .{ \"overflow\", \"hidden hidden\", \"hidden\" },\n        .{ \"gap\", \"10px 10px\", \"10px\" },\n        .{ \"scroll-snap-align\", \"start start\", \"start\" },\n        .{ \"scroll-padding-block\", \"5px 5px\", \"5px\" },\n        .{ \"background-size\", \"auto auto\", \"auto\" },\n        .{ \"overscroll-behavior\", \"auto auto\", \"auto\" },\n        // Different values should NOT collapse\n        .{ \"overflow\", \"hidden scroll\", \"hidden scroll\" },\n        .{ \"gap\", \"10px 20px\", \"10px 20px\" },\n    };\n    inline for (cases) |case| {\n        const result = try normalizePropertyValue(testing.allocator, case[0], case[1]);\n        try testing.expectEqualStrings(case[2], result);\n    }\n}\n"
  },
  {
    "path": "src/browser/webapi/css/CSSStyleProperties.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Element = @import(\"../Element.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst CSSStyleDeclaration = @import(\"CSSStyleDeclaration.zig\");\n\nconst CSSStyleProperties = @This();\n\n_proto: *CSSStyleDeclaration,\n\npub fn init(element: ?*Element, is_computed: bool, page: *Page) !*CSSStyleProperties {\n    return page._factory.create(CSSStyleProperties{\n        ._proto = try CSSStyleDeclaration.init(element, is_computed, page),\n    });\n}\n\npub fn asCSSStyleDeclaration(self: *CSSStyleProperties) *CSSStyleDeclaration {\n    return self._proto;\n}\n\npub fn setNamed(self: *CSSStyleProperties, name: []const u8, value: []const u8, page: *Page) !void {\n    if (method_names.has(name)) {\n        return error.NotHandled;\n    }\n    const dash_case = camelCaseToDashCase(name, &page.buf);\n    try self._proto.setProperty(dash_case, value, null, page);\n}\n\npub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {\n    if (method_names.has(name)) {\n        return error.NotHandled;\n    }\n\n    const dash_case = camelCaseToDashCase(name, &page.buf);\n\n    // Only apply vendor prefix filtering for camelCase access (no dashes in input)\n    // Bracket notation with dash-case (e.g., div.style['-moz-user-select']) should return the actual value\n    const is_camelcase_access = std.mem.indexOfScalar(u8, name, '-') == null;\n    if (is_camelcase_access and std.mem.startsWith(u8, dash_case, \"-\")) {\n        // We only support -webkit-, other vendor prefixes return undefined for camelCase access\n        const is_webkit = std.mem.startsWith(u8, dash_case, \"-webkit-\");\n        const is_moz = std.mem.startsWith(u8, dash_case, \"-moz-\");\n        const is_ms = std.mem.startsWith(u8, dash_case, \"-ms-\");\n        const is_o = std.mem.startsWith(u8, dash_case, \"-o-\");\n\n        if ((is_moz or is_ms or is_o) and !is_webkit) {\n            return error.NotHandled;\n        }\n    }\n\n    const value = self._proto.getPropertyValue(dash_case, page);\n\n    // Property accessors have special handling for empty values:\n    // - Known CSS properties return '' when not set\n    // - Vendor-prefixed properties return undefined when not set\n    // - Unknown properties return undefined\n    if (value.len == 0) {\n        // Vendor-prefixed properties always return undefined when not set\n        if (std.mem.startsWith(u8, dash_case, \"-\")) {\n            return error.NotHandled;\n        }\n\n        // Known CSS properties return '', unknown properties return undefined\n        if (!isKnownCSSProperty(dash_case)) {\n            return error.NotHandled;\n        }\n\n        return \"\";\n    }\n\n    return value;\n}\n\nfn isKnownCSSProperty(dash_case: []const u8) bool {\n    const known_properties = std.StaticStringMap(void).initComptime(.{\n        // Colors & backgrounds\n        .{ \"color\", {} },\n        .{ \"background\", {} },\n        .{ \"background-color\", {} },\n        .{ \"background-image\", {} },\n        .{ \"background-position\", {} },\n        .{ \"background-repeat\", {} },\n        .{ \"background-size\", {} },\n        .{ \"background-attachment\", {} },\n        .{ \"background-clip\", {} },\n        .{ \"background-origin\", {} },\n        // Typography\n        .{ \"font\", {} },\n        .{ \"font-family\", {} },\n        .{ \"font-size\", {} },\n        .{ \"font-style\", {} },\n        .{ \"font-weight\", {} },\n        .{ \"font-variant\", {} },\n        .{ \"line-height\", {} },\n        .{ \"letter-spacing\", {} },\n        .{ \"word-spacing\", {} },\n        .{ \"text-align\", {} },\n        .{ \"text-decoration\", {} },\n        .{ \"text-indent\", {} },\n        .{ \"text-transform\", {} },\n        .{ \"white-space\", {} },\n        .{ \"word-break\", {} },\n        .{ \"word-wrap\", {} },\n        .{ \"overflow-wrap\", {} },\n        // Box model\n        .{ \"margin\", {} },\n        .{ \"margin-top\", {} },\n        .{ \"margin-right\", {} },\n        .{ \"margin-bottom\", {} },\n        .{ \"margin-left\", {} },\n        .{ \"margin-block\", {} },\n        .{ \"margin-block-start\", {} },\n        .{ \"margin-block-end\", {} },\n        .{ \"margin-inline\", {} },\n        .{ \"margin-inline-start\", {} },\n        .{ \"margin-inline-end\", {} },\n        .{ \"padding\", {} },\n        .{ \"padding-top\", {} },\n        .{ \"padding-right\", {} },\n        .{ \"padding-bottom\", {} },\n        .{ \"padding-left\", {} },\n        .{ \"padding-block\", {} },\n        .{ \"padding-block-start\", {} },\n        .{ \"padding-block-end\", {} },\n        .{ \"padding-inline\", {} },\n        .{ \"padding-inline-start\", {} },\n        .{ \"padding-inline-end\", {} },\n        // Border\n        .{ \"border\", {} },\n        .{ \"border-width\", {} },\n        .{ \"border-style\", {} },\n        .{ \"border-color\", {} },\n        .{ \"border-top\", {} },\n        .{ \"border-top-width\", {} },\n        .{ \"border-top-style\", {} },\n        .{ \"border-top-color\", {} },\n        .{ \"border-right\", {} },\n        .{ \"border-right-width\", {} },\n        .{ \"border-right-style\", {} },\n        .{ \"border-right-color\", {} },\n        .{ \"border-bottom\", {} },\n        .{ \"border-bottom-width\", {} },\n        .{ \"border-bottom-style\", {} },\n        .{ \"border-bottom-color\", {} },\n        .{ \"border-left\", {} },\n        .{ \"border-left-width\", {} },\n        .{ \"border-left-style\", {} },\n        .{ \"border-left-color\", {} },\n        .{ \"border-radius\", {} },\n        .{ \"border-top-left-radius\", {} },\n        .{ \"border-top-right-radius\", {} },\n        .{ \"border-bottom-left-radius\", {} },\n        .{ \"border-bottom-right-radius\", {} },\n        .{ \"border-collapse\", {} },\n        .{ \"border-spacing\", {} },\n        // Sizing\n        .{ \"width\", {} },\n        .{ \"height\", {} },\n        .{ \"min-width\", {} },\n        .{ \"min-height\", {} },\n        .{ \"max-width\", {} },\n        .{ \"max-height\", {} },\n        .{ \"box-sizing\", {} },\n        // Positioning\n        .{ \"position\", {} },\n        .{ \"top\", {} },\n        .{ \"right\", {} },\n        .{ \"bottom\", {} },\n        .{ \"left\", {} },\n        .{ \"inset\", {} },\n        .{ \"inset-block\", {} },\n        .{ \"inset-block-start\", {} },\n        .{ \"inset-block-end\", {} },\n        .{ \"inset-inline\", {} },\n        .{ \"inset-inline-start\", {} },\n        .{ \"inset-inline-end\", {} },\n        .{ \"z-index\", {} },\n        .{ \"float\", {} },\n        .{ \"clear\", {} },\n        // Display & visibility\n        .{ \"display\", {} },\n        .{ \"visibility\", {} },\n        .{ \"opacity\", {} },\n        .{ \"overflow\", {} },\n        .{ \"overflow-x\", {} },\n        .{ \"overflow-y\", {} },\n        .{ \"clip\", {} },\n        .{ \"clip-path\", {} },\n        // Flexbox\n        .{ \"flex\", {} },\n        .{ \"flex-direction\", {} },\n        .{ \"flex-wrap\", {} },\n        .{ \"flex-flow\", {} },\n        .{ \"flex-grow\", {} },\n        .{ \"flex-shrink\", {} },\n        .{ \"flex-basis\", {} },\n        .{ \"order\", {} },\n        // Grid\n        .{ \"grid\", {} },\n        .{ \"grid-template\", {} },\n        .{ \"grid-template-columns\", {} },\n        .{ \"grid-template-rows\", {} },\n        .{ \"grid-template-areas\", {} },\n        .{ \"grid-auto-columns\", {} },\n        .{ \"grid-auto-rows\", {} },\n        .{ \"grid-auto-flow\", {} },\n        .{ \"grid-column\", {} },\n        .{ \"grid-column-start\", {} },\n        .{ \"grid-column-end\", {} },\n        .{ \"grid-row\", {} },\n        .{ \"grid-row-start\", {} },\n        .{ \"grid-row-end\", {} },\n        .{ \"grid-area\", {} },\n        .{ \"gap\", {} },\n        .{ \"row-gap\", {} },\n        .{ \"column-gap\", {} },\n        // Alignment (flexbox & grid)\n        .{ \"align-content\", {} },\n        .{ \"align-items\", {} },\n        .{ \"align-self\", {} },\n        .{ \"justify-content\", {} },\n        .{ \"justify-items\", {} },\n        .{ \"justify-self\", {} },\n        .{ \"place-content\", {} },\n        .{ \"place-items\", {} },\n        .{ \"place-self\", {} },\n        // Transforms & animations\n        .{ \"transform\", {} },\n        .{ \"transform-origin\", {} },\n        .{ \"transform-style\", {} },\n        .{ \"perspective\", {} },\n        .{ \"perspective-origin\", {} },\n        .{ \"transition\", {} },\n        .{ \"transition-property\", {} },\n        .{ \"transition-duration\", {} },\n        .{ \"transition-timing-function\", {} },\n        .{ \"transition-delay\", {} },\n        .{ \"animation\", {} },\n        .{ \"animation-name\", {} },\n        .{ \"animation-duration\", {} },\n        .{ \"animation-timing-function\", {} },\n        .{ \"animation-delay\", {} },\n        .{ \"animation-iteration-count\", {} },\n        .{ \"animation-direction\", {} },\n        .{ \"animation-fill-mode\", {} },\n        .{ \"animation-play-state\", {} },\n        // Filters & effects\n        .{ \"filter\", {} },\n        .{ \"backdrop-filter\", {} },\n        .{ \"box-shadow\", {} },\n        .{ \"text-shadow\", {} },\n        // Outline\n        .{ \"outline\", {} },\n        .{ \"outline-width\", {} },\n        .{ \"outline-style\", {} },\n        .{ \"outline-color\", {} },\n        .{ \"outline-offset\", {} },\n        // Lists\n        .{ \"list-style\", {} },\n        .{ \"list-style-type\", {} },\n        .{ \"list-style-position\", {} },\n        .{ \"list-style-image\", {} },\n        // Tables\n        .{ \"table-layout\", {} },\n        .{ \"caption-side\", {} },\n        .{ \"empty-cells\", {} },\n        // Misc\n        .{ \"cursor\", {} },\n        .{ \"pointer-events\", {} },\n        .{ \"user-select\", {} },\n        .{ \"resize\", {} },\n        .{ \"object-fit\", {} },\n        .{ \"object-position\", {} },\n        .{ \"vertical-align\", {} },\n        .{ \"content\", {} },\n        .{ \"quotes\", {} },\n        .{ \"counter-reset\", {} },\n        .{ \"counter-increment\", {} },\n        // Scrolling\n        .{ \"scroll-behavior\", {} },\n        .{ \"scroll-margin\", {} },\n        .{ \"scroll-padding\", {} },\n        .{ \"overscroll-behavior\", {} },\n        .{ \"overscroll-behavior-x\", {} },\n        .{ \"overscroll-behavior-y\", {} },\n        // Containment\n        .{ \"contain\", {} },\n        .{ \"container\", {} },\n        .{ \"container-type\", {} },\n        .{ \"container-name\", {} },\n        // Aspect ratio\n        .{ \"aspect-ratio\", {} },\n    });\n\n    return known_properties.has(dash_case);\n}\n\nfn camelCaseToDashCase(name: []const u8, buf: []u8) []const u8 {\n    if (name.len == 0) {\n        return name;\n    }\n\n    // Special case: cssFloat -> float\n    const lower_name = std.ascii.lowerString(buf, name);\n    if (std.mem.eql(u8, lower_name, \"cssfloat\")) {\n        return \"float\";\n    }\n\n    // If already contains dashes, just return lowercased\n    if (std.mem.indexOfScalar(u8, name, '-')) |_| {\n        return lower_name;\n    }\n\n    // Check if this looks like proper camelCase (starts with lowercase)\n    // If not (e.g. \"COLOR\", \"BackgroundColor\"), just lowercase it\n    if (name.len == 0 or !std.ascii.isLower(name[0])) {\n        return lower_name;\n    }\n\n    // Check for vendor prefixes: webkitTransform -> -webkit-transform\n    // Must have uppercase letter after the prefix\n    const has_vendor_prefix = blk: {\n        if (name.len > 6 and std.mem.startsWith(u8, name, \"webkit\") and std.ascii.isUpper(name[6])) break :blk true;\n        if (name.len > 3 and std.mem.startsWith(u8, name, \"moz\") and std.ascii.isUpper(name[3])) break :blk true;\n        if (name.len > 2 and std.mem.startsWith(u8, name, \"ms\") and std.ascii.isUpper(name[2])) break :blk true;\n        if (name.len > 1 and std.mem.startsWith(u8, name, \"o\") and std.ascii.isUpper(name[1])) break :blk true;\n        break :blk false;\n    };\n\n    var write_pos: usize = 0;\n\n    if (has_vendor_prefix) {\n        buf[write_pos] = '-';\n        write_pos += 1;\n    }\n\n    for (name, 0..) |c, i| {\n        if (write_pos >= buf.len) {\n            return lower_name;\n        }\n\n        if (std.ascii.isUpper(c)) {\n            const skip_dash = has_vendor_prefix and i < 10 and write_pos == 1;\n\n            if (i > 0 and !skip_dash) {\n                if (write_pos >= buf.len) break;\n                buf[write_pos] = '-';\n                write_pos += 1;\n            }\n            if (write_pos >= buf.len) break;\n            buf[write_pos] = std.ascii.toLower(c);\n            write_pos += 1;\n        } else {\n            buf[write_pos] = c;\n            write_pos += 1;\n        }\n    }\n\n    return buf[0..write_pos];\n}\n\nconst method_names = std.StaticStringMap(void).initComptime(.{\n    .{ \"getPropertyValue\", {} },\n    .{ \"setProperty\", {} },\n    .{ \"removeProperty\", {} },\n    .{ \"getPropertyPriority\", {} },\n    .{ \"item\", {} },\n    .{ \"cssText\", {} },\n    .{ \"length\", {} },\n});\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSSStyleProperties);\n\n    pub const Meta = struct {\n        pub const name = \"CSSStyleProperties\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const @\"[]\" = bridge.namedIndexed(CSSStyleProperties.getNamed, CSSStyleProperties.setNamed, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/css/CSSStyleRule.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst CSSRule = @import(\"CSSRule.zig\");\nconst CSSStyleDeclaration = @import(\"CSSStyleDeclaration.zig\");\n\nconst CSSStyleRule = @This();\n\n_proto: *CSSRule,\n_selector_text: []const u8 = \"\",\n_style: ?*CSSStyleDeclaration = null,\n\npub fn init(page: *Page) !*CSSStyleRule {\n    const rule = try CSSRule.init(.style, page);\n    return page._factory.create(CSSStyleRule{\n        ._proto = rule,\n    });\n}\n\npub fn getSelectorText(self: *const CSSStyleRule) []const u8 {\n    return self._selector_text;\n}\n\npub fn setSelectorText(self: *CSSStyleRule, text: []const u8, page: *Page) !void {\n    self._selector_text = try page.dupeString(text);\n}\n\npub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration {\n    if (self._style) |style| {\n        return style;\n    }\n    const style = try CSSStyleDeclaration.init(null, false, page);\n    self._style = style;\n    return style;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSSStyleRule);\n\n    pub const Meta = struct {\n        pub const name = \"CSSStyleRule\";\n        pub const prototype_chain = bridge.prototypeChain(CSSRule);\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{});\n    pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/css/CSSStyleSheet.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Element = @import(\"../Element.zig\");\nconst CSSRuleList = @import(\"CSSRuleList.zig\");\nconst CSSRule = @import(\"CSSRule.zig\");\n\nconst CSSStyleSheet = @This();\n\n_href: ?[]const u8 = null,\n_title: []const u8 = \"\",\n_disabled: bool = false,\n_css_rules: ?*CSSRuleList = null,\n_owner_rule: ?*CSSRule = null,\n_owner_node: ?*Element = null,\n\npub fn init(page: *Page) !*CSSStyleSheet {\n    return page._factory.create(CSSStyleSheet{});\n}\n\npub fn initWithOwner(owner: *Element, page: *Page) !*CSSStyleSheet {\n    return page._factory.create(CSSStyleSheet{ ._owner_node = owner });\n}\n\npub fn getOwnerNode(self: *const CSSStyleSheet) ?*Element {\n    return self._owner_node;\n}\n\npub fn getHref(self: *const CSSStyleSheet) ?[]const u8 {\n    return self._href;\n}\n\npub fn getTitle(self: *const CSSStyleSheet) []const u8 {\n    return self._title;\n}\n\npub fn getDisabled(self: *const CSSStyleSheet) bool {\n    return self._disabled;\n}\n\npub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void {\n    self._disabled = disabled;\n}\n\npub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList {\n    if (self._css_rules) |rules| return rules;\n    const rules = try CSSRuleList.init(page);\n    self._css_rules = rules;\n    return rules;\n}\n\npub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {\n    return self._owner_rule;\n}\n\npub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 {\n    _ = self;\n    _ = rule;\n    _ = index;\n    _ = page;\n    return 0;\n}\n\npub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {\n    _ = self;\n    _ = index;\n    _ = page;\n}\n\npub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {\n    _ = self;\n    _ = text;\n    // TODO: clear self.css_rules\n    return page.js.local.?.resolvePromise({});\n}\n\npub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {\n    _ = self;\n    _ = text;\n    // TODO: clear self.css_rules\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CSSStyleSheet);\n\n    pub const Meta = struct {\n        pub const name = \"CSSStyleSheet\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(CSSStyleSheet.init, .{});\n    pub const ownerNode = bridge.accessor(CSSStyleSheet.getOwnerNode, null, .{ .null_as_undefined = true });\n    pub const href = bridge.accessor(CSSStyleSheet.getHref, null, .{ .null_as_undefined = true });\n    pub const title = bridge.accessor(CSSStyleSheet.getTitle, null, .{});\n    pub const disabled = bridge.accessor(CSSStyleSheet.getDisabled, CSSStyleSheet.setDisabled, .{});\n    pub const cssRules = bridge.accessor(CSSStyleSheet.getCssRules, null, .{});\n    pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{});\n    pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{});\n    pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{});\n    pub const replace = bridge.function(CSSStyleSheet.replace, .{});\n    pub const replaceSync = bridge.function(CSSStyleSheet.replaceSync, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: CSSStyleSheet\" {\n    try testing.htmlRunner(\"css/stylesheet.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/css/FontFace.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst FontFace = @This();\n\n_arena: Allocator,\n_family: []const u8,\n\npub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace {\n    _ = source;\n\n    const arena = try page.getArena(.{ .debug = \"FontFace\" });\n    errdefer page.releaseArena(arena);\n\n    const self = try arena.create(FontFace);\n    self.* = .{\n        ._arena = arena,\n        ._family = try arena.dupe(u8, family),\n    };\n    return self;\n}\n\npub fn deinit(self: *FontFace, _: bool, session: *Session) void {\n    session.releaseArena(self._arena);\n}\n\npub fn getFamily(self: *const FontFace) []const u8 {\n    return self._family;\n}\n\n// load() - resolves immediately; headless browser has no real font loading.\npub fn load(_: *FontFace, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise({});\n}\n\n// loaded - returns an already-resolved Promise.\npub fn getLoaded(_: *FontFace, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise({});\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FontFace);\n\n    pub const Meta = struct {\n        pub const name = \"FontFace\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(FontFace.deinit);\n    };\n\n    pub const constructor = bridge.constructor(FontFace.init, .{});\n    pub const family = bridge.accessor(FontFace.getFamily, null, .{});\n    pub const status = bridge.property(\"loaded\", .{ .template = false, .readonly = true });\n    pub const style = bridge.property(\"normal\", .{ .template = false, .readonly = true });\n    pub const weight = bridge.property(\"normal\", .{ .template = false, .readonly = true });\n    pub const stretch = bridge.property(\"normal\", .{ .template = false, .readonly = true });\n    pub const unicodeRange = bridge.property(\"U+0-10FFFF\", .{ .template = false, .readonly = true });\n    pub const variant = bridge.property(\"normal\", .{ .template = false, .readonly = true });\n    pub const featureSettings = bridge.property(\"normal\", .{ .template = false, .readonly = true });\n    pub const display = bridge.property(\"auto\", .{ .template = false, .readonly = true });\n    pub const loaded = bridge.accessor(FontFace.getLoaded, null, .{});\n    pub const load = bridge.function(FontFace.load, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: FontFace\" {\n    try testing.htmlRunner(\"css/font_face.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/css/FontFaceSet.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst FontFace = @import(\"FontFace.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\nconst Event = @import(\"../Event.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst FontFaceSet = @This();\n\n_proto: *EventTarget,\n_arena: Allocator,\n\npub fn init(page: *Page) !*FontFaceSet {\n    const arena = try page.getArena(.{ .debug = \"FontFaceSet\" });\n    errdefer page.releaseArena(arena);\n\n    return page._factory.eventTargetWithAllocator(arena, FontFaceSet{\n        ._proto = undefined,\n        ._arena = arena,\n    });\n}\n\npub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void {\n    session.releaseArena(self._arena);\n}\n\npub fn asEventTarget(self: *FontFaceSet) *EventTarget {\n    return self._proto;\n}\n\n// FontFaceSet.ready - returns an already-resolved Promise.\n// In a headless browser there is no font loading, so fonts are always ready.\npub fn getReady(_: *FontFaceSet, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise({});\n}\n\n// check(font, text?) - always true; headless has no real fonts to check.\npub fn check(_: *const FontFaceSet, font: []const u8) bool {\n    _ = font;\n    return true;\n}\n\n// load(font, text?) - resolves immediately with an empty array.\npub fn load(self: *FontFaceSet, font: []const u8, page: *Page) !js.Promise {\n    // TODO parse font to check if the font has been added before dispatching\n    // events.\n    _ = font;\n\n    // Dispatch loading event\n    const target = self.asEventTarget();\n    if (page._event_manager.hasDirectListeners(target, \"loading\", null)) {\n        const event = try Event.initTrusted(comptime .wrap(\"loading\"), .{}, page);\n        try page._event_manager.dispatchDirect(target, event, null, .{ .context = \"load font face set\" });\n    }\n\n    // Dispatch loadingdone event\n    if (page._event_manager.hasDirectListeners(target, \"loadingdone\", null)) {\n        const event = try Event.initTrusted(comptime .wrap(\"loadingdone\"), .{}, page);\n        try page._event_manager.dispatchDirect(target, event, null, .{ .context = \"load font face set\" });\n    }\n\n    return page.js.local.?.resolvePromise({});\n}\n\n// add(fontFace) - no-op; headless browser does not track loaded fonts.\npub fn add(self: *FontFaceSet, _: *FontFace) *FontFaceSet {\n    return self;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FontFaceSet);\n\n    pub const Meta = struct {\n        pub const name = \"FontFaceSet\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(FontFaceSet.deinit);\n    };\n\n    pub const size = bridge.property(0, .{ .template = false, .readonly = true });\n    pub const status = bridge.property(\"loaded\", .{ .template = false, .readonly = true });\n    pub const ready = bridge.accessor(FontFaceSet.getReady, null, .{});\n    pub const check = bridge.function(FontFaceSet.check, .{});\n    pub const load = bridge.function(FontFaceSet.load, .{});\n    pub const add = bridge.function(FontFaceSet.add, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: FontFaceSet\" {\n    try testing.htmlRunner(\"css/font_face_set.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/css/MediaQueryList.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\n// zlint-disable unused-decls\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\n\nconst MediaQueryList = @This();\n\n_proto: *EventTarget,\n_media: []const u8,\n\npub fn deinit(self: *MediaQueryList) void {\n    _ = self;\n}\n\npub fn asEventTarget(self: *MediaQueryList) *EventTarget {\n    return self._proto;\n}\n\npub fn getMedia(self: *const MediaQueryList) []const u8 {\n    return self._media;\n}\n\npub fn addListener(_: *const MediaQueryList, _: js.Function) void {}\npub fn removeListener(_: *const MediaQueryList, _: js.Function) void {}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MediaQueryList);\n\n    pub const Meta = struct {\n        pub const name = \"MediaQueryList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{});\n    pub const matches = bridge.property(false, .{ .template = false, .readonly = true });\n    pub const addListener = bridge.function(MediaQueryList.addListener, .{ .noop = true });\n    pub const removeListener = bridge.function(MediaQueryList.removeListener, .{ .noop = true });\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: MediaQueryList\" {\n    try testing.htmlRunner(\"css/media_query_list.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/css/StyleSheetList.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst CSSStyleSheet = @import(\"CSSStyleSheet.zig\");\n\nconst StyleSheetList = @This();\n\n_sheets: []*CSSStyleSheet = &.{},\n\npub fn init(page: *Page) !*StyleSheetList {\n    return page._factory.create(StyleSheetList{});\n}\n\npub fn length(self: *const StyleSheetList) u32 {\n    return @intCast(self._sheets.len);\n}\n\npub fn item(self: *const StyleSheetList, index: usize) ?*CSSStyleSheet {\n    if (index >= self._sheets.len) return null;\n    return self._sheets[index];\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(StyleSheetList);\n\n    pub const Meta = struct {\n        pub const name = \"StyleSheetList\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const length = bridge.accessor(StyleSheetList.length, null, .{});\n    pub const @\"[]\" = bridge.indexed(StyleSheetList.item, null, .{ .null_as_undefined = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/element/Attribute.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\nconst GenericIterator = @import(\"../collections/iterator.zig\").Entry;\n\nconst Page = @import(\"../../Page.zig\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub fn registerTypes() []const type {\n    return &.{\n        Attribute,\n        NamedNodeMap,\n        NamedNodeMap.Iterator,\n    };\n}\n\npub const Attribute = @This();\n\n_proto: *Node,\n_name: String,\n_value: String,\n_element: ?*Element,\n\npub fn format(self: *const Attribute, writer: *std.Io.Writer) !void {\n    return formatAttribute(self._name, self._value, writer);\n}\n\npub fn getName(self: *const Attribute) String {\n    return self._name;\n}\n\npub fn getValue(self: *const Attribute) String {\n    return self._value;\n}\n\npub fn setValue(self: *Attribute, data_: ?String, page: *Page) !void {\n    const data = data_ orelse String.empty;\n    const el = self._element orelse {\n        self._value = try data.dupe(page.arena);\n        return;\n    };\n    // this takes ownership of the data\n    try el.setAttribute(self._name, data, page);\n\n    // not the most efficient, but we don't expect this to be called often\n    self._value = (try el.getAttribute(self._name, page)) orelse String.empty;\n}\n\npub fn getNamespaceURI(_: *const Attribute) ?[]const u8 {\n    return null;\n}\n\npub fn getOwnerElement(self: *const Attribute) ?*Element {\n    return self._element;\n}\n\npub fn isEqualNode(self: *const Attribute, other: *const Attribute) bool {\n    return self.getName().eql(other.getName()) and self.getValue().eql(other.getValue());\n}\n\npub fn clone(self: *const Attribute, page: *Page) !*Attribute {\n    return page._factory.node(Attribute{\n        ._proto = undefined,\n        ._element = self._element,\n        ._name = self._name,\n        ._value = self._value,\n    });\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Attribute);\n\n    pub const Meta = struct {\n        pub const name = \"Attr\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const enumerable = false;\n    };\n\n    pub const name = bridge.accessor(Attribute.getName, null, .{});\n    pub const localName = bridge.accessor(Attribute.getName, null, .{});\n    pub const value = bridge.accessor(Attribute.getValue, Attribute.setValue, .{});\n    pub const namespaceURI = bridge.accessor(Attribute.getNamespaceURI, null, .{});\n    pub const ownerElement = bridge.accessor(Attribute.getOwnerElement, null, .{});\n};\n\n// This is what an Element references. It isn't exposed to JavaScript. In\n// JavaScript, the element attribute list (el.attributes) is the NamedNodeMap\n// which exposes Attributes. It isn't ideal that we have both.\n// NamedNodeMap and Attribute are relatively fat and awkward to use. You can\n// imagine a page will have tens of thousands of attributes, and it's very likely\n// that page will _never_ load a single Attribute. It might get a string value\n// from a string key, but it won't load the full Attribute. And, even if it does,\n// it will almost certainly load realtively few.\n// The main issue with Attribute is that it's a full Node -> EventTarget. It's\n// _huge_ for something that's essentially just name=>value.\n// That said, we need identity. el.getAttributeNode(\"id\") should return the same\n// Attribute value (the same JSValue) when called multiple time, and that gets\n// more important when you look at the [hardly every used] el.removeAttributeNode\n// and setAttributeNode.\n// So, we maintain a lookup, page._attribute_lookup, to serve as an identity map\n// from our internal Entry to a proper Attribute. This is lazily populated\n// whenever an Attribute is created. Why not just have an ?*Attribute field\n// in our Entry? Because that would require an extra 8 bytes for every single\n// attribute in the DOM, and, again, we expect that to almost always be null.\npub const List = struct {\n    normalize: bool,\n    /// Length of items in `_list`. Not usize to increase memory usage.\n    /// Honestly, this is more than enough.\n    _len: u32 = 0,\n    _list: std.DoublyLinkedList = .{},\n\n    pub fn isEmpty(self: *const List) bool {\n        return self._list.first == null;\n    }\n\n    pub fn get(self: *const List, name: String, page: *Page) !?String {\n        const entry = (try self.getEntry(name, page)) orelse return null;\n        return entry._value;\n    }\n\n    pub inline fn length(self: *const List) usize {\n        return self._len;\n    }\n\n    /// Compares 2 attribute lists for equality.\n    pub fn eql(self: *List, other: *List) bool {\n        if (self.length() != other.length()) {\n            return false;\n        }\n\n        var iter = self.iterator();\n        search: while (iter.next()) |attr| {\n            // Iterate over all `other` attributes.\n            var other_iter = other.iterator();\n            while (other_iter.next()) |other_attr| {\n                if (attr.eql(other_attr)) {\n                    continue :search; // Found match.\n                }\n            }\n            // Iterated over all `other` and not match.\n            return false;\n        }\n        return true;\n    }\n\n    // meant for internal usage, where the name is known to be properly cased\n    pub fn getSafe(self: *const List, name: String) ?[]const u8 {\n        const entry = self.getEntryWithNormalizedName(name) orelse return null;\n        return entry._value.str();\n    }\n\n    // meant for internal usage, where the name is known to be properly cased\n    pub fn hasSafe(self: *const List, name: String) bool {\n        return self.getEntryWithNormalizedName(name) != null;\n    }\n\n    pub fn getAttribute(self: *const List, name: String, element: ?*Element, page: *Page) !?*Attribute {\n        const entry = (try self.getEntry(name, page)) orelse return null;\n        const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry));\n        if (gop.found_existing) {\n            return gop.value_ptr.*;\n        }\n        const attribute = try entry.toAttribute(element, page);\n        gop.value_ptr.* = attribute;\n        return attribute;\n    }\n\n    pub fn put(self: *List, name: String, value: String, element: *Element, page: *Page) !*Entry {\n        const result = try self.getEntryAndNormalizedName(name, page);\n        return self._put(result, value, element, page);\n    }\n\n    pub fn putSafe(self: *List, name: String, value: String, element: *Element, page: *Page) !*Entry {\n        const entry = self.getEntryWithNormalizedName(name);\n        return self._put(.{ .entry = entry, .normalized = name }, value, element, page);\n    }\n\n    fn _put(self: *List, result: NormalizeAndEntry, value: String, element: *Element, page: *Page) !*Entry {\n        const is_id = shouldAddToIdMap(result.normalized, element);\n\n        var entry: *Entry = undefined;\n        var old_value: ?String = null;\n        if (result.entry) |e| {\n            old_value = try e._value.dupe(page.call_arena);\n            if (is_id) {\n                page.removeElementId(element, e._value.str());\n            }\n            e._value = try value.dupe(page.arena);\n            entry = e;\n        } else {\n            entry = try page._factory.create(Entry{\n                ._node = .{},\n                ._name = try result.normalized.dupe(page.arena),\n                ._value = try value.dupe(page.arena),\n            });\n            self._list.append(&entry._node);\n            self._len += 1;\n        }\n\n        if (is_id) {\n            const parent = element.asNode()._parent orelse {\n                return entry;\n            };\n            try page.addElementId(parent, element, entry._value.str());\n        }\n        page.domChanged();\n        page.attributeChange(element, result.normalized, entry._value, old_value);\n        return entry;\n    }\n\n    // Optimized for cloning. We know `name` is already normalized. We know there isn't duplicates.\n    // We know the Element is detatched (and thus, don't need to check for `id`).\n    pub fn putForCloned(self: *List, name: []const u8, value: []const u8, page: *Page) !void {\n        const entry = try page._factory.create(Entry{\n            ._node = .{},\n            ._name = try String.init(page.arena, name, .{}),\n            ._value = try String.init(page.arena, value, .{}),\n        });\n        self._list.append(&entry._node);\n        self._len += 1;\n    }\n\n    // not efficient, won't be called often (if ever!)\n    pub fn putAttribute(self: *List, attribute: *Attribute, element: *Element, page: *Page) !?*Attribute {\n        // we expect our caller to make sure this is true\n        if (comptime IS_DEBUG) {\n            std.debug.assert(attribute._element == null);\n        }\n\n        const existing_attribute = try self.getAttribute(attribute._name, element, page);\n        if (existing_attribute) |ea| {\n            try self.delete(ea._name, element, page);\n        }\n\n        const entry = try self.put(attribute._name, attribute._value, element, page);\n        attribute._element = element;\n        try page._attribute_lookup.put(page.arena, @intFromPtr(entry), attribute);\n        return existing_attribute;\n    }\n\n    // called form our parser, names already lower-cased\n    pub fn putNew(self: *List, name: []const u8, value: []const u8, page: *Page) !void {\n        if (try self.getEntry(.wrap(name), page) != null) {\n            // When parsing, if there are dupicate names, it isn't valid, and\n            // the first is kept\n            return;\n        }\n\n        const entry = try page._factory.create(Entry{\n            ._node = .{},\n            ._name = try String.init(page.arena, name, .{}),\n            ._value = try String.init(page.arena, value, .{}),\n        });\n        self._list.append(&entry._node);\n        self._len += 1;\n    }\n\n    pub fn delete(self: *List, name: String, element: *Element, page: *Page) !void {\n        const result = try self.getEntryAndNormalizedName(name, page);\n        const entry = result.entry orelse return;\n\n        const is_id = shouldAddToIdMap(result.normalized, element);\n        const old_value = entry._value;\n\n        if (is_id) {\n            page.removeElementId(element, entry._value.str());\n        }\n\n        page.domChanged();\n        page.attributeRemove(element, result.normalized, old_value);\n        _ = page._attribute_lookup.remove(@intFromPtr(entry));\n        self._list.remove(&entry._node);\n        self._len -= 1;\n        page._factory.destroy(entry);\n    }\n\n    pub fn getNames(self: *const List, page: *Page) ![][]const u8 {\n        var arr: std.ArrayList([]const u8) = .empty;\n        var node = self._list.first;\n        while (node) |n| {\n            try arr.append(page.call_arena, Entry.fromNode(n)._name.str());\n            node = n.next;\n        }\n        return arr.items;\n    }\n\n    pub fn iterator(self: *List) InnerIterator {\n        return .{ ._node = self._list.first };\n    }\n\n    fn getEntry(self: *const List, name: String, page: *Page) !?*Entry {\n        const result = try self.getEntryAndNormalizedName(name, page);\n        return result.entry;\n    }\n\n    // Dangerous, the returned normalized name is only valid until someone\n    // else uses pages.buf.\n    const NormalizeAndEntry = struct {\n        entry: ?*Entry,\n        normalized: String,\n    };\n    fn getEntryAndNormalizedName(self: *const List, name: String, page: *Page) !NormalizeAndEntry {\n        const normalized =\n            if (self.normalize) try normalizeNameForLookup(name, page) else name;\n\n        return .{\n            .normalized = normalized,\n            .entry = self.getEntryWithNormalizedName(normalized),\n        };\n    }\n\n    fn getEntryWithNormalizedName(self: *const List, name: String) ?*Entry {\n        var node = self._list.first;\n        while (node) |n| {\n            var e = Entry.fromNode(n);\n            if (e._name.eql(name)) {\n                return e;\n            }\n            node = n.next;\n        }\n        return null;\n    }\n\n    pub const Entry = struct {\n        _name: String,\n        _value: String,\n        _node: std.DoublyLinkedList.Node,\n\n        fn fromNode(n: *std.DoublyLinkedList.Node) *Entry {\n            return @alignCast(@fieldParentPtr(\"_node\", n));\n        }\n\n        /// Returns true if 2 entries are equal.\n        /// This doesn't compare `_node` fields.\n        pub fn eql(self: *const Entry, other: *const Entry) bool {\n            return self._name.eql(other._name) and self._value.eql(other._value);\n        }\n\n        pub fn format(self: *const Entry, writer: *std.Io.Writer) !void {\n            return formatAttribute(self._name, self._value, writer);\n        }\n\n        pub fn toAttribute(self: *const Entry, element: ?*Element, page: *Page) !*Attribute {\n            return page._factory.node(Attribute{\n                ._proto = undefined,\n                ._element = element,\n                // Cannot directly reference self._name.str() and self._value.str()\n                // This attribute can outlive the list entry (the node can be\n                // removed from the element's attribute, but still exist in the DOM)\n                ._name = try self._name.dupe(page.arena),\n                ._value = try self._value.dupe(page.arena),\n            });\n        }\n    };\n};\n\nfn shouldAddToIdMap(normalized_name: String, element: *Element) bool {\n    if (!normalized_name.eql(comptime .wrap(\"id\"))) {\n        return false;\n    }\n\n    const node = element.asNode();\n    // Shadow tree elements are always added to their shadow root's map\n    if (node.isInShadowTree()) {\n        return true;\n    }\n    // Document tree elements only when connected\n    return node.isConnected();\n}\n\npub fn validateAttributeName(name: String) !void {\n    const name_str = name.str();\n\n    if (name_str.len == 0) {\n        return error.InvalidCharacterError;\n    }\n\n    const first = name_str[0];\n    if ((first >= '0' and first <= '9') or first == '-' or first == '.') {\n        return error.InvalidCharacterError;\n    }\n\n    for (name_str) |c| {\n        if (c == 0 or c == '/' or c == '=' or c == '>' or std.ascii.isWhitespace(c)) {\n            return error.InvalidCharacterError;\n        }\n\n        const is_valid = (c >= 'a' and c <= 'z') or\n            (c >= 'A' and c <= 'Z') or\n            (c >= '0' and c <= '9') or\n            c == '_' or c == '-' or c == '.' or c == ':';\n\n        if (!is_valid) {\n            return error.InvalidCharacterError;\n        }\n    }\n}\n\npub fn normalizeNameForLookup(name: String, page: *Page) !String {\n    if (!needsLowerCasing(name.str())) {\n        return name;\n    }\n    const normalized = if (name.len < page.buf.len)\n        std.ascii.lowerString(&page.buf, name.str())\n    else\n        try std.ascii.allocLowerString(page.call_arena, name.str());\n\n    return .wrap(normalized);\n}\n\nfn needsLowerCasing(name: []const u8) bool {\n    var remaining = name;\n    if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {\n        while (remaining.len > vector_len) {\n            const chunk: @Vector(vector_len, u8) = remaining[0..vector_len].*;\n            if (@reduce(.Min, chunk) <= 'Z') {\n                return true;\n            }\n            remaining = remaining[vector_len..];\n        }\n    }\n\n    for (remaining) |b| {\n        if (std.ascii.isUpper(b)) {\n            return true;\n        }\n    }\n    return false;\n}\n\npub const NamedNodeMap = struct {\n    _list: *List,\n\n    // Whenever the NamedNodeMap creates an Attribute, it needs to provide the\n    // \"ownerElement\".\n    _element: *Element,\n\n    pub fn length(self: *const NamedNodeMap) u32 {\n        return @intCast(self._list._list.len());\n    }\n\n    pub fn getAtIndex(self: *const NamedNodeMap, index: usize, page: *Page) !?*Attribute {\n        var i: usize = 0;\n        var node = self._list._list.first;\n        while (node) |n| {\n            if (i == index) {\n                var entry = List.Entry.fromNode(n);\n                const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry));\n                if (gop.found_existing) {\n                    return gop.value_ptr.*;\n                }\n                const attribute = try entry.toAttribute(self._element, page);\n                gop.value_ptr.* = attribute;\n                return attribute;\n            }\n            node = n.next;\n            i += 1;\n        }\n        return null;\n    }\n\n    pub fn getByName(self: *const NamedNodeMap, name: String, page: *Page) !?*Attribute {\n        return self._list.getAttribute(name, self._element, page);\n    }\n\n    pub fn set(self: *const NamedNodeMap, attribute: *Attribute, page: *Page) !?*Attribute {\n        attribute._element = null; // just a requirement of list.putAttribute, it'll re-set it.\n        return self._list.putAttribute(attribute, self._element, page);\n    }\n\n    pub fn removeByName(self: *const NamedNodeMap, name: String, page: *Page) !?*Attribute {\n        // this 2-step process (get then delete) isn't efficient. But we don't\n        // expect this to be called often, and this lets us keep delete straightforward.\n        const attr = (try self.getByName(name, page)) orelse return null;\n        try self._list.delete(name, self._element, page);\n        return attr;\n    }\n\n    pub fn iterator(self: *const NamedNodeMap, page: *Page) !*Iterator {\n        return .init(.{ .list = self }, page);\n    }\n\n    pub const Iterator = GenericIterator(struct {\n        index: usize = 0,\n        list: *const NamedNodeMap,\n\n        pub fn next(self: *@This(), page: *Page) !?*Attribute {\n            const index = self.index;\n            self.index = index + 1;\n            return self.list.getAtIndex(index, page);\n        }\n    }, null);\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(NamedNodeMap);\n\n        pub const Meta = struct {\n            pub const name = \"NamedNodeMap\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n\n        pub const length = bridge.accessor(NamedNodeMap.length, null, .{});\n        pub const @\"[int]\" = bridge.indexed(NamedNodeMap.getAtIndex, null, .{ .null_as_undefined = true });\n        pub const @\"[str]\" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });\n        pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});\n        pub const setNamedItem = bridge.function(NamedNodeMap.set, .{});\n        pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{});\n        pub const item = bridge.function(_item, .{});\n        fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute {\n            // the bridge.indexed handles this, so if we want\n            //   list.item(-2) to return the same as list[-2] we need to\n            // 1 - take an i32 for the index\n            // 2 - return null if it's < 0\n            if (index < 0) {\n                return null;\n            }\n            return self.getAtIndex(@intCast(index), page);\n        }\n        pub const symbol_iterator = bridge.iterator(NamedNodeMap.iterator, .{});\n    };\n};\n\n// Not meant to be exposed. The \"public\" iterator is a NamedNodeMap, and it's a\n// bit awkward. Having this for more straightforward key=>value is useful for\n// the few internal places we need to iterate through the attributes (e.g. dump)\npub const InnerIterator = struct {\n    _node: ?*std.DoublyLinkedList.Node = null,\n\n    pub fn next(self: *InnerIterator) ?*List.Entry {\n        const node = self._node orelse return null;\n        self._node = node.next;\n        return List.Entry.fromNode(node);\n    }\n};\n\nfn formatAttribute(name: String, value_: String, writer: *std.Io.Writer) !void {\n    try writer.writeAll(name.str());\n\n    // Boolean attributes with empty values are serialized without a value\n\n    const value = value_.str();\n    if (value.len == 0 and boolean_attributes_lookup.has(name.str())) {\n        return;\n    }\n\n    try writer.writeByte('=');\n    if (value.len == 0) {\n        return writer.writeAll(\"\\\"\\\"\");\n    }\n\n    try writer.writeByte('\"');\n    const offset = std.mem.indexOfAny(u8, value, \"`' &\\\"<>=\") orelse {\n        try writer.writeAll(value);\n        return writer.writeByte('\"');\n    };\n\n    try writeEscapedAttributeValue(value, offset, writer);\n    return writer.writeByte('\"');\n}\n\nconst boolean_attributes = [_][]const u8{\n    \"checked\",\n    \"disabled\",\n    \"required\",\n    \"readonly\",\n    \"multiple\",\n    \"selected\",\n    \"autofocus\",\n    \"autoplay\",\n    \"controls\",\n    \"loop\",\n    \"muted\",\n    \"hidden\",\n    \"async\",\n    \"defer\",\n    \"novalidate\",\n    \"formnovalidate\",\n    \"ismap\",\n    \"reversed\",\n    \"default\",\n    \"open\",\n};\n\nconst boolean_attributes_lookup = std.StaticStringMap(void).initComptime(blk: {\n    var entries: [boolean_attributes.len]struct { []const u8, void } = undefined;\n    for (boolean_attributes, 0..) |attr, i| {\n        entries[i] = .{ attr, {} };\n    }\n    break :blk entries;\n});\n\nfn writeEscapedAttributeValue(value: []const u8, first_offset: usize, writer: *std.Io.Writer) !void {\n    // Write everything before the first special character\n    try writer.writeAll(value[0..first_offset]);\n    try writer.writeAll(switch (value[first_offset]) {\n        '&' => \"&amp;\",\n        '\"' => \"&quot;\",\n        '<' => \"&lt;\",\n        '>' => \"&gt;\",\n        '=' => \"=\",\n        ' ' => \" \",\n        '`' => \"`\",\n        '\\'' => \"'\",\n        else => unreachable,\n    });\n\n    var remaining = value[first_offset + 1 ..];\n    while (std.mem.indexOfAny(u8, remaining, \"&\\\"<>\")) |offset| {\n        try writer.writeAll(remaining[0..offset]);\n        try writer.writeAll(switch (remaining[offset]) {\n            '&' => \"&amp;\",\n            '\"' => \"&quot;\",\n            '<' => \"&lt;\",\n            '>' => \"&gt;\",\n            else => unreachable,\n        });\n        remaining = remaining[offset + 1 ..];\n    }\n\n    if (remaining.len > 0) {\n        try writer.writeAll(remaining);\n    }\n}\n"
  },
  {
    "path": "src/browser/webapi/element/DOMStringMap.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Element = @import(\"../Element.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst Allocator = std.mem.Allocator;\n\nconst DOMStringMap = @This();\n\n_element: *Element,\n\nfn getProperty(self: *DOMStringMap, name: String, page: *Page) !?String {\n    const attr_name = try camelToKebab(page.call_arena, name);\n    return try self._element.getAttribute(attr_name, page);\n}\n\nfn setProperty(self: *DOMStringMap, name: String, value: String, page: *Page) !void {\n    const attr_name = try camelToKebab(page.call_arena, name);\n    return self._element.setAttributeSafe(attr_name, value, page);\n}\n\nfn deleteProperty(self: *DOMStringMap, name: String, page: *Page) !void {\n    const attr_name = try camelToKebab(page.call_arena, name);\n    try self._element.removeAttribute(attr_name, page);\n}\n\n// fooBar -> data-foo-bar (with SSO optimization for short strings)\nfn camelToKebab(arena: Allocator, camel: String) !String {\n    const camel_str = camel.str();\n\n    // Calculate output length\n    var output_len: usize = 5; // \"data-\"\n    for (camel_str, 0..) |c, i| {\n        output_len += 1;\n        if (std.ascii.isUpper(c) and i > 0) output_len += 1; // extra char for '-'\n    }\n\n    if (output_len <= 12) {\n        // SSO path - no allocation!\n        var content: [12]u8 = @splat(0);\n        @memcpy(content[0..5], \"data-\");\n        var idx: usize = 5;\n\n        for (camel_str, 0..) |c, i| {\n            if (std.ascii.isUpper(c)) {\n                if (i > 0) {\n                    content[idx] = '-';\n                    idx += 1;\n                }\n                content[idx] = std.ascii.toLower(c);\n            } else {\n                content[idx] = c;\n            }\n            idx += 1;\n        }\n\n        return .{ .len = @intCast(output_len), .payload = .{ .content = content } };\n    }\n\n    // Fallback: allocate for longer strings\n    var result: std.ArrayList(u8) = .empty;\n    try result.ensureTotalCapacity(arena, output_len);\n    result.appendSliceAssumeCapacity(\"data-\");\n\n    for (camel_str, 0..) |c, i| {\n        if (std.ascii.isUpper(c)) {\n            if (i > 0) {\n                result.appendAssumeCapacity('-');\n            }\n            result.appendAssumeCapacity(std.ascii.toLower(c));\n        } else {\n            result.appendAssumeCapacity(c);\n        }\n    }\n\n    return try String.init(arena, result.items, .{});\n}\n\n// data-foo-bar -> fooBar\nfn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 {\n    if (!std.mem.startsWith(u8, kebab, \"data-\")) {\n        return null;\n    }\n\n    const data_part = kebab[5..]; // Skip \"data-\"\n    if (data_part.len == 0) {\n        return null;\n    }\n\n    var result: std.ArrayList(u8) = .empty;\n    try result.ensureTotalCapacity(arena, data_part.len);\n\n    var capitalize_next = false;\n    for (data_part) |c| {\n        if (c == '-') {\n            capitalize_next = true;\n        } else if (capitalize_next) {\n            result.appendAssumeCapacity(std.ascii.toUpper(c));\n            capitalize_next = false;\n        } else {\n            result.appendAssumeCapacity(c);\n        }\n    }\n\n    return result.items;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DOMStringMap);\n\n    pub const Meta = struct {\n        pub const name = \"DOMStringMap\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const @\"[]\" = bridge.namedIndexed(getProperty, setProperty, deleteProperty, .{ .null_as_undefined = true });\n};\n"
  },
  {
    "path": "src/browser/webapi/element/Html.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst reflect = @import(\"../../reflect.zig\");\nconst log = @import(\"../../../log.zig\");\n\nconst global_event_handlers = @import(\"../global_event_handlers.zig\");\nconst GlobalEventHandlersLookup = global_event_handlers.Lookup;\nconst GlobalEventHandler = global_event_handlers.Handler;\n\nconst Page = @import(\"../../Page.zig\");\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\n\npub const Anchor = @import(\"html/Anchor.zig\");\npub const Area = @import(\"html/Area.zig\");\npub const Base = @import(\"html/Base.zig\");\npub const Body = @import(\"html/Body.zig\");\npub const BR = @import(\"html/BR.zig\");\npub const Button = @import(\"html/Button.zig\");\npub const Canvas = @import(\"html/Canvas.zig\");\npub const Custom = @import(\"html/Custom.zig\");\npub const Data = @import(\"html/Data.zig\");\npub const DataList = @import(\"html/DataList.zig\");\npub const Details = @import(\"html/Details.zig\");\npub const Dialog = @import(\"html/Dialog.zig\");\npub const Directory = @import(\"html/Directory.zig\");\npub const Div = @import(\"html/Div.zig\");\npub const DList = @import(\"html/DList.zig\");\npub const Embed = @import(\"html/Embed.zig\");\npub const FieldSet = @import(\"html/FieldSet.zig\");\npub const Font = @import(\"html/Font.zig\");\npub const Form = @import(\"html/Form.zig\");\npub const Generic = @import(\"html/Generic.zig\");\npub const Head = @import(\"html/Head.zig\");\npub const Heading = @import(\"html/Heading.zig\");\npub const HR = @import(\"html/HR.zig\");\npub const Html = @import(\"html/Html.zig\");\npub const IFrame = @import(\"html/IFrame.zig\");\npub const Image = @import(\"html/Image.zig\");\npub const Input = @import(\"html/Input.zig\");\npub const Label = @import(\"html/Label.zig\");\npub const Legend = @import(\"html/Legend.zig\");\npub const LI = @import(\"html/LI.zig\");\npub const Link = @import(\"html/Link.zig\");\npub const Map = @import(\"html/Map.zig\");\npub const Media = @import(\"html/Media.zig\");\npub const Meta = @import(\"html/Meta.zig\");\npub const Meter = @import(\"html/Meter.zig\");\npub const Mod = @import(\"html/Mod.zig\");\npub const Object = @import(\"html/Object.zig\");\npub const OL = @import(\"html/OL.zig\");\npub const OptGroup = @import(\"html/OptGroup.zig\");\npub const Option = @import(\"html/Option.zig\");\npub const Output = @import(\"html/Output.zig\");\npub const Paragraph = @import(\"html/Paragraph.zig\");\npub const Picture = @import(\"html/Picture.zig\");\npub const Param = @import(\"html/Param.zig\");\npub const Pre = @import(\"html/Pre.zig\");\npub const Progress = @import(\"html/Progress.zig\");\npub const Quote = @import(\"html/Quote.zig\");\npub const Script = @import(\"html/Script.zig\");\npub const Select = @import(\"html/Select.zig\");\npub const Slot = @import(\"html/Slot.zig\");\npub const Source = @import(\"html/Source.zig\");\npub const Span = @import(\"html/Span.zig\");\npub const Style = @import(\"html/Style.zig\");\npub const Table = @import(\"html/Table.zig\");\npub const TableCaption = @import(\"html/TableCaption.zig\");\npub const TableCell = @import(\"html/TableCell.zig\");\npub const TableCol = @import(\"html/TableCol.zig\");\npub const TableRow = @import(\"html/TableRow.zig\");\npub const TableSection = @import(\"html/TableSection.zig\");\npub const Template = @import(\"html/Template.zig\");\npub const TextArea = @import(\"html/TextArea.zig\");\npub const Time = @import(\"html/Time.zig\");\npub const Title = @import(\"html/Title.zig\");\npub const Track = @import(\"html/Track.zig\");\npub const UL = @import(\"html/UL.zig\");\npub const Unknown = @import(\"html/Unknown.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst HtmlElement = @This();\n\n_type: Type,\n_proto: *Element,\n\n// Special constructor for custom elements\npub fn construct(page: *Page) !*Element {\n    const node = page._upgrading_element orelse return error.IllegalConstructor;\n    return node.is(Element) orelse return error.IllegalConstructor;\n}\n\npub const Type = union(enum) {\n    anchor: *Anchor,\n    area: *Area,\n    base: *Base,\n    body: *Body,\n    br: *BR,\n    button: *Button,\n    canvas: *Canvas,\n    custom: *Custom,\n    data: *Data,\n    datalist: *DataList,\n    details: *Details,\n    dialog: *Dialog,\n    directory: *Directory,\n    div: *Div,\n    dl: *DList,\n    embed: *Embed,\n    fieldset: *FieldSet,\n    font: *Font,\n    form: *Form,\n    generic: *Generic,\n    heading: *Heading,\n    head: *Head,\n    html: *Html,\n    hr: *HR,\n    img: *Image,\n    iframe: *IFrame,\n    input: *Input,\n    label: *Label,\n    legend: *Legend,\n    li: *LI,\n    link: *Link,\n    map: *Map,\n    media: *Media,\n    meta: *Meta,\n    meter: *Meter,\n    mod: *Mod,\n    object: *Object,\n    ol: *OL,\n    optgroup: *OptGroup,\n    option: *Option,\n    output: *Output,\n    p: *Paragraph,\n    picture: *Picture,\n    param: *Param,\n    pre: *Pre,\n    progress: *Progress,\n    quote: *Quote,\n    script: *Script,\n    select: *Select,\n    slot: *Slot,\n    source: *Source,\n    span: *Span,\n    style: *Style,\n    table: *Table,\n    table_caption: *TableCaption,\n    table_cell: *TableCell,\n    table_col: *TableCol,\n    table_row: *TableRow,\n    table_section: *TableSection,\n    template: *Template,\n    textarea: *TextArea,\n    time: *Time,\n    title: *Title,\n    track: *Track,\n    ul: *UL,\n    unknown: *Unknown,\n};\n\npub fn is(self: *HtmlElement, comptime T: type) ?*T {\n    inline for (@typeInfo(Type).@\"union\".fields) |f| {\n        if (@field(Type, f.name) == self._type) {\n            if (f.type == T) {\n                return &@field(self._type, f.name);\n            }\n            if (f.type == *T) {\n                return @field(self._type, f.name);\n            }\n        }\n    }\n    return null;\n}\n\npub fn asElement(self: *HtmlElement) *Element {\n    return self._proto;\n}\n\npub fn asNode(self: *HtmlElement) *Node {\n    return self._proto._proto;\n}\n\npub fn asEventTarget(self: *HtmlElement) *@import(\"../EventTarget.zig\") {\n    return self._proto._proto._proto;\n}\n\n// innerText represents the **rendered** text content of a node and its\n// descendants.\npub fn getInnerText(self: *HtmlElement, writer: *std.Io.Writer) !void {\n    var state = innerTextState{};\n    return try self._getInnerText(writer, &state);\n}\n\nconst innerTextState = struct {\n    pre_w: bool = false,\n    trim_left: bool = true,\n};\n\nfn _getInnerText(self: *HtmlElement, writer: *std.Io.Writer, state: *innerTextState) !void {\n    var it = self.asElement().asNode().childrenIterator();\n    while (it.next()) |child| {\n        switch (child._type) {\n            .element => |e| switch (e._type) {\n                .html => |he| switch (he._type) {\n                    .br => {\n                        try writer.writeByte('\\n');\n                        state.pre_w = false; // prevent a next pre space.\n                        state.trim_left = true;\n                    },\n                    .script, .style, .template => {\n                        state.pre_w = false; // prevent a next pre space.\n                        state.trim_left = true;\n                    },\n                    else => try he._getInnerText(writer, state), // TODO check if elt is hidden.\n                },\n                .svg => {},\n            },\n            .cdata => |c| switch (c._type) {\n                .comment => {\n                    state.pre_w = false; // prevent a next pre space.\n                    state.trim_left = true;\n                },\n                .text => {\n                    if (state.pre_w) try writer.writeByte(' ');\n                    state.pre_w = try c.render(writer, .{ .trim_left = state.trim_left });\n                    // if we had a pre space, trim left next one.\n                    state.trim_left = state.pre_w;\n                },\n                // CDATA sections should not be used within HTML. They are\n                // considered comments and are not displayed.\n                .cdata_section => {},\n                // Processing instructions are not displayed in innerText\n                .processing_instruction => {},\n            },\n            .document => {},\n            .document_type => {},\n            .document_fragment => {},\n            .attribute => |attr| try writer.writeAll(attr._value.str()),\n        }\n    }\n}\n\npub fn setInnerText(self: *HtmlElement, text: []const u8, page: *Page) !void {\n    const parent = self.asElement().asNode();\n\n    // Remove all existing children\n    page.domChanged();\n    var it = parent.childrenIterator();\n    while (it.next()) |child| {\n        page.removeNode(parent, child, .{ .will_be_reconnected = false });\n    }\n\n    // Fast path: skip if text is empty\n    if (text.len == 0) {\n        return;\n    }\n\n    // Create and append text node\n    const text_node = try page.createTextNode(text);\n    try page.appendNode(parent, text_node, .{ .child_already_connected = false });\n}\n\npub fn insertAdjacentHTML(\n    self: *HtmlElement,\n    position: []const u8,\n    html: []const u8,\n    page: *Page,\n) !void {\n\n    // Create a new HTMLDocument.\n    const doc = try page._factory.document(@import(\"../HTMLDocument.zig\"){\n        ._proto = undefined,\n    });\n    const doc_node = doc.asNode();\n\n    const arena = try page.getArena(.{ .debug = \"HTML.insertAdjacentHTML\" });\n    defer page.releaseArena(arena);\n\n    const Parser = @import(\"../../parser/Parser.zig\");\n    var parser = Parser.init(arena, doc_node, page);\n    parser.parse(html);\n\n    // Check if there's parsing error.\n    if (parser.err) |_| {\n        return error.Invalid;\n    }\n\n    // The parser wraps content in a document structure:\n    // - Typical: <html><head>...</head><body>...</body></html>\n    // - Head-only: <html><head><meta></head></html> (no body)\n    // - Empty/comments: May have no <html> element at all\n    const html_node = doc_node.firstChild() orelse return;\n\n    const target_node, const prev_node = try self.asElement().asNode().findAdjacentNodes(position);\n\n    // Iterate through all children of <html> (typically <head> and/or <body>)\n    // and insert their children (not the containers themselves) into the target.\n    // This handles both body content AND head-only elements like <meta>, <title>, etc.\n    var html_children = html_node.childrenIterator();\n    while (html_children.next()) |container| {\n        var iter = container.childrenIterator();\n        while (iter.next()) |child_node| {\n            _ = try target_node.insertBefore(child_node, prev_node, page);\n        }\n    }\n}\n\npub fn click(self: *HtmlElement, page: *Page) !void {\n    switch (self._type) {\n        inline .button, .input, .textarea, .select => |i| {\n            if (i.getDisabled()) {\n                return;\n            }\n        },\n        else => {},\n    }\n\n    const event = (try @import(\"../event/MouseEvent.zig\").init(\"click\", .{\n        .bubbles = true,\n        .cancelable = true,\n        .composed = true,\n        .clientX = 0,\n        .clientY = 0,\n    }, page)).asEvent();\n    try page._event_manager.dispatch(self.asEventTarget(), event);\n}\n\n// TODO: Per spec, hidden is a tristate: true | false | \"until-found\".\n// We only support boolean for now; \"until-found\" would need bridge union support.\npub fn getHidden(self: *HtmlElement) bool {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"hidden\")) != null;\n}\n\npub fn setHidden(self: *HtmlElement, hidden: bool, page: *Page) !void {\n    if (hidden) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"hidden\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"hidden\"), page);\n    }\n}\n\npub fn getTabIndex(self: *HtmlElement) i32 {\n    const attr = self.asElement().getAttributeSafe(comptime .wrap(\"tabindex\")) orelse {\n        // Per spec, interactive/focusable elements default to 0 when tabindex is absent\n        return switch (self._type) {\n            .anchor, .area, .button, .input, .select, .textarea, .iframe => 0,\n            else => -1,\n        };\n    };\n    return std.fmt.parseInt(i32, attr, 10) catch -1;\n}\n\npub fn setTabIndex(self: *HtmlElement, value: i32, page: *Page) !void {\n    var buf: [12]u8 = undefined;\n    const str = std.fmt.bufPrint(&buf, \"{d}\", .{value}) catch unreachable;\n    try self.asElement().setAttributeSafe(comptime .wrap(\"tabindex\"), .wrap(str), page);\n}\n\npub fn getAttributeFunction(\n    self: *HtmlElement,\n    listener_type: GlobalEventHandler,\n    page: *Page,\n) !?js.Function.Global {\n    const element = self.asElement();\n    if (page._event_target_attr_listeners.get(.{ .target = element.asEventTarget(), .handler = listener_type })) |cached| {\n        return cached;\n    }\n\n    const attr = element.getAttributeSafe(.wrap(@tagName(listener_type))) orelse return null;\n    const function = page.js.stringToPersistedFunction(attr, &.{\"event\"}, &.{}) catch |err| {\n        // Not a valid expression; log this to find out if its something we should be supporting.\n        log.warn(.js, \"Html.getAttributeFunction\", .{\n            .expression = attr,\n            .err = err,\n        });\n        return null;\n    };\n\n    try self.setAttributeListener(listener_type, function, page);\n    return function;\n}\n\npub fn hasAttributeFunction(self: *HtmlElement, listener_type: GlobalEventHandler, page: *const Page) bool {\n    return page._event_target_attr_listeners.contains(.{ .target = self.asEventTarget(), .handler = listener_type });\n}\n\nfn setAttributeListener(\n    self: *Element.Html,\n    listener_type: GlobalEventHandler,\n    listener_callback: ?js.Function.Global,\n    page: *Page,\n) !void {\n    if (comptime IS_DEBUG) {\n        log.debug(.event, \"Html.setAttributeListener\", .{\n            .type = std.meta.activeTag(self._type),\n            .listener_type = listener_type,\n        });\n    }\n\n    if (listener_callback) |cb| {\n        try page._event_target_attr_listeners.put(page.arena, .{\n            .target = self.asEventTarget(),\n            .handler = listener_type,\n        }, cb);\n        return;\n    }\n\n    // The listener is null, remove existing listener.\n    _ = page._event_target_attr_listeners.remove(.{\n        .target = self.asEventTarget(),\n        .handler = listener_type,\n    });\n}\n\npub fn setOnAbort(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onabort, callback, page);\n}\n\npub fn getOnAbort(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onabort, page);\n}\n\npub fn setOnAnimationCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onanimationcancel, callback, page);\n}\n\npub fn getOnAnimationCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onanimationcancel, page);\n}\n\npub fn setOnAnimationEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onanimationend, callback, page);\n}\n\npub fn getOnAnimationEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onanimationend, page);\n}\n\npub fn setOnAnimationIteration(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onanimationiteration, callback, page);\n}\n\npub fn getOnAnimationIteration(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onanimationiteration, page);\n}\n\npub fn setOnAnimationStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onanimationstart, callback, page);\n}\n\npub fn getOnAnimationStart(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onanimationstart, page);\n}\n\npub fn setOnAuxClick(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onauxclick, callback, page);\n}\n\npub fn getOnAuxClick(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onauxclick, page);\n}\n\npub fn setOnBeforeInput(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onbeforeinput, callback, page);\n}\n\npub fn getOnBeforeInput(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onbeforeinput, page);\n}\n\npub fn setOnBeforeMatch(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onbeforematch, callback, page);\n}\n\npub fn getOnBeforeMatch(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onbeforematch, page);\n}\n\npub fn setOnBeforeToggle(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onbeforetoggle, callback, page);\n}\n\npub fn getOnBeforeToggle(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onbeforetoggle, page);\n}\n\npub fn setOnBlur(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onblur, callback, page);\n}\n\npub fn getOnBlur(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onblur, page);\n}\n\npub fn setOnCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncancel, callback, page);\n}\n\npub fn getOnCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncancel, page);\n}\n\npub fn setOnCanPlay(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncanplay, callback, page);\n}\n\npub fn getOnCanPlay(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncanplay, page);\n}\n\npub fn setOnCanPlayThrough(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncanplaythrough, callback, page);\n}\n\npub fn getOnCanPlayThrough(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncanplaythrough, page);\n}\n\npub fn setOnChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onchange, callback, page);\n}\n\npub fn getOnChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onchange, page);\n}\n\npub fn setOnClick(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onclick, callback, page);\n}\n\npub fn getOnClick(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onclick, page);\n}\n\npub fn setOnClose(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onclose, callback, page);\n}\n\npub fn getOnClose(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onclose, page);\n}\n\npub fn setOnCommand(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncommand, callback, page);\n}\n\npub fn getOnCommand(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncommand, page);\n}\n\npub fn setOnContentVisibilityAutoStateChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncontentvisibilityautostatechange, callback, page);\n}\n\npub fn getOnContentVisibilityAutoStateChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncontentvisibilityautostatechange, page);\n}\n\npub fn setOnContextLost(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncontextlost, callback, page);\n}\n\npub fn getOnContextLost(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncontextlost, page);\n}\n\npub fn setOnContextMenu(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncontextmenu, callback, page);\n}\n\npub fn getOnContextMenu(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncontextmenu, page);\n}\n\npub fn setOnContextRestored(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncontextrestored, callback, page);\n}\n\npub fn getOnContextRestored(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncontextrestored, page);\n}\n\npub fn setOnCopy(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncopy, callback, page);\n}\n\npub fn getOnCopy(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncopy, page);\n}\n\npub fn setOnCueChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncuechange, callback, page);\n}\n\npub fn getOnCueChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncuechange, page);\n}\n\npub fn setOnCut(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oncut, callback, page);\n}\n\npub fn getOnCut(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oncut, page);\n}\n\npub fn setOnDblClick(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondblclick, callback, page);\n}\n\npub fn getOnDblClick(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondblclick, page);\n}\n\npub fn setOnDrag(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondrag, callback, page);\n}\n\npub fn getOnDrag(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondrag, page);\n}\n\npub fn setOnDragEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondragend, callback, page);\n}\n\npub fn getOnDragEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondragend, page);\n}\n\npub fn setOnDragEnter(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondragenter, callback, page);\n}\n\npub fn getOnDragEnter(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondragenter, page);\n}\n\npub fn setOnDragExit(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondragexit, callback, page);\n}\n\npub fn getOnDragExit(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondragexit, page);\n}\n\npub fn setOnDragLeave(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondragleave, callback, page);\n}\n\npub fn getOnDragLeave(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondragleave, page);\n}\n\npub fn setOnDragOver(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondragover, callback, page);\n}\n\npub fn getOnDragOver(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondragover, page);\n}\n\npub fn setOnDragStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondragstart, callback, page);\n}\n\npub fn getOnDragStart(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondragstart, page);\n}\n\npub fn setOnDrop(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondrop, callback, page);\n}\n\npub fn getOnDrop(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondrop, page);\n}\n\npub fn setOnDurationChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ondurationchange, callback, page);\n}\n\npub fn getOnDurationChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ondurationchange, page);\n}\n\npub fn setOnEmptied(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onemptied, callback, page);\n}\n\npub fn getOnEmptied(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onemptied, page);\n}\n\npub fn setOnEnded(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onended, callback, page);\n}\n\npub fn getOnEnded(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onended, page);\n}\n\npub fn setOnError(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onerror, callback, page);\n}\n\npub fn getOnError(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onerror, page);\n}\n\npub fn setOnFocus(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onfocus, callback, page);\n}\n\npub fn getOnFocus(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onfocus, page);\n}\n\npub fn setOnFormData(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onformdata, callback, page);\n}\n\npub fn getOnFormData(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onformdata, page);\n}\n\npub fn setOnFullscreenChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onfullscreenchange, callback, page);\n}\n\npub fn getOnFullscreenChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onfullscreenchange, page);\n}\n\npub fn setOnFullscreenError(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onfullscreenerror, callback, page);\n}\n\npub fn getOnFullscreenError(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onfullscreenerror, page);\n}\n\npub fn setOnGotPointerCapture(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ongotpointercapture, callback, page);\n}\n\npub fn getOnGotPointerCapture(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ongotpointercapture, page);\n}\n\npub fn setOnInput(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oninput, callback, page);\n}\n\npub fn getOnInput(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oninput, page);\n}\n\npub fn setOnInvalid(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.oninvalid, callback, page);\n}\n\npub fn getOnInvalid(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.oninvalid, page);\n}\n\npub fn setOnKeyDown(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onkeydown, callback, page);\n}\n\npub fn getOnKeyDown(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onkeydown, page);\n}\n\npub fn setOnKeyPress(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onkeypress, callback, page);\n}\n\npub fn getOnKeyPress(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onkeypress, page);\n}\n\npub fn setOnKeyUp(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onkeyup, callback, page);\n}\n\npub fn getOnKeyUp(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onkeyup, page);\n}\n\npub fn setOnLoad(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onload, callback, page);\n}\n\npub fn getOnLoad(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onload, page);\n}\n\npub fn setOnLoadedData(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onloadeddata, callback, page);\n}\n\npub fn getOnLoadedData(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onloadeddata, page);\n}\n\npub fn setOnLoadedMetadata(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onloadedmetadata, callback, page);\n}\n\npub fn getOnLoadedMetadata(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onloadedmetadata, page);\n}\n\npub fn setOnLoadStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onloadstart, callback, page);\n}\n\npub fn getOnLoadStart(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onloadstart, page);\n}\n\npub fn setOnLostPointerCapture(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onlostpointercapture, callback, page);\n}\n\npub fn getOnLostPointerCapture(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onlostpointercapture, page);\n}\n\npub fn setOnMouseDown(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onmousedown, callback, page);\n}\n\npub fn getOnMouseDown(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onmousedown, page);\n}\n\npub fn setOnMouseMove(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onmousemove, callback, page);\n}\n\npub fn getOnMouseMove(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onmousemove, page);\n}\n\npub fn setOnMouseOut(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onmouseout, callback, page);\n}\n\npub fn getOnMouseOut(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onmouseout, page);\n}\n\npub fn setOnMouseOver(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onmouseover, callback, page);\n}\n\npub fn getOnMouseOver(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onmouseover, page);\n}\n\npub fn setOnMouseUp(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onmouseup, callback, page);\n}\n\npub fn getOnMouseUp(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onmouseup, page);\n}\n\npub fn setOnPaste(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpaste, callback, page);\n}\n\npub fn getOnPaste(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpaste, page);\n}\n\npub fn setOnPause(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpause, callback, page);\n}\n\npub fn getOnPause(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpause, page);\n}\n\npub fn setOnPlay(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onplay, callback, page);\n}\n\npub fn getOnPlay(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onplay, page);\n}\n\npub fn setOnPlaying(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onplaying, callback, page);\n}\n\npub fn getOnPlaying(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onplaying, page);\n}\n\npub fn setOnPointerCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointercancel, callback, page);\n}\n\npub fn getOnPointerCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointercancel, page);\n}\n\npub fn setOnPointerDown(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerdown, callback, page);\n}\n\npub fn getOnPointerDown(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerdown, page);\n}\n\npub fn setOnPointerEnter(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerenter, callback, page);\n}\n\npub fn getOnPointerEnter(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerenter, page);\n}\n\npub fn setOnPointerLeave(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerleave, callback, page);\n}\n\npub fn getOnPointerLeave(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerleave, page);\n}\n\npub fn setOnPointerMove(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointermove, callback, page);\n}\n\npub fn getOnPointerMove(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointermove, page);\n}\n\npub fn setOnPointerOut(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerout, callback, page);\n}\n\npub fn getOnPointerOut(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerout, page);\n}\n\npub fn setOnPointerOver(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerover, callback, page);\n}\n\npub fn getOnPointerOver(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerover, page);\n}\n\npub fn setOnPointerRawUpdate(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerrawupdate, callback, page);\n}\n\npub fn getOnPointerRawUpdate(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerrawupdate, page);\n}\n\npub fn setOnPointerUp(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onpointerup, callback, page);\n}\n\npub fn getOnPointerUp(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onpointerup, page);\n}\n\npub fn setOnProgress(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onprogress, callback, page);\n}\n\npub fn getOnProgress(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onprogress, page);\n}\n\npub fn setOnRateChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onratechange, callback, page);\n}\n\npub fn getOnRateChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onratechange, page);\n}\n\npub fn setOnReset(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onreset, callback, page);\n}\n\npub fn getOnReset(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onreset, page);\n}\n\npub fn setOnResize(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onresize, callback, page);\n}\n\npub fn getOnResize(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onresize, page);\n}\n\npub fn setOnScroll(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onscroll, callback, page);\n}\n\npub fn getOnScroll(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onscroll, page);\n}\n\npub fn setOnScrollEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onscrollend, callback, page);\n}\n\npub fn getOnScrollEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onscrollend, page);\n}\n\npub fn setOnSecurityPolicyViolation(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onsecuritypolicyviolation, callback, page);\n}\n\npub fn getOnSecurityPolicyViolation(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onsecuritypolicyviolation, page);\n}\n\npub fn setOnSeeked(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onseeked, callback, page);\n}\n\npub fn getOnSeeked(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onseeked, page);\n}\n\npub fn setOnSeeking(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onseeking, callback, page);\n}\n\npub fn getOnSeeking(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onseeking, page);\n}\n\npub fn setOnSelect(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onselect, callback, page);\n}\n\npub fn getOnSelect(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onselect, page);\n}\n\npub fn setOnSelectionChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onselectionchange, callback, page);\n}\n\npub fn getOnSelectionChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onselectionchange, page);\n}\n\npub fn setOnSelectStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onselectstart, callback, page);\n}\n\npub fn getOnSelectStart(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onselectstart, page);\n}\n\npub fn setOnSlotChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onslotchange, callback, page);\n}\n\npub fn getOnSlotChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onslotchange, page);\n}\n\npub fn setOnStalled(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onstalled, callback, page);\n}\n\npub fn getOnStalled(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onstalled, page);\n}\n\npub fn setOnSubmit(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onsubmit, callback, page);\n}\n\npub fn getOnSubmit(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onsubmit, page);\n}\n\npub fn setOnSuspend(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onsuspend, callback, page);\n}\n\npub fn getOnSuspend(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onsuspend, page);\n}\n\npub fn setOnTimeUpdate(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ontimeupdate, callback, page);\n}\n\npub fn getOnTimeUpdate(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ontimeupdate, page);\n}\n\npub fn setOnToggle(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ontoggle, callback, page);\n}\n\npub fn getOnToggle(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ontoggle, page);\n}\n\npub fn setOnTransitionCancel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ontransitioncancel, callback, page);\n}\n\npub fn getOnTransitionCancel(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ontransitioncancel, page);\n}\n\npub fn setOnTransitionEnd(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ontransitionend, callback, page);\n}\n\npub fn getOnTransitionEnd(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ontransitionend, page);\n}\n\npub fn setOnTransitionRun(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ontransitionrun, callback, page);\n}\n\npub fn getOnTransitionRun(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ontransitionrun, page);\n}\n\npub fn setOnTransitionStart(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.ontransitionstart, callback, page);\n}\n\npub fn getOnTransitionStart(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.ontransitionstart, page);\n}\n\npub fn setOnVolumeChange(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onvolumechange, callback, page);\n}\n\npub fn getOnVolumeChange(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onvolumechange, page);\n}\n\npub fn setOnWaiting(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onwaiting, callback, page);\n}\n\npub fn getOnWaiting(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onwaiting, page);\n}\n\npub fn setOnWheel(self: *HtmlElement, callback: ?js.Function.Global, page: *Page) !void {\n    return self.setAttributeListener(.onwheel, callback, page);\n}\n\npub fn getOnWheel(self: *HtmlElement, page: *Page) !?js.Function.Global {\n    return self.getAttributeFunction(.onwheel, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HtmlElement);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(HtmlElement.construct, .{});\n\n    pub const innerText = bridge.accessor(_innerText, HtmlElement.setInnerText, .{});\n    fn _innerText(self: *HtmlElement, page: *const Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.getInnerText(&buf.writer);\n        return buf.written();\n    }\n    pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true });\n    pub const click = bridge.function(HtmlElement.click, .{});\n\n    pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});\n    pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});\n\n    pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});\n    pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{});\n    pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{});\n    pub const onanimationiteration = bridge.accessor(HtmlElement.getOnAnimationIteration, HtmlElement.setOnAnimationIteration, .{});\n    pub const onanimationstart = bridge.accessor(HtmlElement.getOnAnimationStart, HtmlElement.setOnAnimationStart, .{});\n    pub const onauxclick = bridge.accessor(HtmlElement.getOnAuxClick, HtmlElement.setOnAuxClick, .{});\n    pub const onbeforeinput = bridge.accessor(HtmlElement.getOnBeforeInput, HtmlElement.setOnBeforeInput, .{});\n    pub const onbeforematch = bridge.accessor(HtmlElement.getOnBeforeMatch, HtmlElement.setOnBeforeMatch, .{});\n    pub const onbeforetoggle = bridge.accessor(HtmlElement.getOnBeforeToggle, HtmlElement.setOnBeforeToggle, .{});\n    pub const onblur = bridge.accessor(HtmlElement.getOnBlur, HtmlElement.setOnBlur, .{});\n    pub const oncancel = bridge.accessor(HtmlElement.getOnCancel, HtmlElement.setOnCancel, .{});\n    pub const oncanplay = bridge.accessor(HtmlElement.getOnCanPlay, HtmlElement.setOnCanPlay, .{});\n    pub const oncanplaythrough = bridge.accessor(HtmlElement.getOnCanPlayThrough, HtmlElement.setOnCanPlayThrough, .{});\n    pub const onchange = bridge.accessor(HtmlElement.getOnChange, HtmlElement.setOnChange, .{});\n    pub const onclick = bridge.accessor(HtmlElement.getOnClick, HtmlElement.setOnClick, .{});\n    pub const onclose = bridge.accessor(HtmlElement.getOnClose, HtmlElement.setOnClose, .{});\n    pub const oncommand = bridge.accessor(HtmlElement.getOnCommand, HtmlElement.setOnCommand, .{});\n    pub const oncontentvisibilityautostatechange = bridge.accessor(HtmlElement.getOnContentVisibilityAutoStateChange, HtmlElement.setOnContentVisibilityAutoStateChange, .{});\n    pub const oncontextlost = bridge.accessor(HtmlElement.getOnContextLost, HtmlElement.setOnContextLost, .{});\n    pub const oncontextmenu = bridge.accessor(HtmlElement.getOnContextMenu, HtmlElement.setOnContextMenu, .{});\n    pub const oncontextrestored = bridge.accessor(HtmlElement.getOnContextRestored, HtmlElement.setOnContextRestored, .{});\n    pub const oncopy = bridge.accessor(HtmlElement.getOnCopy, HtmlElement.setOnCopy, .{});\n    pub const oncuechange = bridge.accessor(HtmlElement.getOnCueChange, HtmlElement.setOnCueChange, .{});\n    pub const oncut = bridge.accessor(HtmlElement.getOnCut, HtmlElement.setOnCut, .{});\n    pub const ondblclick = bridge.accessor(HtmlElement.getOnDblClick, HtmlElement.setOnDblClick, .{});\n    pub const ondrag = bridge.accessor(HtmlElement.getOnDrag, HtmlElement.setOnDrag, .{});\n    pub const ondragend = bridge.accessor(HtmlElement.getOnDragEnd, HtmlElement.setOnDragEnd, .{});\n    pub const ondragenter = bridge.accessor(HtmlElement.getOnDragEnter, HtmlElement.setOnDragEnter, .{});\n    pub const ondragexit = bridge.accessor(HtmlElement.getOnDragExit, HtmlElement.setOnDragExit, .{});\n    pub const ondragleave = bridge.accessor(HtmlElement.getOnDragLeave, HtmlElement.setOnDragLeave, .{});\n    pub const ondragover = bridge.accessor(HtmlElement.getOnDragOver, HtmlElement.setOnDragOver, .{});\n    pub const ondragstart = bridge.accessor(HtmlElement.getOnDragStart, HtmlElement.setOnDragStart, .{});\n    pub const ondrop = bridge.accessor(HtmlElement.getOnDrop, HtmlElement.setOnDrop, .{});\n    pub const ondurationchange = bridge.accessor(HtmlElement.getOnDurationChange, HtmlElement.setOnDurationChange, .{});\n    pub const onemptied = bridge.accessor(HtmlElement.getOnEmptied, HtmlElement.setOnEmptied, .{});\n    pub const onended = bridge.accessor(HtmlElement.getOnEnded, HtmlElement.setOnEnded, .{});\n    pub const onerror = bridge.accessor(HtmlElement.getOnError, HtmlElement.setOnError, .{});\n    pub const onfocus = bridge.accessor(HtmlElement.getOnFocus, HtmlElement.setOnFocus, .{});\n    pub const onformdata = bridge.accessor(HtmlElement.getOnFormData, HtmlElement.setOnFormData, .{});\n    pub const onfullscreenchange = bridge.accessor(HtmlElement.getOnFullscreenChange, HtmlElement.setOnFullscreenChange, .{});\n    pub const onfullscreenerror = bridge.accessor(HtmlElement.getOnFullscreenError, HtmlElement.setOnFullscreenError, .{});\n    pub const ongotpointercapture = bridge.accessor(HtmlElement.getOnGotPointerCapture, HtmlElement.setOnGotPointerCapture, .{});\n    pub const oninput = bridge.accessor(HtmlElement.getOnInput, HtmlElement.setOnInput, .{});\n    pub const oninvalid = bridge.accessor(HtmlElement.getOnInvalid, HtmlElement.setOnInvalid, .{});\n    pub const onkeydown = bridge.accessor(HtmlElement.getOnKeyDown, HtmlElement.setOnKeyDown, .{});\n    pub const onkeypress = bridge.accessor(HtmlElement.getOnKeyPress, HtmlElement.setOnKeyPress, .{});\n    pub const onkeyup = bridge.accessor(HtmlElement.getOnKeyUp, HtmlElement.setOnKeyUp, .{});\n    pub const onload = bridge.accessor(HtmlElement.getOnLoad, HtmlElement.setOnLoad, .{});\n    pub const onloadeddata = bridge.accessor(HtmlElement.getOnLoadedData, HtmlElement.setOnLoadedData, .{});\n    pub const onloadedmetadata = bridge.accessor(HtmlElement.getOnLoadedMetadata, HtmlElement.setOnLoadedMetadata, .{});\n    pub const onloadstart = bridge.accessor(HtmlElement.getOnLoadStart, HtmlElement.setOnLoadStart, .{});\n    pub const onlostpointercapture = bridge.accessor(HtmlElement.getOnLostPointerCapture, HtmlElement.setOnLostPointerCapture, .{});\n    pub const onmousedown = bridge.accessor(HtmlElement.getOnMouseDown, HtmlElement.setOnMouseDown, .{});\n    pub const onmousemove = bridge.accessor(HtmlElement.getOnMouseMove, HtmlElement.setOnMouseMove, .{});\n    pub const onmouseout = bridge.accessor(HtmlElement.getOnMouseOut, HtmlElement.setOnMouseOut, .{});\n    pub const onmouseover = bridge.accessor(HtmlElement.getOnMouseOver, HtmlElement.setOnMouseOver, .{});\n    pub const onmouseup = bridge.accessor(HtmlElement.getOnMouseUp, HtmlElement.setOnMouseUp, .{});\n    pub const onpaste = bridge.accessor(HtmlElement.getOnPaste, HtmlElement.setOnPaste, .{});\n    pub const onpause = bridge.accessor(HtmlElement.getOnPause, HtmlElement.setOnPause, .{});\n    pub const onplay = bridge.accessor(HtmlElement.getOnPlay, HtmlElement.setOnPlay, .{});\n    pub const onplaying = bridge.accessor(HtmlElement.getOnPlaying, HtmlElement.setOnPlaying, .{});\n    pub const onpointercancel = bridge.accessor(HtmlElement.getOnPointerCancel, HtmlElement.setOnPointerCancel, .{});\n    pub const onpointerdown = bridge.accessor(HtmlElement.getOnPointerDown, HtmlElement.setOnPointerDown, .{});\n    pub const onpointerenter = bridge.accessor(HtmlElement.getOnPointerEnter, HtmlElement.setOnPointerEnter, .{});\n    pub const onpointerleave = bridge.accessor(HtmlElement.getOnPointerLeave, HtmlElement.setOnPointerLeave, .{});\n    pub const onpointermove = bridge.accessor(HtmlElement.getOnPointerMove, HtmlElement.setOnPointerMove, .{});\n    pub const onpointerout = bridge.accessor(HtmlElement.getOnPointerOut, HtmlElement.setOnPointerOut, .{});\n    pub const onpointerover = bridge.accessor(HtmlElement.getOnPointerOver, HtmlElement.setOnPointerOver, .{});\n    pub const onpointerrawupdate = bridge.accessor(HtmlElement.getOnPointerRawUpdate, HtmlElement.setOnPointerRawUpdate, .{});\n    pub const onpointerup = bridge.accessor(HtmlElement.getOnPointerUp, HtmlElement.setOnPointerUp, .{});\n    pub const onprogress = bridge.accessor(HtmlElement.getOnProgress, HtmlElement.setOnProgress, .{});\n    pub const onratechange = bridge.accessor(HtmlElement.getOnRateChange, HtmlElement.setOnRateChange, .{});\n    pub const onreset = bridge.accessor(HtmlElement.getOnReset, HtmlElement.setOnReset, .{});\n    pub const onresize = bridge.accessor(HtmlElement.getOnResize, HtmlElement.setOnResize, .{});\n    pub const onscroll = bridge.accessor(HtmlElement.getOnScroll, HtmlElement.setOnScroll, .{});\n    pub const onscrollend = bridge.accessor(HtmlElement.getOnScrollEnd, HtmlElement.setOnScrollEnd, .{});\n    pub const onsecuritypolicyviolation = bridge.accessor(HtmlElement.getOnSecurityPolicyViolation, HtmlElement.setOnSecurityPolicyViolation, .{});\n    pub const onseeked = bridge.accessor(HtmlElement.getOnSeeked, HtmlElement.setOnSeeked, .{});\n    pub const onseeking = bridge.accessor(HtmlElement.getOnSeeking, HtmlElement.setOnSeeking, .{});\n    pub const onselect = bridge.accessor(HtmlElement.getOnSelect, HtmlElement.setOnSelect, .{});\n    pub const onselectionchange = bridge.accessor(HtmlElement.getOnSelectionChange, HtmlElement.setOnSelectionChange, .{});\n    pub const onselectstart = bridge.accessor(HtmlElement.getOnSelectStart, HtmlElement.setOnSelectStart, .{});\n    pub const onslotchange = bridge.accessor(HtmlElement.getOnSlotChange, HtmlElement.setOnSlotChange, .{});\n    pub const onstalled = bridge.accessor(HtmlElement.getOnStalled, HtmlElement.setOnStalled, .{});\n    pub const onsubmit = bridge.accessor(HtmlElement.getOnSubmit, HtmlElement.setOnSubmit, .{});\n    pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{});\n    pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{});\n    pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{});\n    pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{});\n    pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{});\n    pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{});\n    pub const ontransitionstart = bridge.accessor(HtmlElement.getOnTransitionStart, HtmlElement.setOnTransitionStart, .{});\n    pub const onvolumechange = bridge.accessor(HtmlElement.getOnVolumeChange, HtmlElement.setOnVolumeChange, .{});\n    pub const onwaiting = bridge.accessor(HtmlElement.getOnWaiting, HtmlElement.setOnWaiting, .{});\n    pub const onwheel = bridge.accessor(HtmlElement.getOnWheel, HtmlElement.setOnWheel, .{});\n};\n\npub const Build = struct {\n    // Calls `func_name` with `args` on the most specific type where it is\n    // implement. This could be on the HtmlElement itself.\n    pub fn call(self: *const HtmlElement, comptime func_name: []const u8, args: anytype) !bool {\n        inline for (@typeInfo(HtmlElement.Type).@\"union\".fields) |f| {\n            if (@field(HtmlElement.Type, f.name) == self._type) {\n                // The inner type implements this function. Call it and we're done.\n                const S = reflect.Struct(f.type);\n                if (@hasDecl(S, \"Build\")) {\n                    if (@hasDecl(S.Build, func_name)) {\n                        try @call(.auto, @field(S.Build, func_name), args);\n                        return true;\n                    }\n                }\n            }\n        }\n\n        if (@hasDecl(HtmlElement.Build, func_name)) {\n            // Our last resort - the node implements this function.\n            try @call(.auto, @field(HtmlElement.Build, func_name), args);\n            return true;\n        }\n\n        // inform our caller (the Element) that we didn't find anything that implemented\n        // func_name and it should keep searching for a match.\n        return false;\n    }\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: HTML.event_listeners\" {\n    try testing.htmlRunner(\"element/html/event_listeners.html\", .{});\n}\ntest \"WebApi: HTMLElement.props\" {\n    try testing.htmlRunner(\"element/html/htmlelement-props.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/Svg.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Element = @import(\"../Element.zig\");\n\npub const Generic = @import(\"svg/Generic.zig\");\n\nconst Svg = @This();\n_type: Type,\n_proto: *Element,\n_tag_name: String, // Svg elements are case-preserving\n\npub const Type = union(enum) {\n    svg,\n    generic: *Generic,\n};\n\npub fn is(self: *Svg, comptime T: type) ?*T {\n    inline for (@typeInfo(Type).@\"union\".fields) |f| {\n        if (@field(Type, f.name) == self._type) {\n            if (f.type == T) {\n                return &@field(self._type, f.name);\n            }\n            if (f.type == *T) {\n                return @field(self._type, f.name);\n            }\n        }\n    }\n    return null;\n}\n\npub fn asElement(self: *Svg) *Element {\n    return self._proto;\n}\npub fn asNode(self: *Svg) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Svg);\n\n    pub const Meta = struct {\n        pub const name = \"SVGElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: Svg\" {\n    try testing.htmlRunner(\"element/svg\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Anchor.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst URL = @import(\"../../../URL.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Anchor = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Anchor) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Anchor) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Anchor) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getHref(self: *Anchor, page: *Page) ![]const u8 {\n    const element = self.asElement();\n    const href = element.getAttributeSafe(comptime .wrap(\"href\")) orelse return \"\";\n    if (href.len == 0) {\n        return \"\";\n    }\n    return URL.resolve(page.call_arena, page.base(), href, .{ .encode = true });\n}\n\npub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"href\"), .wrap(value), page);\n}\n\npub fn getTarget(self: *Anchor) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"target\")) orelse \"\";\n}\n\npub fn setTarget(self: *Anchor, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"target\"), .wrap(value), page);\n}\n\npub fn getOrigin(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    return (try URL.getOrigin(page.call_arena, href)) orelse \"null\";\n}\n\npub fn getHost(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    const host = URL.getHost(href);\n    const protocol = URL.getProtocol(href);\n    const port = URL.getPort(href);\n\n    // Strip default ports\n    if (port.len > 0) {\n        if ((std.mem.eql(u8, protocol, \"https:\") and std.mem.eql(u8, port, \"443\")) or\n            (std.mem.eql(u8, protocol, \"http:\") and std.mem.eql(u8, port, \"80\")))\n        {\n            return URL.getHostname(href);\n        }\n    }\n\n    return host;\n}\n\npub fn setHost(self: *Anchor, value: []const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setHost(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getHostname(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    return URL.getHostname(href);\n}\n\npub fn setHostname(self: *Anchor, value: []const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setHostname(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getPort(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    const port = URL.getPort(href);\n    const protocol = URL.getProtocol(href);\n\n    // Return empty string for default ports\n    if (port.len > 0) {\n        if ((std.mem.eql(u8, protocol, \"https:\") and std.mem.eql(u8, port, \"443\")) or\n            (std.mem.eql(u8, protocol, \"http:\") and std.mem.eql(u8, port, \"80\")))\n        {\n            return \"\";\n        }\n    }\n\n    return port;\n}\n\npub fn setPort(self: *Anchor, value: ?[]const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setPort(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getSearch(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    return URL.getSearch(href);\n}\n\npub fn setSearch(self: *Anchor, value: []const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setSearch(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getHash(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    return URL.getHash(href);\n}\n\npub fn setHash(self: *Anchor, value: []const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setHash(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getPathname(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    return URL.getPathname(href);\n}\n\npub fn setPathname(self: *Anchor, value: []const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setPathname(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getProtocol(self: *Anchor, page: *Page) ![]const u8 {\n    const href = try getResolvedHref(self, page) orelse return \"\";\n    return URL.getProtocol(href);\n}\n\npub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void {\n    const href = try getResolvedHref(self, page) orelse return;\n    const new_href = try URL.setProtocol(href, value, page.call_arena);\n    try setHref(self, new_href, page);\n}\n\npub fn getType(self: *Anchor) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"type\")) orelse \"\";\n}\n\npub fn setType(self: *Anchor, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"type\"), .wrap(value), page);\n}\n\npub fn getName(self: *const Anchor) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Anchor, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(value), page);\n}\n\npub fn getText(self: *Anchor, page: *Page) ![:0]const u8 {\n    return self.asNode().getTextContentAlloc(page.call_arena);\n}\n\npub fn setText(self: *Anchor, value: []const u8, page: *Page) !void {\n    try self.asNode().setTextContent(value, page);\n}\n\nfn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 {\n    const href = self.asElement().getAttributeSafe(comptime .wrap(\"href\")) orelse return null;\n    if (href.len == 0) {\n        return null;\n    }\n    return try URL.resolve(page.call_arena, page.base(), href, .{});\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Anchor);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLAnchorElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{});\n    pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{});\n    pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{});\n    pub const origin = bridge.accessor(Anchor.getOrigin, null, .{});\n    pub const protocol = bridge.accessor(Anchor.getProtocol, Anchor.setProtocol, .{});\n    pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{});\n    pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{});\n    pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{});\n    pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{});\n    pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{});\n    pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});\n    pub const @\"type\" = bridge.accessor(Anchor.getType, Anchor.setType, .{});\n    pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{});\n    pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });\n    pub const toString = bridge.function(Anchor.getHref, .{});\n\n    fn _getRelList(self: *Anchor, page: *Page) !?*@import(\"../../collections.zig\").DOMTokenList {\n        const element = self.asElement();\n        // relList is only valid for HTML and SVG <a> elements\n        const namespace = element._namespace;\n        if (namespace != .html and namespace != .svg) {\n            return null;\n        }\n        return element.getRelList(page);\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Anchor\" {\n    try testing.htmlRunner(\"element/html/anchor.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Area.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Area = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Area) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Area) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Area);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLAreaElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Audio.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst Media = @import(\"Media.zig\");\n\nconst Audio = @This();\n\n_proto: *Media,\n\npub fn constructor(maybe_url: ?String, page: *Page) !*Media {\n    const node = try page.createElementNS(.html, \"audio\", null);\n    const el = node.as(Element);\n\n    const list = try el.getOrCreateAttributeList(page);\n    // Always set to \"auto\" initially.\n    _ = try list.putSafe(comptime .wrap(\"preload\"), comptime .wrap(\"auto\"), el, page);\n    // Set URL if provided.\n    if (maybe_url) |url| {\n        _ = try list.putSafe(comptime .wrap(\"src\"), url, el, page);\n    }\n\n    return node.as(Media);\n}\n\npub fn asMedia(self: *Audio) *Media {\n    return self._proto;\n}\n\npub fn asElement(self: *Audio) *Element {\n    return self._proto.asElement();\n}\n\npub fn asNode(self: *Audio) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Audio);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLAudioElement\";\n        pub const constructor_alias = \"Audio\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(Audio.constructor, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/BR.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst BR = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *BR) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *BR) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(BR);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLBRElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Base.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Base = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Base) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Base) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Base);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLBaseElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Body.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst log = @import(\"../../../../log.zig\");\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Body = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Body) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Body) *Node {\n    return self.asElement().asNode();\n}\n\n/// Special-case: `body.onload` is actually an alias for `window.onload`.\npub fn setOnLoad(_: *Body, callback: ?js.Function.Global, page: *Page) !void {\n    page.window._on_load = callback;\n}\n\n/// Special-case: `body.onload` is actually an alias for `window.onload`.\npub fn getOnLoad(_: *Body, page: *Page) ?js.Function.Global {\n    return page.window._on_load;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Body);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLBodyElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const onload = bridge.accessor(getOnLoad, setOnLoad, .{ .null_as_undefined = false });\n};\n\npub const Build = struct {\n    pub fn complete(node: *Node, page: *Page) !void {\n        const el = node.as(Element);\n        const on_load = el.getAttributeSafe(comptime .wrap(\"onload\")) orelse return;\n        if (page.js.stringToPersistedFunction(on_load, &.{\"event\"}, &.{})) |func| {\n            page.window._on_load = func;\n        } else |err| {\n            log.err(.js, \"body.onload\", .{ .err = err, .str = on_load });\n        }\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Button.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst Form = @import(\"Form.zig\");\n\nconst Button = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Button) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Button) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Button) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getDisabled(self: *const Button) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *Button, disabled: bool, page: *Page) !void {\n    if (disabled) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getName(self: *const Button) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Button, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\npub fn getType(self: *const Button) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"type\")) orelse \"submit\";\n}\n\npub fn setType(self: *Button, typ: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"type\"), .wrap(typ), page);\n}\n\npub fn getValue(self: *const Button) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"value\")) orelse \"\";\n}\n\npub fn setValue(self: *Button, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"value\"), .wrap(value), page);\n}\n\npub fn getRequired(self: *const Button) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"required\")) != null;\n}\n\npub fn setRequired(self: *Button, required: bool, page: *Page) !void {\n    if (required) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"required\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"required\"), page);\n    }\n}\n\npub fn getForm(self: *Button, page: *Page) ?*Form {\n    const element = self.asElement();\n\n    // If form attribute exists, ONLY use that (even if it references nothing)\n    if (element.getAttributeSafe(comptime .wrap(\"form\"))) |form_id| {\n        if (page.document.getElementById(form_id, page)) |form_element| {\n            return form_element.is(Form);\n        }\n        // form attribute present but invalid - no form owner\n        return null;\n    }\n\n    // No form attribute - traverse ancestors looking for a <form>\n    var node = element.asNode()._parent;\n    while (node) |n| {\n        if (n.is(Element.Html.Form)) |form| {\n            return form;\n        }\n        node = n._parent;\n    }\n\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Button);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLButtonElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const disabled = bridge.accessor(Button.getDisabled, Button.setDisabled, .{});\n    pub const name = bridge.accessor(Button.getName, Button.setName, .{});\n    pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{});\n    pub const form = bridge.accessor(Button.getForm, null, .{});\n    pub const value = bridge.accessor(Button.getValue, Button.setValue, .{});\n    pub const @\"type\" = bridge.accessor(Button.getType, Button.setType, .{});\n};\n\npub const Build = struct {\n    pub fn created(_: *Node, _: *Page) !void {\n        // No initialization needed - disabled is lazy from attribute\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Button\" {\n    try testing.htmlRunner(\"element/html/button.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Canvas.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst CanvasRenderingContext2D = @import(\"../../canvas/CanvasRenderingContext2D.zig\");\nconst WebGLRenderingContext = @import(\"../../canvas/WebGLRenderingContext.zig\");\nconst OffscreenCanvas = @import(\"../../canvas/OffscreenCanvas.zig\");\n\nconst Canvas = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Canvas) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Canvas) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Canvas) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getWidth(self: *const Canvas) u32 {\n    const attr = self.asConstElement().getAttributeSafe(comptime .wrap(\"width\")) orelse return 300;\n    return std.fmt.parseUnsigned(u32, attr, 10) catch 300;\n}\n\npub fn setWidth(self: *Canvas, value: u32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"width\"), .wrap(str), page);\n}\n\npub fn getHeight(self: *const Canvas) u32 {\n    const attr = self.asConstElement().getAttributeSafe(comptime .wrap(\"height\")) orelse return 150;\n    return std.fmt.parseUnsigned(u32, attr, 10) catch 150;\n}\n\npub fn setHeight(self: *Canvas, value: u32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"height\"), .wrap(str), page);\n}\n\n/// Since there's no base class rendering contextes inherit from,\n/// we're using tagged union.\nconst DrawingContext = union(enum) {\n    @\"2d\": *CanvasRenderingContext2D,\n    webgl: *WebGLRenderingContext,\n};\n\npub fn getContext(_: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {\n    if (std.mem.eql(u8, context_type, \"2d\")) {\n        const ctx = try page._factory.create(CanvasRenderingContext2D{});\n        return .{ .@\"2d\" = ctx };\n    }\n\n    if (std.mem.eql(u8, context_type, \"webgl\") or std.mem.eql(u8, context_type, \"experimental-webgl\")) {\n        const ctx = try page._factory.create(WebGLRenderingContext{});\n        return .{ .webgl = ctx };\n    }\n\n    return null;\n}\n\n/// Transfers control of the canvas to an OffscreenCanvas.\n/// Returns an OffscreenCanvas with the same dimensions.\npub fn transferControlToOffscreen(self: *Canvas, page: *Page) !*OffscreenCanvas {\n    const width = self.getWidth();\n    const height = self.getHeight();\n    return OffscreenCanvas.constructor(width, height, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Canvas);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLCanvasElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const width = bridge.accessor(Canvas.getWidth, Canvas.setWidth, .{});\n    pub const height = bridge.accessor(Canvas.getHeight, Canvas.setHeight, .{});\n    pub const getContext = bridge.function(Canvas.getContext, .{});\n    pub const transferControlToOffscreen = bridge.function(Canvas.transferControlToOffscreen, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Custom.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\nconst log = @import(\"../../../../log.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst CustomElementDefinition = @import(\"../../CustomElementDefinition.zig\");\n\nconst Custom = @This();\n_proto: *HtmlElement,\n_tag_name: String,\n_definition: ?*CustomElementDefinition,\n_connected_callback_invoked: bool = false,\n_disconnected_callback_invoked: bool = false,\n\npub fn asElement(self: *Custom) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Custom) *Node {\n    return self.asElement().asNode();\n}\n\npub fn invokeConnectedCallback(self: *Custom, page: *Page) void {\n    // Only invoke if we haven't already called it while connected\n    if (self._connected_callback_invoked) {\n        return;\n    }\n\n    self._connected_callback_invoked = true;\n    self._disconnected_callback_invoked = false;\n    self.invokeCallback(\"connectedCallback\", .{}, page);\n}\n\npub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {\n    // Only invoke if we haven't already called it while disconnected\n    if (self._disconnected_callback_invoked) {\n        return;\n    }\n\n    self._disconnected_callback_invoked = true;\n    self._connected_callback_invoked = false;\n    self.invokeCallback(\"disconnectedCallback\", .{}, page);\n}\n\npub fn invokeAttributeChangedCallback(self: *Custom, name: String, old_value: ?String, new_value: ?String, page: *Page) void {\n    const definition = self._definition orelse return;\n    if (!definition.isAttributeObserved(name)) {\n        return;\n    }\n    self.invokeCallback(\"attributeChangedCallback\", .{ name, old_value, new_value }, page);\n}\n\npub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, page: *Page) !void {\n    // Autonomous custom element\n    if (element.is(Custom)) |custom| {\n        // If the element is undefined, check if a definition now exists and upgrade\n        if (custom._definition == null) {\n            const name = custom._tag_name.str();\n            if (page.window._custom_elements._definitions.get(name)) |definition| {\n                const CustomElementRegistry = @import(\"../../CustomElementRegistry.zig\");\n                CustomElementRegistry.upgradeCustomElement(custom, definition, page) catch {};\n                return;\n            }\n        }\n\n        if (comptime from_parser) {\n            // From parser, we know the element is brand new\n            custom._connected_callback_invoked = true;\n            custom.invokeCallback(\"connectedCallback\", .{}, page);\n        } else {\n            custom.invokeConnectedCallback(page);\n        }\n        return;\n    }\n\n    // Customized built-in element - check if it actually has a definition first\n    const definition = page.getCustomizedBuiltInDefinition(element) orelse return;\n\n    if (comptime from_parser) {\n        // From parser, we know the element is brand new, skip the tracking check\n        try page._customized_builtin_connected_callback_invoked.put(\n            page.arena,\n            element,\n            {},\n        );\n    } else {\n        // Not from parser, check if we've already invoked while connected\n        const gop = try page._customized_builtin_connected_callback_invoked.getOrPut(\n            page.arena,\n            element,\n        );\n        if (gop.found_existing) {\n            return;\n        }\n        gop.value_ptr.* = {};\n    }\n\n    _ = page._customized_builtin_disconnected_callback_invoked.remove(element);\n    invokeCallbackOnElement(element, definition, \"connectedCallback\", .{}, page);\n}\n\npub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void {\n    // Autonomous custom element\n    if (element.is(Custom)) |custom| {\n        custom.invokeDisconnectedCallback(page);\n        return;\n    }\n\n    // Customized built-in element - check if it actually has a definition first\n    const definition = page.getCustomizedBuiltInDefinition(element) orelse return;\n\n    // Check if we've already invoked disconnectedCallback while disconnected\n    const gop = page._customized_builtin_disconnected_callback_invoked.getOrPut(\n        page.arena,\n        element,\n    ) catch return;\n    if (gop.found_existing) return;\n    gop.value_ptr.* = {};\n\n    _ = page._customized_builtin_connected_callback_invoked.remove(element);\n\n    invokeCallbackOnElement(element, definition, \"disconnectedCallback\", .{}, page);\n}\n\npub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, page: *Page) void {\n    // Autonomous custom element\n    if (element.is(Custom)) |custom| {\n        custom.invokeAttributeChangedCallback(name, old_value, new_value, page);\n        return;\n    }\n\n    // Customized built-in element - check if attribute is observed\n    const definition = page.getCustomizedBuiltInDefinition(element) orelse return;\n    if (!definition.isAttributeObserved(name)) return;\n    invokeCallbackOnElement(element, definition, \"attributeChangedCallback\", .{ name, old_value, new_value }, page);\n}\n\nfn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {\n    _ = definition;\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    // Get the JS element object\n    const js_val = ls.local.zigValueToJs(element, .{}) catch return;\n    const js_element = js_val.toObject();\n\n    // Call the callback method if it exists\n    js_element.callMethod(void, callback_name, args) catch return;\n}\n\n// Check if element has \"is\" attribute and attach customized built-in definition\npub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {\n    const is_value = element.getAttributeSafe(comptime .wrap(\"is\")) orelse return;\n\n    const custom_elements = page.window.getCustomElements();\n    const definition = custom_elements._definitions.get(is_value) orelse return;\n\n    const extends_tag = definition.extends orelse return;\n    if (extends_tag != element.getTag()) {\n        return;\n    }\n\n    // Attach the definition\n    try page.setCustomizedBuiltInDefinition(element, definition);\n\n    // Reset callback flags since this is a fresh upgrade\n    _ = page._customized_builtin_connected_callback_invoked.remove(element);\n    _ = page._customized_builtin_disconnected_callback_invoked.remove(element);\n\n    // Invoke constructor\n    const prev_upgrading = page._upgrading_element;\n    const node = element.asNode();\n    page._upgrading_element = node;\n    defer page._upgrading_element = prev_upgrading;\n\n    // PERFORMANCE OPTIMIZATION: This pattern is discouraged in general code.\n    // Used here because: (1) multiple early returns before needing Local,\n    // (2) called from both V8 callbacks (Local exists) and parser (no Local).\n    // Prefer either: requiring *const js.Local parameter, OR always creating\n    // Local.Scope upfront.\n    var ls: ?js.Local.Scope = null;\n    var local = blk: {\n        if (page.js.local) |l| {\n            break :blk l;\n        }\n        ls = undefined;\n        page.js.localScope(&ls.?);\n        break :blk &ls.?.local;\n    };\n    defer if (ls) |*_ls| {\n        _ls.deinit();\n    };\n\n    var caught: js.TryCatch.Caught = undefined;\n    _ = local.toLocal(definition.constructor).newInstance(&caught) catch |err| {\n        log.warn(.js, \"custom builtin ctor\", .{ .name = is_value, .err = err, .caught = caught });\n        return;\n    };\n}\n\nfn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {\n    if (self._definition == null) {\n        return;\n    }\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    const js_val = ls.local.zigValueToJs(self, .{}) catch return;\n    const js_element = js_val.toObject();\n\n    js_element.callMethod(void, callback_name, args) catch return;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Custom);\n\n    pub const Meta = struct {\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/DList.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst DList = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *DList) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *DList) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DList);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDListElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Data.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Data = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Data) *Element {\n    return self._proto._proto;\n}\n\npub fn asNode(self: *Data) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getValue(self: *Data) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"value\")) orelse \"\";\n}\n\npub fn setValue(self: *Data, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"value\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Data);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDataElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const value = bridge.accessor(Data.getValue, Data.setValue, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/DataList.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst DataList = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *DataList) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *DataList) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(DataList);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDataListElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Details.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Details = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Details) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Details) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Details) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getOpen(self: *const Details) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"open\")) != null;\n}\n\npub fn setOpen(self: *Details, open: bool, page: *Page) !void {\n    if (open) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"open\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"open\"), page);\n    }\n}\n\npub fn getName(self: *const Details) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Details, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Details);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDetailsElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const open = bridge.accessor(Details.getOpen, Details.setOpen, .{});\n    pub const name = bridge.accessor(Details.getName, Details.setName, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Details\" {\n    try testing.htmlRunner(\"element/html/details.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Dialog.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Dialog = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Dialog) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Dialog) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Dialog) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getOpen(self: *const Dialog) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"open\")) != null;\n}\n\npub fn setOpen(self: *Dialog, open: bool, page: *Page) !void {\n    if (open) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"open\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"open\"), page);\n    }\n}\n\npub fn getReturnValue(self: *const Dialog) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"returnvalue\")) orelse \"\";\n}\n\npub fn setReturnValue(self: *Dialog, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"returnvalue\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Dialog);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDialogElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const open = bridge.accessor(Dialog.getOpen, Dialog.setOpen, .{});\n    pub const returnValue = bridge.accessor(Dialog.getReturnValue, Dialog.setReturnValue, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Dialog\" {\n    try testing.htmlRunner(\"element/html/dialog.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Directory.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Directory = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Directory) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Directory) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Directory);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDirectoryElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Div.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Div = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Div) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Div) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Div);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLDivElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Embed.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Embed = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Embed) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Embed) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Embed);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLEmbedElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/FieldSet.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst FieldSet = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *FieldSet) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *FieldSet) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getDisabled(self: *FieldSet) bool {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *FieldSet, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getName(self: *FieldSet) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *FieldSet, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FieldSet);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLFieldSetElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const disabled = bridge.accessor(FieldSet.getDisabled, FieldSet.setDisabled, .{});\n    pub const name = bridge.accessor(FieldSet.getName, FieldSet.setName, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.FieldSet\" {\n    try testing.htmlRunner(\"element/html/fieldset.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Font.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Font = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Font) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Font) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Font);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLFontElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Form.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst URL = @import(\"../../../URL.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst collections = @import(\"../../collections.zig\");\n\npub const Input = @import(\"Input.zig\");\npub const Button = @import(\"Button.zig\");\npub const Select = @import(\"Select.zig\");\npub const TextArea = @import(\"TextArea.zig\");\n\nconst Form = @This();\n_proto: *HtmlElement,\n\npub fn asHtmlElement(self: *Form) *HtmlElement {\n    return self._proto;\n}\nfn asConstElement(self: *const Form) *const Element {\n    return self._proto._proto;\n}\npub fn asElement(self: *Form) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Form) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getName(self: *const Form) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Form, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\npub fn getMethod(self: *const Form) []const u8 {\n    const method = self.asConstElement().getAttributeSafe(comptime .wrap(\"method\")) orelse return \"get\";\n\n    if (std.ascii.eqlIgnoreCase(method, \"post\")) {\n        return \"post\";\n    }\n    if (std.ascii.eqlIgnoreCase(method, \"dialog\")) {\n        return \"dialog\";\n    }\n    // invalid, or it was get all along\n    return \"get\";\n}\n\npub fn setMethod(self: *Form, method: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"method\"), .wrap(method), page);\n}\n\npub fn getElements(self: *Form, page: *Page) !*collections.HTMLFormControlsCollection {\n    const form_id = self.asElement().getAttributeSafe(comptime .wrap(\"id\"));\n    const root = if (form_id != null)\n        self.asNode().getRootNode(null) // Has ID: walk entire document to find form=ID controls\n    else\n        self.asNode(); // No ID: walk only form subtree (no external controls possible)\n\n    const node_live = collections.NodeLive(.form).init(root, self, page);\n    const html_collection = try node_live.runtimeGenericWrap(page);\n\n    return page._factory.create(collections.HTMLFormControlsCollection{\n        ._proto = html_collection,\n    });\n}\n\npub fn getAction(self: *Form, page: *Page) ![]const u8 {\n    const element = self.asElement();\n    const action = element.getAttributeSafe(comptime .wrap(\"action\")) orelse return page.url;\n    if (action.len == 0) {\n        return page.url;\n    }\n    return URL.resolve(page.call_arena, page.base(), action, .{ .encode = true });\n}\n\npub fn setAction(self: *Form, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"action\"), .wrap(value), page);\n}\n\npub fn getTarget(self: *Form) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"target\")) orelse \"\";\n}\n\npub fn setTarget(self: *Form, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"target\"), .wrap(value), page);\n}\n\npub fn getLength(self: *Form, page: *Page) !u32 {\n    const elements = try self.getElements(page);\n    return elements.length(page);\n}\n\npub fn submit(self: *Form, page: *Page) !void {\n    return page.submitForm(null, self, .{ .fire_event = false });\n}\n\n/// https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit\n/// Like submit(), but fires the submit event and validates the form.\npub fn requestSubmit(self: *Form, submitter: ?*Element, page: *Page) !void {\n    const submitter_element = if (submitter) |s| blk: {\n        // The submitter must be a submit button.\n        if (!isSubmitButton(s)) return error.TypeError;\n\n        // The submitter's form owner must be this form element.\n        const submitter_form = getFormOwner(s, page);\n        if (submitter_form == null or submitter_form.? != self) return error.NotFound;\n\n        break :blk s;\n    } else self.asElement();\n\n    return page.submitForm(submitter_element, self, .{});\n}\n\n/// Returns true if the element is a submit button per the HTML spec:\n/// - <input type=\"submit\"> or <input type=\"image\">\n/// - <button type=\"submit\"> (including default, since button's default type is \"submit\")\nfn isSubmitButton(element: *Element) bool {\n    if (element.is(Input)) |input| {\n        return input._input_type == .submit or input._input_type == .image;\n    }\n    if (element.is(Button)) |button| {\n        return std.mem.eql(u8, button.getType(), \"submit\");\n    }\n    return false;\n}\n\n/// Returns the form owner of a submittable element (Input or Button).\nfn getFormOwner(element: *Element, page: *Page) ?*Form {\n    if (element.is(Input)) |input| {\n        return input.getForm(page);\n    }\n    if (element.is(Button)) |button| {\n        return button.getForm(page);\n    }\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Form);\n    pub const Meta = struct {\n        pub const name = \"HTMLFormElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const name = bridge.accessor(Form.getName, Form.setName, .{});\n    pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});\n    pub const action = bridge.accessor(Form.getAction, Form.setAction, .{});\n    pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{});\n    pub const elements = bridge.accessor(Form.getElements, null, .{});\n    pub const length = bridge.accessor(Form.getLength, null, .{});\n    pub const submit = bridge.function(Form.submit, .{});\n    pub const requestSubmit = bridge.function(Form.requestSubmit, .{ .dom_exception = true });\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Form\" {\n    try testing.htmlRunner(\"element/html/form.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Generic.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Generic = @This();\n_tag_name: String,\n_tag: Element.Tag,\n_proto: *HtmlElement,\n\npub fn asElement(self: *Generic) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Generic) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Generic);\n\n    pub const Meta = struct {\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/HR.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst HR = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *HR) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *HR) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(HR);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLHRElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Head.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Head = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Head) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Head) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Head);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLHeadElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Heading.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Heading = @This();\n_proto: *HtmlElement,\n_tag_name: String,\n_tag: Element.Tag,\n\npub fn asElement(self: *Heading) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Heading) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Heading);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLHeadingElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Html.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Html = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Html) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Html) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Html);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLHtmlElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/IFrame.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst log = @import(\"../../../../log.zig\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Window = @import(\"../../Window.zig\");\nconst Document = @import(\"../../Document.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst URL = @import(\"../../URL.zig\");\n\nconst IFrame = @This();\n_proto: *HtmlElement,\n_src: []const u8 = \"\",\n_executed: bool = false,\n_window: ?*Window = null,\n\npub fn asElement(self: *IFrame) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *IFrame) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getContentWindow(self: *const IFrame) ?*Window {\n    return self._window;\n}\n\npub fn getContentDocument(self: *const IFrame) ?*Document {\n    const window = self._window orelse return null;\n    return window._document;\n}\n\npub fn getSrc(self: *const IFrame, page: *Page) ![:0]const u8 {\n    if (self._src.len == 0) return \"\";\n    return try URL.resolve(page.call_arena, page.base(), self._src, .{ .encode = true });\n}\n\npub fn setSrc(self: *IFrame, src: []const u8, page: *Page) !void {\n    const element = self.asElement();\n    try element.setAttributeSafe(comptime .wrap(\"src\"), .wrap(src), page);\n    self._src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse unreachable;\n    if (element.asNode().isConnected()) {\n        // unlike script, an iframe is reloaded every time the src is set\n        // even if it's set to the same URL.\n        self._executed = false;\n        try page.iframeAddedCallback(self);\n    }\n}\n\npub fn getName(self: *IFrame) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *IFrame, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(IFrame);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLIFrameElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const src = bridge.accessor(IFrame.getSrc, IFrame.setSrc, .{});\n    pub const name = bridge.accessor(IFrame.getName, IFrame.setName, .{});\n    pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{});\n    pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});\n};\n\npub const Build = struct {\n    pub fn complete(node: *Node, _: *Page) !void {\n        const self = node.as(IFrame);\n        const element = self.asElement();\n        self._src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse \"\";\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Image.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst URL = @import(\"../../../URL.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst Event = @import(\"../../Event.zig\");\nconst log = @import(\"../../../../log.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Image = @This();\n_proto: *HtmlElement,\n\npub fn constructor(w_: ?u32, h_: ?u32, page: *Page) !*Image {\n    const node = try page.createElementNS(.html, \"img\", null);\n    const el = node.as(Element);\n\n    if (w_) |w| blk: {\n        const w_string = std.fmt.bufPrint(&page.buf, \"{d}\", .{w}) catch break :blk;\n        try el.setAttributeSafe(comptime .wrap(\"width\"), .wrap(w_string), page);\n    }\n    if (h_) |h| blk: {\n        const h_string = std.fmt.bufPrint(&page.buf, \"{d}\", .{h}) catch break :blk;\n        try el.setAttributeSafe(comptime .wrap(\"height\"), .wrap(h_string), page);\n    }\n    return el.as(Image);\n}\n\npub fn asElement(self: *Image) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Image) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Image) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getSrc(self: *const Image, page: *Page) ![]const u8 {\n    const element = self.asConstElement();\n    const src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse return \"\";\n    if (src.len == 0) {\n        return \"\";\n    }\n\n    // Always resolve the src against the page URL\n    return URL.resolve(page.call_arena, page.base(), src, .{ .encode = true });\n}\n\npub fn setSrc(self: *Image, value: []const u8, page: *Page) !void {\n    const element = self.asElement();\n    try element.setAttributeSafe(comptime .wrap(\"src\"), .wrap(value), page);\n    // No need to check if `Image` is connected to DOM; this is a special case.\n    return self.imageAddedCallback(page);\n}\n\npub fn getAlt(self: *const Image) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"alt\")) orelse \"\";\n}\n\npub fn setAlt(self: *Image, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"alt\"), .wrap(value), page);\n}\n\npub fn getWidth(self: *const Image) u32 {\n    const attr = self.asConstElement().getAttributeSafe(comptime .wrap(\"width\")) orelse return 0;\n    return std.fmt.parseUnsigned(u32, attr, 10) catch 0;\n}\n\npub fn setWidth(self: *Image, value: u32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"width\"), .wrap(str), page);\n}\n\npub fn getHeight(self: *const Image) u32 {\n    const attr = self.asConstElement().getAttributeSafe(comptime .wrap(\"height\")) orelse return 0;\n    return std.fmt.parseUnsigned(u32, attr, 10) catch 0;\n}\n\npub fn setHeight(self: *Image, value: u32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"height\"), .wrap(str), page);\n}\n\npub fn getCrossOrigin(self: *const Image) ?[]const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"crossorigin\"));\n}\n\npub fn setCrossOrigin(self: *Image, value: ?[]const u8, page: *Page) !void {\n    if (value) |v| {\n        return self.asElement().setAttributeSafe(comptime .wrap(\"crossorigin\"), .wrap(v), page);\n    }\n    return self.asElement().removeAttribute(comptime .wrap(\"crossorigin\"), page);\n}\n\npub fn getLoading(self: *const Image) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"loading\")) orelse \"eager\";\n}\n\npub fn setLoading(self: *Image, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"loading\"), .wrap(value), page);\n}\n\npub fn getNaturalWidth(_: *const Image) u32 {\n    // this is a valid response under a number of normal conditions, but could\n    // be used to detect the nature of Browser.\n    return 0;\n}\n\npub fn getNaturalHeight(_: *const Image) u32 {\n    // this is a valid response under a number of normal conditions, but could\n    // be used to detect the nature of Browser.\n    return 0;\n}\n\npub fn getComplete(_: *const Image) bool {\n    // Per spec, complete is true when: no src/srcset, src is empty,\n    // image is fully available, or image is broken (with no pending request).\n    // Since we never fetch images, they are in the \"broken\" state, which has\n    // complete=true. This is consistent with naturalWidth/naturalHeight=0.\n    return true;\n}\n\n/// Used in `Page.nodeIsReady`.\npub fn imageAddedCallback(self: *Image, page: *Page) !void {\n    // if we're planning on navigating to another page, don't trigger load event.\n    if (page.isGoingAway()) {\n        return;\n    }\n\n    const element = self.asElement();\n    // Exit if src not set.\n    const src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse return;\n    if (src.len == 0) return;\n\n    try page._to_load.append(page.arena, self._proto);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Image);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLImageElement\";\n        pub const constructor_alias = \"Image\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(Image.constructor, .{});\n    pub const src = bridge.accessor(Image.getSrc, Image.setSrc, .{});\n    pub const alt = bridge.accessor(Image.getAlt, Image.setAlt, .{});\n    pub const width = bridge.accessor(Image.getWidth, Image.setWidth, .{});\n    pub const height = bridge.accessor(Image.getHeight, Image.setHeight, .{});\n    pub const crossOrigin = bridge.accessor(Image.getCrossOrigin, Image.setCrossOrigin, .{});\n    pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{});\n    pub const naturalWidth = bridge.accessor(Image.getNaturalWidth, null, .{});\n    pub const naturalHeight = bridge.accessor(Image.getNaturalHeight, null, .{});\n    pub const complete = bridge.accessor(Image.getComplete, null, .{});\n};\n\npub const Build = struct {\n    pub fn created(node: *Node, page: *Page) !void {\n        const self = node.as(Image);\n        return self.imageAddedCallback(page);\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Image\" {\n    try testing.htmlRunner(\"element/html/image.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Input.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst Form = @import(\"Form.zig\");\nconst Selection = @import(\"../../Selection.zig\");\nconst Event = @import(\"../../Event.zig\");\nconst InputEvent = @import(\"../../event/InputEvent.zig\");\n\nconst Input = @This();\n\npub const Type = enum {\n    text,\n    password,\n    checkbox,\n    radio,\n    submit,\n    reset,\n    button,\n    hidden,\n    image,\n    file,\n    email,\n    url,\n    tel,\n    search,\n    number,\n    range,\n    date,\n    time,\n    @\"datetime-local\",\n    month,\n    week,\n    color,\n\n    pub fn fromString(str: []const u8) Type {\n        // Longest type name is \"datetime-local\" at 14 chars\n        if (str.len > 32) {\n            return .text;\n        }\n\n        var buf: [32]u8 = undefined;\n        const lower = std.ascii.lowerString(&buf, str);\n        return std.meta.stringToEnum(Type, lower) orelse .text;\n    }\n\n    pub fn toString(self: Type) []const u8 {\n        return @tagName(self);\n    }\n};\n\n_proto: *HtmlElement,\n_default_value: ?[]const u8 = null,\n_default_checked: bool = false,\n_value: ?[]const u8 = null,\n_checked: bool = false,\n_checked_dirty: bool = false,\n_input_type: Type = .text,\n_indeterminate: bool = false,\n\n_selection_start: u32 = 0,\n_selection_end: u32 = 0,\n_selection_direction: Selection.SelectionDirection = .none,\n\n_on_selectionchange: ?js.Function.Global = null,\n\npub fn getOnSelectionChange(self: *Input) ?js.Function.Global {\n    return self._on_selectionchange;\n}\n\npub fn setOnSelectionChange(self: *Input, listener: ?js.Function) !void {\n    if (listener) |listen| {\n        self._on_selectionchange = try listen.persistWithThis(self);\n    } else {\n        self._on_selectionchange = null;\n    }\n}\n\nfn dispatchSelectionChangeEvent(self: *Input, page: *Page) !void {\n    const event = try Event.init(\"selectionchange\", .{ .bubbles = true }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event);\n}\n\nfn dispatchInputEvent(self: *Input, data: ?[]const u8, input_type: []const u8, page: *Page) !void {\n    const event = try InputEvent.initTrusted(comptime .wrap(\"input\"), .{ .data = data, .inputType = input_type }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());\n}\n\npub fn asElement(self: *Input) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Input) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Input) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getType(self: *const Input) []const u8 {\n    return self._input_type.toString();\n}\n\npub fn setType(self: *Input, typ: []const u8, page: *Page) !void {\n    // Setting the type property should update the attribute, which will trigger attributeChange\n    const type_enum = Type.fromString(typ);\n    try self.asElement().setAttributeSafe(comptime .wrap(\"type\"), .wrap(type_enum.toString()), page);\n}\n\npub fn getValue(self: *const Input) []const u8 {\n    if (self._input_type == .file) return \"\";\n    return self._value orelse self._default_value orelse switch (self._input_type) {\n        .checkbox, .radio => \"on\",\n        else => \"\",\n    };\n}\n\npub fn setValue(self: *Input, value: []const u8, page: *Page) !void {\n    // File inputs: setting to empty string is a no-op, anything else throws\n    if (self._input_type == .file) {\n        if (value.len == 0) return;\n        return error.InvalidStateError;\n    }\n    // This should _not_ call setAttribute. It updates the current state only\n    self._value = try self.sanitizeValue(true, value, page);\n}\n\npub fn getDefaultValue(self: *const Input) []const u8 {\n    return self._default_value orelse \"\";\n}\n\npub fn setDefaultValue(self: *Input, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"value\"), .wrap(value), page);\n}\n\npub fn getChecked(self: *const Input) bool {\n    return self._checked;\n}\n\npub fn setChecked(self: *Input, checked: bool, page: *Page) !void {\n    // If checking a radio button, uncheck others in the group first\n    if (checked and self._input_type == .radio) {\n        try self.uncheckRadioGroup(page);\n    }\n    // This should _not_ call setAttribute. It updates the current state only\n    self._checked = checked;\n    self._checked_dirty = true;\n}\n\npub fn getIndeterminate(self: *const Input) bool {\n    return self._indeterminate;\n}\n\npub fn setIndeterminate(self: *Input, value: bool) !void {\n    self._indeterminate = value;\n}\n\npub fn getDefaultChecked(self: *const Input) bool {\n    return self._default_checked;\n}\n\npub fn setDefaultChecked(self: *Input, checked: bool, page: *Page) !void {\n    if (checked) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"checked\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"checked\"), page);\n    }\n}\n\npub fn getWillValidate(self: *const Input) bool {\n    // An input element is barred from constraint validation if:\n    // - type is hidden, button, or reset\n    // - element is disabled\n    // - element has a datalist ancestor\n    return switch (self._input_type) {\n        .hidden, .button, .reset => false,\n        else => !self.getDisabled() and !self.hasDatalistAncestor(),\n    };\n}\n\nfn hasDatalistAncestor(self: *const Input) bool {\n    var node = self.asConstElement().asConstNode().parentElement();\n    while (node) |parent| {\n        if (parent.is(HtmlElement.DataList) != null) return true;\n        node = parent.asConstNode().parentElement();\n    }\n    return false;\n}\n\npub fn getDisabled(self: *const Input) bool {\n    // TODO: Also check for disabled fieldset ancestors\n    // (but not if we're inside a <legend> of that fieldset)\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *Input, disabled: bool, page: *Page) !void {\n    if (disabled) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getName(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Input, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\npub fn getAccept(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"accept\")) orelse \"\";\n}\n\npub fn setAccept(self: *Input, accept: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"accept\"), .wrap(accept), page);\n}\n\npub fn getAlt(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"alt\")) orelse \"\";\n}\n\npub fn setAlt(self: *Input, alt: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"alt\"), .wrap(alt), page);\n}\n\npub fn getMaxLength(self: *const Input) i32 {\n    const attr = self.asConstElement().getAttributeSafe(comptime .wrap(\"maxlength\")) orelse return -1;\n    return std.fmt.parseInt(i32, attr, 10) catch -1;\n}\n\npub fn setMaxLength(self: *Input, max_length: i32, page: *Page) !void {\n    if (max_length < 0) {\n        return error.IndexSizeError;\n    }\n    var buf: [32]u8 = undefined;\n    const value = std.fmt.bufPrint(&buf, \"{d}\", .{max_length}) catch unreachable;\n    try self.asElement().setAttributeSafe(comptime .wrap(\"maxlength\"), .wrap(value), page);\n}\n\npub fn getSize(self: *const Input) i32 {\n    const attr = self.asConstElement().getAttributeSafe(comptime .wrap(\"size\")) orelse return 20;\n    const parsed = std.fmt.parseInt(i32, attr, 10) catch return 20;\n    return if (parsed == 0) 20 else parsed;\n}\n\npub fn setSize(self: *Input, size: i32, page: *Page) !void {\n    if (size == 0) {\n        return error.ZeroNotAllowed;\n    }\n    if (size < 0) {\n        return self.asElement().setAttributeSafe(comptime .wrap(\"size\"), .wrap(\"20\"), page);\n    }\n\n    var buf: [32]u8 = undefined;\n    const value = std.fmt.bufPrint(&buf, \"{d}\", .{size}) catch unreachable;\n    try self.asElement().setAttributeSafe(comptime .wrap(\"size\"), .wrap(value), page);\n}\n\npub fn getSrc(self: *const Input, page: *Page) ![]const u8 {\n    const src = self.asConstElement().getAttributeSafe(comptime .wrap(\"src\")) orelse return \"\";\n    // If attribute is explicitly set (even if empty), resolve it against the base URL\n    return @import(\"../../URL.zig\").resolve(page.call_arena, page.base(), src, .{});\n}\n\npub fn setSrc(self: *Input, src: []const u8, page: *Page) !void {\n    const trimmed = std.mem.trim(u8, src, &std.ascii.whitespace);\n    try self.asElement().setAttributeSafe(comptime .wrap(\"src\"), .wrap(trimmed), page);\n}\n\npub fn getReadonly(self: *const Input) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"readonly\")) != null;\n}\n\npub fn setReadonly(self: *Input, readonly: bool, page: *Page) !void {\n    if (readonly) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"readonly\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"readonly\"), page);\n    }\n}\n\npub fn getRequired(self: *const Input) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"required\")) != null;\n}\n\npub fn setRequired(self: *Input, required: bool, page: *Page) !void {\n    if (required) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"required\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"required\"), page);\n    }\n}\n\npub fn getPlaceholder(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"placeholder\")) orelse \"\";\n}\n\npub fn setPlaceholder(self: *Input, placeholder: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"placeholder\"), .wrap(placeholder), page);\n}\n\npub fn getMin(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"min\")) orelse \"\";\n}\n\npub fn setMin(self: *Input, min: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"min\"), .wrap(min), page);\n}\n\npub fn getMax(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"max\")) orelse \"\";\n}\n\npub fn setMax(self: *Input, max: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"max\"), .wrap(max), page);\n}\n\npub fn getStep(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"step\")) orelse \"\";\n}\n\npub fn setStep(self: *Input, step: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"step\"), .wrap(step), page);\n}\n\npub fn getMultiple(self: *const Input) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"multiple\")) != null;\n}\n\npub fn setMultiple(self: *Input, multiple: bool, page: *Page) !void {\n    if (multiple) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"multiple\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"multiple\"), page);\n    }\n}\n\npub fn getAutocomplete(self: *const Input) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"autocomplete\")) orelse \"\";\n}\n\npub fn setAutocomplete(self: *Input, autocomplete: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"autocomplete\"), .wrap(autocomplete), page);\n}\n\npub fn select(self: *Input, page: *Page) !void {\n    const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;\n    try self.setSelectionRange(0, len, null, page);\n    const event = try Event.init(\"select\", .{ .bubbles = true }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event);\n}\n\nfn selectionAvailable(self: *const Input) bool {\n    switch (self._input_type) {\n        .text, .search, .url, .tel, .password => return true,\n        else => return false,\n    }\n}\n\nconst HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };\n\nfn howSelected(self: *const Input) HowSelected {\n    if (!self.selectionAvailable()) return .none;\n    const value = self._value orelse return .none;\n\n    if (self._selection_start == self._selection_end) return .none;\n    if (self._selection_start == 0 and self._selection_end == value.len) return .full;\n    return .{ .partial = .{ self._selection_start, self._selection_end } };\n}\n\npub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {\n    const arena = page.arena;\n\n    switch (self.howSelected()) {\n        .full => {\n            // if the input is fully selected, replace the content.\n            const new_value = try arena.dupe(u8, str);\n            try self.setValue(new_value, page);\n            self._selection_start = @intCast(new_value.len);\n            self._selection_end = @intCast(new_value.len);\n            self._selection_direction = .none;\n            try self.dispatchSelectionChangeEvent(page);\n        },\n        .partial => |range| {\n            // if the input is partially selected, replace the selected content.\n            const current_value = self.getValue();\n            const before = current_value[0..range[0]];\n            const remaining = current_value[range[1]..];\n\n            const new_value = try std.mem.concat(\n                arena,\n                u8,\n                &.{ before, str, remaining },\n            );\n            try self.setValue(new_value, page);\n\n            const new_pos = range[0] + str.len;\n            self._selection_start = @intCast(new_pos);\n            self._selection_end = @intCast(new_pos);\n            self._selection_direction = .none;\n            try self.dispatchSelectionChangeEvent(page);\n        },\n        .none => {\n            // if the input is not selected, just insert at cursor.\n            const current_value = self.getValue();\n            const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });\n            try self.setValue(new_value, page);\n        },\n    }\n    try self.dispatchInputEvent(str, \"insertText\", page);\n}\n\npub fn getSelectionDirection(self: *const Input) []const u8 {\n    return @tagName(self._selection_direction);\n}\n\npub fn getSelectionStart(self: *const Input) !?u32 {\n    if (!self.selectionAvailable()) return null;\n    return self._selection_start;\n}\n\npub fn setSelectionStart(self: *Input, value: u32, page: *Page) !void {\n    if (!self.selectionAvailable()) return error.InvalidStateError;\n    self._selection_start = value;\n    try self.dispatchSelectionChangeEvent(page);\n}\n\npub fn getSelectionEnd(self: *const Input) !?u32 {\n    if (!self.selectionAvailable()) return null;\n    return self._selection_end;\n}\n\npub fn setSelectionEnd(self: *Input, value: u32, page: *Page) !void {\n    if (!self.selectionAvailable()) return error.InvalidStateError;\n    self._selection_end = value;\n    try self.dispatchSelectionChangeEvent(page);\n}\n\npub fn setSelectionRange(\n    self: *Input,\n    selection_start: u32,\n    selection_end: u32,\n    selection_dir: ?[]const u8,\n    page: *Page,\n) !void {\n    if (!self.selectionAvailable()) return error.InvalidStateError;\n\n    const direction = blk: {\n        if (selection_dir) |sd| {\n            break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;\n        } else break :blk .none;\n    };\n\n    const value = self._value orelse {\n        self._selection_start = 0;\n        self._selection_end = 0;\n        self._selection_direction = .none;\n        return;\n    };\n\n    const len_u32: u32 = @intCast(value.len);\n    var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;\n    const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;\n\n    // If end is less than start, both are equal to end.\n    if (end < start) {\n        start = end;\n    }\n\n    self._selection_direction = direction;\n    self._selection_start = start;\n    self._selection_end = end;\n\n    try self.dispatchSelectionChangeEvent(page);\n}\n\npub fn getForm(self: *Input, page: *Page) ?*Form {\n    const element = self.asElement();\n\n    // If form attribute exists, ONLY use that (even if it references nothing)\n    if (element.getAttributeSafe(comptime .wrap(\"form\"))) |form_id| {\n        if (page.document.getElementById(form_id, page)) |form_element| {\n            return form_element.is(Form);\n        }\n        // form attribute present but invalid - no form owner\n        return null;\n    }\n\n    // No form attribute - traverse ancestors looking for a <form>\n    var node = element.asNode()._parent;\n    while (node) |n| {\n        if (n.is(Element.Html.Form)) |form| {\n            return form;\n        }\n        node = n._parent;\n    }\n\n    return null;\n}\n\n/// Sanitize the value according to the current input type\nfn sanitizeValue(self: *Input, comptime dupe: bool, value: []const u8, page: *Page) ![]const u8 {\n    switch (self._input_type) {\n        .text, .search, .tel, .password, .url, .email => {\n            const sanitized = blk: {\n                const first = std.mem.indexOfAny(u8, value, \"\\r\\n\") orelse {\n                    break :blk if (comptime dupe) try page.dupeString(value) else value;\n                };\n\n                var result = try page.arena.alloc(u8, value.len);\n                @memcpy(result[0..first], value[0..first]);\n\n                var i: usize = first;\n                for (value[first + 1 ..]) |c| {\n                    if (c != '\\r' and c != '\\n') {\n                        result[i] = c;\n                        i += 1;\n                    }\n                }\n                break :blk result[0..i];\n            };\n\n            return switch (self._input_type) {\n                .url, .email => std.mem.trim(u8, sanitized, &std.ascii.whitespace),\n                else => sanitized,\n            };\n        },\n        .date => return if (isValidDate(value)) if (comptime dupe) try page.dupeString(value) else value else \"\",\n        .month => return if (isValidMonth(value)) if (comptime dupe) try page.dupeString(value) else value else \"\",\n        .week => return if (isValidWeek(value)) if (comptime dupe) try page.dupeString(value) else value else \"\",\n        .time => return if (isValidTime(value)) if (comptime dupe) try page.dupeString(value) else value else \"\",\n        .@\"datetime-local\" => return try sanitizeDatetimeLocal(dupe, value, page.arena),\n        .number => return if (isValidFloatingPoint(value)) if (comptime dupe) try page.dupeString(value) else value else \"\",\n        .range => return if (isValidFloatingPoint(value)) if (comptime dupe) try page.dupeString(value) else value else \"50\",\n        .color => {\n            if (value.len == 7 and value[0] == '#') {\n                var needs_lower = false;\n                for (value[1..]) |c| {\n                    if (!std.ascii.isHex(c)) {\n                        return \"#000000\";\n                    }\n                    if (c >= 'A' and c <= 'F') {\n                        needs_lower = true;\n                    }\n                }\n                if (!needs_lower) {\n                    return if (comptime dupe) try page.dupeString(value) else value;\n                }\n\n                // Normalize to lowercase per spec\n                const result = try page.arena.alloc(u8, 7);\n                result[0] = '#';\n                for (value[1..], 1..) |c, j| {\n                    result[j] = std.ascii.toLower(c);\n                }\n                return result;\n            }\n            return \"#000000\";\n        },\n        .file => return \"\", // File: always empty\n        .checkbox, .radio, .submit, .image, .reset, .button, .hidden => return if (comptime dupe) try page.dupeString(value) else value, // no sanitization\n    }\n}\n\n/// WHATWG \"valid floating-point number\" grammar check + overflow detection.\n/// Rejects \"+1\", \"1.\", \"Infinity\", \"NaN\", \"2e308\", leading whitespace, trailing junk.\nfn isValidFloatingPoint(value: []const u8) bool {\n    if (value.len == 0) return false;\n    var pos: usize = 0;\n\n    // Optional leading minus (no plus allowed)\n    if (value[pos] == '-') {\n        pos += 1;\n        if (pos >= value.len) return false;\n    }\n\n    // Must have one or both of: digit-sequence, dot+digit-sequence\n    var has_integer = false;\n    var has_decimal = false;\n\n    if (pos < value.len and std.ascii.isDigit(value[pos])) {\n        has_integer = true;\n        while (pos < value.len and std.ascii.isDigit(value[pos])) : (pos += 1) {}\n    }\n\n    if (pos < value.len and value[pos] == '.') {\n        pos += 1;\n        if (pos < value.len and std.ascii.isDigit(value[pos])) {\n            has_decimal = true;\n            while (pos < value.len and std.ascii.isDigit(value[pos])) : (pos += 1) {}\n        } else {\n            return false; // dot without trailing digits (\"1.\")\n        }\n    }\n\n    if (!has_integer and !has_decimal) return false;\n\n    // Optional exponent: (e|E) [+|-] digits\n    if (pos < value.len and (value[pos] == 'e' or value[pos] == 'E')) {\n        pos += 1;\n        if (pos >= value.len) return false;\n        if (value[pos] == '+' or value[pos] == '-') {\n            pos += 1;\n            if (pos >= value.len) return false;\n        }\n        if (!std.ascii.isDigit(value[pos])) return false;\n        while (pos < value.len and std.ascii.isDigit(value[pos])) : (pos += 1) {}\n    }\n\n    if (pos != value.len) return false; // trailing junk\n\n    // Grammar is valid; now check the parsed value doesn't overflow\n    const f = std.fmt.parseFloat(f64, value) catch return false;\n    return !std.math.isInf(f) and !std.math.isNan(f);\n}\n\n/// Validate a WHATWG \"valid date string\": YYYY-MM-DD\nfn isValidDate(value: []const u8) bool {\n    // Minimum: 4-digit year + \"-MM-DD\" = 10 chars\n    if (value.len < 10) return false;\n    const year_len = value.len - 6; // \"-MM-DD\" is always 6 chars from end\n    if (year_len < 4) return false;\n    if (value[year_len] != '-' or value[year_len + 3] != '-') return false;\n\n    const year = parseAllDigits(value[0..year_len]) orelse return false;\n    if (year == 0) return false;\n    const month = parseAllDigits(value[year_len + 1 .. year_len + 3]) orelse return false;\n    if (month < 1 or month > 12) return false;\n    const day = parseAllDigits(value[year_len + 4 .. year_len + 6]) orelse return false;\n    if (day < 1 or day > daysInMonth(@intCast(year), @intCast(month))) return false;\n    return true;\n}\n\n/// Validate a WHATWG \"valid month string\": YYYY-MM\nfn isValidMonth(value: []const u8) bool {\n    if (value.len < 7) return false;\n    const year_len = value.len - 3; // \"-MM\" is 3 chars from end\n    if (year_len < 4) return false;\n    if (value[year_len] != '-') return false;\n\n    const year = parseAllDigits(value[0..year_len]) orelse return false;\n    if (year == 0) return false;\n    const month = parseAllDigits(value[year_len + 1 .. year_len + 3]) orelse return false;\n    return month >= 1 and month <= 12;\n}\n\n/// Validate a WHATWG \"valid week string\": YYYY-Www\nfn isValidWeek(value: []const u8) bool {\n    if (value.len < 8) return false;\n    const year_len = value.len - 4; // \"-Www\" is 4 chars from end\n    if (year_len < 4) return false;\n    if (value[year_len] != '-' or value[year_len + 1] != 'W') return false;\n\n    const year = parseAllDigits(value[0..year_len]) orelse return false;\n    if (year == 0) return false;\n    const week = parseAllDigits(value[year_len + 2 .. year_len + 4]) orelse return false;\n    if (week < 1) return false;\n    return week <= maxWeeksInYear(@intCast(year));\n}\n\n/// Validate a WHATWG \"valid time string\": HH:MM[:SS[.s{1,3}]]\nfn isValidTime(value: []const u8) bool {\n    if (value.len < 5) return false;\n    if (value[2] != ':') return false;\n    const hour = parseAllDigits(value[0..2]) orelse return false;\n    if (hour > 23) return false;\n    const minute = parseAllDigits(value[3..5]) orelse return false;\n    if (minute > 59) return false;\n    if (value.len == 5) return true;\n\n    // Optional seconds\n    if (value.len < 8 or value[5] != ':') return false;\n    const second = parseAllDigits(value[6..8]) orelse return false;\n    if (second > 59) return false;\n    if (value.len == 8) return true;\n\n    // Optional fractional seconds: 1-3 digits\n    if (value[8] != '.') return false;\n    const frac_len = value.len - 9;\n    if (frac_len < 1 or frac_len > 3) return false;\n    for (value[9..]) |c| {\n        if (!std.ascii.isDigit(c)) return false;\n    }\n    return true;\n}\n\n/// Sanitize datetime-local: validate and normalize, or return \"\".\n/// Spec: if valid, normalize to \"YYYY-MM-DDThh:mm\" (shortest time form);\n/// otherwise set to \"\".\nfn sanitizeDatetimeLocal(comptime dupe: bool, value: []const u8, arena: std.mem.Allocator) ![]const u8 {\n    if (value.len < 16) {\n        return \"\";\n    }\n\n    // Find separator (T or space) by scanning for it before a valid time start\n    var sep_pos: ?usize = null;\n    if (value.len >= 16) {\n        for (0..value.len - 4) |i| {\n            if ((value[i] == 'T' or value[i] == ' ') and\n                i + 3 < value.len and\n                std.ascii.isDigit(value[i + 1]) and\n                std.ascii.isDigit(value[i + 2]) and\n                value[i + 3] == ':')\n            {\n                sep_pos = i;\n                break;\n            }\n        }\n    }\n    const sep = sep_pos orelse return \"\";\n\n    const date_part = value[0..sep];\n    const time_part = value[sep + 1 ..];\n    if (!isValidDate(date_part) or !isValidTime(time_part)) {\n        return \"\";\n    }\n\n    // Already normalized? (T separator and no trailing :00 or :00.000)\n    if (value[sep] == 'T' and time_part.len == 5) {\n        return if (comptime dupe) arena.dupe(u8, value) else value;\n    }\n\n    // Parse time components for normalization\n    const second: u32 = if (time_part.len >= 8) (parseAllDigits(time_part[6..8]) orelse return \"\") else 0;\n    var has_nonzero_frac = false;\n    var frac_end: usize = 0;\n    if (time_part.len > 9 and time_part[8] == '.') {\n        for (time_part[9..], 0..) |c, fi| {\n            if (c != '0') has_nonzero_frac = true;\n            frac_end = fi + 1;\n        }\n        // Strip trailing zeros from fractional part\n        while (frac_end > 0 and time_part[9 + frac_end - 1] == '0') : (frac_end -= 1) {}\n    }\n\n    // Build shortest time: HH:MM, or HH:MM:SS, or HH:MM:SS.fff\n    const need_seconds = second != 0 or has_nonzero_frac;\n    const time_len: usize = if (need_seconds) (if (frac_end > 0) 9 + frac_end else 8) else 5;\n    const total_len = date_part.len + 1 + time_len;\n\n    const result = try arena.alloc(u8, total_len);\n    @memcpy(result[0..date_part.len], date_part);\n    result[date_part.len] = 'T';\n    @memcpy(result[date_part.len + 1 ..][0..5], time_part[0..5]);\n\n    if (need_seconds) {\n        @memcpy(result[date_part.len + 6 ..][0..3], time_part[5..8]);\n        if (frac_end > 0) {\n            result[date_part.len + 9] = '.';\n            @memcpy(result[date_part.len + 10 ..][0..frac_end], time_part[9..][0..frac_end]);\n        }\n    }\n    return result[0..total_len];\n}\n\n/// Parse a slice that must be ALL ASCII digits into a u32. Returns null if any non-digit or empty.\nfn parseAllDigits(s: []const u8) ?u32 {\n    if (s.len == 0) return null;\n    var result: u32 = 0;\n    for (s) |c| {\n        if (!std.ascii.isDigit(c)) return null;\n        result = result *% 10 +% (c - '0');\n    }\n    return result;\n}\n\nfn isLeapYear(year: u32) bool {\n    return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0);\n}\n\nfn daysInMonth(year: u32, month: u32) u32 {\n    return switch (month) {\n        1, 3, 5, 7, 8, 10, 12 => 31,\n        4, 6, 9, 11 => 30,\n        2 => if (isLeapYear(year)) @as(u32, 29) else 28,\n        else => 0,\n    };\n}\n\n/// ISO 8601: a year has 53 weeks if Jan 1 is Thursday, or Jan 1 is Wednesday and leap year.\nfn maxWeeksInYear(year: u32) u32 {\n    // Gauss's algorithm for Jan 1 day-of-week\n    // dow: 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat\n    const y1 = year - 1;\n    const dow = (1 + 5 * (y1 % 4) + 4 * (y1 % 100) + 6 * (y1 % 400)) % 7;\n    if (dow == 4) return 53; // Jan 1 is Thursday\n    if (dow == 3 and isLeapYear(year)) return 53; // Jan 1 is Wednesday + leap year\n    return 52;\n}\n\nfn uncheckRadioGroup(self: *Input, page: *Page) !void {\n    const element = self.asElement();\n\n    const name = element.getAttributeSafe(comptime .wrap(\"name\")) orelse return;\n    if (name.len == 0) {\n        return;\n    }\n\n    const my_form = self.getForm(page);\n\n    // Walk from the root of the tree containing this element\n    // This handles both document-attached and orphaned elements\n    const root = element.asNode().getRootNode(null);\n\n    const TreeWalker = @import(\"../../TreeWalker.zig\");\n    var walker = TreeWalker.Full.init(root, .{});\n\n    while (walker.next()) |node| {\n        const other_element = node.is(Element) orelse continue;\n        const other_input = other_element.is(Input) orelse continue;\n\n        // Skip self\n        if (other_input == self) {\n            continue;\n        }\n\n        if (other_input._input_type != .radio) {\n            continue;\n        }\n\n        const other_name = other_element.getAttributeSafe(comptime .wrap(\"name\")) orelse continue;\n        if (!std.mem.eql(u8, name, other_name)) {\n            continue;\n        }\n\n        // Check if same form context\n        const other_form = other_input.getForm(page);\n        if (my_form == null and other_form == null) {\n            other_input._checked = false;\n            continue;\n        }\n\n        if (my_form) |mf| {\n            if (other_form) |of| {\n                if (mf == of) {\n                    other_input._checked = false;\n                }\n            }\n        }\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Input);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLInputElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    /// Handles [LegacyNullToEmptyString]: null → \"\" per HTML spec.\n    fn setValueFromJS(self: *Input, js_value: js.Value, page: *Page) !void {\n        if (js_value.isNull()) {\n            return self.setValue(\"\", page);\n        }\n        return self.setValue(try js_value.toZig([]const u8), page);\n    }\n\n    pub const onselectionchange = bridge.accessor(Input.getOnSelectionChange, Input.setOnSelectionChange, .{});\n    pub const @\"type\" = bridge.accessor(Input.getType, Input.setType, .{});\n    pub const value = bridge.accessor(Input.getValue, setValueFromJS, .{ .dom_exception = true });\n    pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{});\n    pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{});\n    pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{});\n    pub const disabled = bridge.accessor(Input.getDisabled, Input.setDisabled, .{});\n    pub const name = bridge.accessor(Input.getName, Input.setName, .{});\n    pub const required = bridge.accessor(Input.getRequired, Input.setRequired, .{});\n    pub const accept = bridge.accessor(Input.getAccept, Input.setAccept, .{});\n    pub const readOnly = bridge.accessor(Input.getReadonly, Input.setReadonly, .{});\n    pub const alt = bridge.accessor(Input.getAlt, Input.setAlt, .{});\n    pub const maxLength = bridge.accessor(Input.getMaxLength, Input.setMaxLength, .{ .dom_exception = true });\n    pub const size = bridge.accessor(Input.getSize, Input.setSize, .{});\n    pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{});\n    pub const form = bridge.accessor(Input.getForm, null, .{});\n    pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});\n    pub const placeholder = bridge.accessor(Input.getPlaceholder, Input.setPlaceholder, .{});\n    pub const min = bridge.accessor(Input.getMin, Input.setMin, .{});\n    pub const max = bridge.accessor(Input.getMax, Input.setMax, .{});\n    pub const step = bridge.accessor(Input.getStep, Input.setStep, .{});\n    pub const multiple = bridge.accessor(Input.getMultiple, Input.setMultiple, .{});\n    pub const autocomplete = bridge.accessor(Input.getAutocomplete, Input.setAutocomplete, .{});\n    pub const willValidate = bridge.accessor(Input.getWillValidate, null, .{});\n    pub const select = bridge.function(Input.select, .{});\n\n    pub const selectionStart = bridge.accessor(Input.getSelectionStart, Input.setSelectionStart, .{});\n    pub const selectionEnd = bridge.accessor(Input.getSelectionEnd, Input.setSelectionEnd, .{});\n    pub const selectionDirection = bridge.accessor(Input.getSelectionDirection, null, .{});\n    pub const setSelectionRange = bridge.function(Input.setSelectionRange, .{ .dom_exception = true });\n};\n\npub const Build = struct {\n    pub fn created(node: *Node, page: *Page) !void {\n        var self = node.as(Input);\n        const element = self.asElement();\n\n        // Store initial values from attributes\n        self._default_value = element.getAttributeSafe(comptime .wrap(\"value\"));\n        self._default_checked = element.getAttributeSafe(comptime .wrap(\"checked\")) != null;\n\n        self._checked = self._default_checked;\n\n        self._input_type = if (element.getAttributeSafe(comptime .wrap(\"type\"))) |type_attr|\n            Type.fromString(type_attr)\n        else\n            .text;\n\n        // Sanitize initial value per input type (e.g. date rejects \"invalid-date\").\n        if (self._default_value) |dv| {\n            self._value = try self.sanitizeValue(false, dv, page);\n        } else {\n            self._value = null;\n        }\n\n        // If this is a checked radio button, uncheck others in its group\n        if (self._checked and self._input_type == .radio) {\n            try self.uncheckRadioGroup(page);\n        }\n    }\n\n    pub fn attributeChange(element: *Element, name: String, value: String, page: *Page) !void {\n        const attribute = std.meta.stringToEnum(enum { type, value, checked }, name.str()) orelse return;\n        const self = element.as(Input);\n        switch (attribute) {\n            .type => {\n                self._input_type = Type.fromString(value.str());\n                // Sanitize the current value according to the new type\n                if (self._value) |current_value| {\n                    self._value = try self.sanitizeValue(false, current_value, page);\n                    // Apply default value for checkbox/radio if value is now empty\n                    if (self._value.?.len == 0 and (self._input_type == .checkbox or self._input_type == .radio)) {\n                        self._value = \"on\";\n                    }\n                }\n            },\n            .value => self._default_value = try page.arena.dupe(u8, value.str()),\n            .checked => {\n                self._default_checked = true;\n                // Only update checked state if it hasn't been manually modified\n                if (!self._checked_dirty) {\n                    self._checked = true;\n                    // If setting a radio button to checked, uncheck others in the group\n                    if (self._input_type == .radio) {\n                        try self.uncheckRadioGroup(page);\n                    }\n                }\n            },\n        }\n    }\n\n    pub fn attributeRemove(element: *Element, name: String, _: *Page) !void {\n        const attribute = std.meta.stringToEnum(enum { type, value, checked }, name.str()) orelse return;\n        const self = element.as(Input);\n        switch (attribute) {\n            .type => self._input_type = .text,\n            .value => self._default_value = null,\n            .checked => {\n                self._default_checked = false;\n                // Only update checked state if it hasn't been manually modified\n                if (!self._checked_dirty) {\n                    self._checked = false;\n                }\n            },\n        }\n    }\n\n    pub fn cloned(source_element: *Element, cloned_element: *Element, _: *Page) !void {\n        const source = source_element.as(Input);\n        const clone = cloned_element.as(Input);\n\n        // Copy runtime state from source to clone\n        clone._value = source._value;\n        clone._checked = source._checked;\n        clone._checked_dirty = source._checked_dirty;\n        clone._selection_direction = source._selection_direction;\n        clone._selection_start = source._selection_start;\n        clone._selection_end = source._selection_end;\n        clone._indeterminate = source._indeterminate;\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Input\" {\n    try testing.htmlRunner(\"element/html/input.html\", .{});\n    try testing.htmlRunner(\"element/html/input_click.html\", .{});\n    try testing.htmlRunner(\"element/html/input_radio.html\", .{});\n    try testing.htmlRunner(\"element/html/input-attrs.html\", .{});\n}\n\ntest \"isValidFloatingPoint\" {\n    // Valid\n    try testing.expect(isValidFloatingPoint(\"1\"));\n    try testing.expect(isValidFloatingPoint(\"0.5\"));\n    try testing.expect(isValidFloatingPoint(\"-1\"));\n    try testing.expect(isValidFloatingPoint(\"-0.5\"));\n    try testing.expect(isValidFloatingPoint(\"1e10\"));\n    try testing.expect(isValidFloatingPoint(\"1E10\"));\n    try testing.expect(isValidFloatingPoint(\"1e+10\"));\n    try testing.expect(isValidFloatingPoint(\"1e-10\"));\n    try testing.expect(isValidFloatingPoint(\"0.123\"));\n    try testing.expect(isValidFloatingPoint(\".5\"));\n    // Invalid\n    try testing.expect(!isValidFloatingPoint(\"\"));\n    try testing.expect(!isValidFloatingPoint(\"+1\"));\n    try testing.expect(!isValidFloatingPoint(\"1.\"));\n    try testing.expect(!isValidFloatingPoint(\"Infinity\"));\n    try testing.expect(!isValidFloatingPoint(\"NaN\"));\n    try testing.expect(!isValidFloatingPoint(\" 1\"));\n    try testing.expect(!isValidFloatingPoint(\"1 \"));\n    try testing.expect(!isValidFloatingPoint(\"1e\"));\n    try testing.expect(!isValidFloatingPoint(\"1e+\"));\n    try testing.expect(!isValidFloatingPoint(\"2e308\")); // overflow\n}\n\ntest \"isValidDate\" {\n    try testing.expect(isValidDate(\"2024-01-01\"));\n    try testing.expect(isValidDate(\"2024-02-29\")); // leap year\n    try testing.expect(isValidDate(\"2024-12-31\"));\n    try testing.expect(isValidDate(\"10000-01-01\")); // >4-digit year\n    try testing.expect(!isValidDate(\"2024-02-30\")); // invalid day\n    try testing.expect(!isValidDate(\"2023-02-29\")); // not leap year\n    try testing.expect(!isValidDate(\"2024-13-01\")); // invalid month\n    try testing.expect(!isValidDate(\"2024-00-01\")); // month 0\n    try testing.expect(!isValidDate(\"0000-01-01\")); // year 0\n    try testing.expect(!isValidDate(\"2024-1-01\")); // single-digit month\n    try testing.expect(!isValidDate(\"\"));\n    try testing.expect(!isValidDate(\"not-a-date\"));\n}\n\ntest \"isValidMonth\" {\n    try testing.expect(isValidMonth(\"2024-01\"));\n    try testing.expect(isValidMonth(\"2024-12\"));\n    try testing.expect(!isValidMonth(\"2024-00\"));\n    try testing.expect(!isValidMonth(\"2024-13\"));\n    try testing.expect(!isValidMonth(\"0000-01\"));\n    try testing.expect(!isValidMonth(\"\"));\n}\n\ntest \"isValidWeek\" {\n    try testing.expect(isValidWeek(\"2024-W01\"));\n    try testing.expect(isValidWeek(\"2024-W52\"));\n    try testing.expect(isValidWeek(\"2020-W53\")); // 2020 has 53 weeks\n    try testing.expect(!isValidWeek(\"2024-W00\"));\n    try testing.expect(!isValidWeek(\"2024-W54\"));\n    try testing.expect(!isValidWeek(\"0000-W01\"));\n    try testing.expect(!isValidWeek(\"\"));\n}\n\ntest \"isValidTime\" {\n    try testing.expect(isValidTime(\"00:00\"));\n    try testing.expect(isValidTime(\"23:59\"));\n    try testing.expect(isValidTime(\"12:30:45\"));\n    try testing.expect(isValidTime(\"12:30:45.1\"));\n    try testing.expect(isValidTime(\"12:30:45.12\"));\n    try testing.expect(isValidTime(\"12:30:45.123\"));\n    try testing.expect(!isValidTime(\"24:00\"));\n    try testing.expect(!isValidTime(\"12:60\"));\n    try testing.expect(!isValidTime(\"12:30:60\"));\n    try testing.expect(!isValidTime(\"12:30:45.1234\")); // >3 frac digits\n    try testing.expect(!isValidTime(\"12:30:45.\")); // dot without digits\n    try testing.expect(!isValidTime(\"\"));\n}\n\ntest \"sanitizeDatetimeLocal\" {\n    const allocator = testing.allocator;\n    // Already normalized — returns input slice, no allocation\n    try testing.expectEqual(\"2024-01-01T12:30\", try sanitizeDatetimeLocal(false, \"2024-01-01T12:30\", allocator));\n    // Space separator → T (allocates)\n    {\n        const result = try sanitizeDatetimeLocal(false, \"2024-01-01 12:30\", allocator);\n        try testing.expectEqual(\"2024-01-01T12:30\", result);\n        allocator.free(result);\n    }\n    // Strip trailing :00 (allocates)\n    {\n        const result = try sanitizeDatetimeLocal(false, \"2024-01-01T12:30:00\", allocator);\n        try testing.expectEqual(\"2024-01-01T12:30\", result);\n        allocator.free(result);\n    }\n    // Keep non-zero seconds (allocates)\n    {\n        const result = try sanitizeDatetimeLocal(false, \"2024-01-01T12:30:45\", allocator);\n        try testing.expectEqual(\"2024-01-01T12:30:45\", result);\n        allocator.free(result);\n    }\n    // Keep fractional seconds, strip trailing zeros (allocates)\n    {\n        const result = try sanitizeDatetimeLocal(false, \"2024-01-01T12:30:45.100\", allocator);\n        try testing.expectEqual(\"2024-01-01T12:30:45.1\", result);\n        allocator.free(result);\n    }\n    // Invalid → \"\" (no allocation)\n    try testing.expectEqual(\"\", try sanitizeDatetimeLocal(false, \"not-a-datetime\", allocator));\n    try testing.expectEqual(\"\", try sanitizeDatetimeLocal(false, \"\", allocator));\n}\n\ntest \"parseAllDigits\" {\n    try testing.expectEqual(@as(?u32, 0), parseAllDigits(\"0\"));\n    try testing.expectEqual(@as(?u32, 123), parseAllDigits(\"123\"));\n    try testing.expectEqual(@as(?u32, 2024), parseAllDigits(\"2024\"));\n    try testing.expectEqual(@as(?u32, null), parseAllDigits(\"\"));\n    try testing.expectEqual(@as(?u32, null), parseAllDigits(\"12a\"));\n    try testing.expectEqual(@as(?u32, null), parseAllDigits(\"abc\"));\n}\n\ntest \"daysInMonth\" {\n    try testing.expectEqual(@as(u32, 31), daysInMonth(2024, 1));\n    try testing.expectEqual(@as(u32, 29), daysInMonth(2024, 2)); // leap\n    try testing.expectEqual(@as(u32, 28), daysInMonth(2023, 2)); // non-leap\n    try testing.expectEqual(@as(u32, 30), daysInMonth(2024, 4));\n    try testing.expectEqual(@as(u32, 29), daysInMonth(2000, 2)); // century leap\n    try testing.expectEqual(@as(u32, 28), daysInMonth(1900, 2)); // century non-leap\n}\n\ntest \"maxWeeksInYear\" {\n    try testing.expectEqual(@as(u32, 52), maxWeeksInYear(2024));\n    try testing.expectEqual(@as(u32, 53), maxWeeksInYear(2020)); // Jan 1 = Wed + leap\n    try testing.expectEqual(@as(u32, 53), maxWeeksInYear(2015)); // Jan 1 = Thu\n    try testing.expectEqual(@as(u32, 52), maxWeeksInYear(2023));\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/LI.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst LI = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *LI) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *LI) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getValue(self: *LI) i32 {\n    const attr = self.asElement().getAttributeSafe(comptime .wrap(\"value\")) orelse return 0;\n    return std.fmt.parseInt(i32, attr, 10) catch 0;\n}\n\npub fn setValue(self: *LI, value: i32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"value\"), .wrap(str), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(LI);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLLIElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const value = bridge.accessor(LI.getValue, LI.setValue, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.LI\" {\n    try testing.htmlRunner(\"element/html/li.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Label.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst TreeWalker = @import(\"../../TreeWalker.zig\");\n\nconst Label = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Label) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Label) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getHtmlFor(self: *Label) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"for\")) orelse \"\";\n}\n\npub fn setHtmlFor(self: *Label, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"for\"), .wrap(value), page);\n}\n\npub fn getControl(self: *Label, page: *Page) ?*Element {\n    if (self.asElement().getAttributeSafe(comptime .wrap(\"for\"))) |id| {\n        const el = page.document.getElementById(id, page) orelse return null;\n        if (!isLabelable(el)) {\n            return null;\n        }\n        return el;\n    }\n\n    var tw = TreeWalker.FullExcludeSelf.Elements.init(self.asNode(), .{});\n    while (tw.next()) |el| {\n        if (isLabelable(el)) {\n            return el;\n        }\n    }\n    return null;\n}\n\nfn isLabelable(el: *Element) bool {\n    const html = el.is(HtmlElement) orelse return false;\n    return switch (html._type) {\n        .button, .meter, .output, .progress, .select, .textarea => true,\n        .input => |input| input._input_type != .hidden,\n        else => false,\n    };\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Label);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLLabelElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const htmlFor = bridge.accessor(Label.getHtmlFor, Label.setHtmlFor, .{});\n    pub const control = bridge.accessor(Label.getControl, null, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Label\" {\n    try testing.htmlRunner(\"element/html/label.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Legend.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Legend = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Legend) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Legend) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Legend);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLLegendElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Link.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst URL = @import(\"../../URL.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Link = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Link) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Link) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Link) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getHref(self: *Link, page: *Page) ![]const u8 {\n    const element = self.asElement();\n    const href = element.getAttributeSafe(comptime .wrap(\"href\")) orelse return \"\";\n    if (href.len == 0) {\n        return \"\";\n    }\n\n    // Always resolve the href against the page URL\n    return URL.resolve(page.call_arena, page.base(), href, .{ .encode = true });\n}\n\npub fn setHref(self: *Link, value: []const u8, page: *Page) !void {\n    const element = self.asElement();\n    try element.setAttributeSafe(comptime .wrap(\"href\"), .wrap(value), page);\n\n    if (element.asNode().isConnected()) {\n        try self.linkAddedCallback(page);\n    }\n}\n\npub fn getRel(self: *Link) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"rel\")) orelse return \"\";\n}\n\npub fn setRel(self: *Link, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"rel\"), .wrap(value), page);\n}\n\npub fn getAs(self: *const Link) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"as\")) orelse \"\";\n}\n\npub fn setAs(self: *Link, value: []const u8, page: *Page) !void {\n    return self.asElement().setAttributeSafe(comptime .wrap(\"as\"), .wrap(value), page);\n}\n\npub fn getCrossOrigin(self: *const Link) ?[]const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"crossOrigin\"));\n}\n\npub fn setCrossOrigin(self: *Link, value: []const u8, page: *Page) !void {\n    var normalized: []const u8 = \"anonymous\";\n    if (std.ascii.eqlIgnoreCase(value, \"use-credentials\")) {\n        normalized = \"use-credentials\";\n    }\n    return self.asElement().setAttributeSafe(comptime .wrap(\"crossOrigin\"), .wrap(normalized), page);\n}\n\npub fn linkAddedCallback(self: *Link, page: *Page) !void {\n    // if we're planning on navigating to another page, don't trigger load event.\n    if (page.isGoingAway()) {\n        return;\n    }\n\n    const element = self.asElement();\n\n    const rel = element.getAttributeSafe(comptime .wrap(\"rel\")) orelse return;\n    const loadable_rels = std.StaticStringMap(void).initComptime(.{\n        .{ \"stylesheet\", {} },\n        .{ \"preload\", {} },\n        .{ \"modulepreload\", {} },\n    });\n    if (loadable_rels.has(rel) == false) {\n        return;\n    }\n\n    const href = element.getAttributeSafe(comptime .wrap(\"href\")) orelse return;\n    if (href.len == 0) {\n        return;\n    }\n\n    try page._to_load.append(page.arena, self._proto);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Link);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLLinkElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const as = bridge.accessor(Link.getAs, Link.setAs, .{});\n    pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{});\n    pub const href = bridge.accessor(Link.getHref, Link.setHref, .{});\n    pub const crossOrigin = bridge.accessor(Link.getCrossOrigin, Link.setCrossOrigin, .{});\n    pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });\n\n    fn _getRelList(self: *Link, page: *Page) !?*@import(\"../../collections.zig\").DOMTokenList {\n        const element = self.asElement();\n        // relList is only valid for HTML <link> elements, not SVG or MathML\n        if (element._namespace != .html) {\n            return null;\n        }\n        return element.getRelList(page);\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Link\" {\n    try testing.htmlRunner(\"element/html/link.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Map.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Map = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Map) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Map) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Map);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLMapElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Media.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst Event = @import(\"../../Event.zig\");\npub const Audio = @import(\"Audio.zig\");\npub const Video = @import(\"Video.zig\");\nconst MediaError = @import(\"../../media/MediaError.zig\");\n\nconst Media = @This();\n\npub const ReadyState = enum(u16) {\n    HAVE_NOTHING = 0,\n    HAVE_METADATA = 1,\n    HAVE_CURRENT_DATA = 2,\n    HAVE_FUTURE_DATA = 3,\n    HAVE_ENOUGH_DATA = 4,\n};\n\npub const NetworkState = enum(u16) {\n    NETWORK_EMPTY = 0,\n    NETWORK_IDLE = 1,\n    NETWORK_LOADING = 2,\n    NETWORK_NO_SOURCE = 3,\n};\n\npub const Type = union(enum) {\n    generic,\n    audio: *Audio,\n    video: *Video,\n};\n\n_type: Type,\n_proto: *HtmlElement,\n_paused: bool = true,\n_current_time: f64 = 0,\n_volume: f64 = 1.0,\n_muted: bool = false,\n_playback_rate: f64 = 1.0,\n_ready_state: ReadyState = .HAVE_NOTHING,\n_network_state: NetworkState = .NETWORK_EMPTY,\n_error: ?*MediaError = null,\n\npub fn asElement(self: *Media) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Media) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Media) *Node {\n    return self.asElement().asNode();\n}\n\npub fn is(self: *Media, comptime T: type) ?*T {\n    const type_name = @typeName(T);\n    switch (self._type) {\n        .audio => |a| {\n            if (T == *Audio) return a;\n            if (comptime std.mem.startsWith(u8, type_name, \"browser.webapi.element.html.Audio\")) {\n                return a;\n            }\n        },\n        .video => |v| {\n            if (T == *Video) return v;\n            if (comptime std.mem.startsWith(u8, type_name, \"browser.webapi.element.html.Video\")) {\n                return v;\n            }\n        },\n        .generic => {},\n    }\n    return null;\n}\n\npub fn as(self: *Media, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn canPlayType(_: *const Media, mime_type: []const u8, page: *Page) []const u8 {\n    const pos = std.mem.indexOfScalar(u8, mime_type, ';') orelse mime_type.len;\n    const base_type = std.mem.trim(u8, mime_type[0..pos], &std.ascii.whitespace);\n\n    if (base_type.len > page.buf.len) {\n        return \"\";\n    }\n    const lower = std.ascii.lowerString(&page.buf, base_type);\n\n    if (isProbablySupported(lower)) {\n        return \"probably\";\n    }\n    if (isMaybeSupported(lower)) {\n        return \"maybe\";\n    }\n    return \"\";\n}\n\nfn isProbablySupported(mime_type: []const u8) bool {\n    if (std.mem.eql(u8, mime_type, \"video/mp4\")) return true;\n    if (std.mem.eql(u8, mime_type, \"video/webm\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/mp4\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/webm\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/mpeg\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/mp3\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/ogg\")) return true;\n    if (std.mem.eql(u8, mime_type, \"video/ogg\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/wav\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/wave\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/x-wav\")) return true;\n    return false;\n}\n\nfn isMaybeSupported(mime_type: []const u8) bool {\n    if (std.mem.eql(u8, mime_type, \"audio/aac\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/x-m4a\")) return true;\n    if (std.mem.eql(u8, mime_type, \"video/x-m4v\")) return true;\n    if (std.mem.eql(u8, mime_type, \"audio/flac\")) return true;\n    return false;\n}\n\npub fn play(self: *Media, page: *Page) !void {\n    const was_paused = self._paused;\n    self._paused = false;\n    self._ready_state = .HAVE_ENOUGH_DATA;\n    self._network_state = .NETWORK_IDLE;\n    if (was_paused) {\n        try self.dispatchEvent(\"play\", page);\n        try self.dispatchEvent(\"playing\", page);\n    }\n}\n\npub fn pause(self: *Media, page: *Page) !void {\n    if (!self._paused) {\n        self._paused = true;\n        try self.dispatchEvent(\"pause\", page);\n    }\n}\n\npub fn load(self: *Media, page: *Page) !void {\n    self._paused = true;\n    self._current_time = 0;\n    self._ready_state = .HAVE_NOTHING;\n    self._network_state = .NETWORK_LOADING;\n    self._error = null;\n    try self.dispatchEvent(\"emptied\", page);\n}\n\nfn dispatchEvent(self: *Media, name: []const u8, page: *Page) !void {\n    const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event);\n}\n\npub fn getPaused(self: *const Media) bool {\n    return self._paused;\n}\n\npub fn getCurrentTime(self: *const Media) f64 {\n    return self._current_time;\n}\n\npub fn getDuration(_: *const Media) f64 {\n    return std.math.nan(f64);\n}\n\npub fn getReadyState(self: *const Media) u16 {\n    return @intFromEnum(self._ready_state);\n}\n\npub fn getNetworkState(self: *const Media) u16 {\n    return @intFromEnum(self._network_state);\n}\n\npub fn getEnded(_: *const Media) bool {\n    return false;\n}\n\npub fn getSeeking(_: *const Media) bool {\n    return false;\n}\n\npub fn getError(self: *const Media) ?*MediaError {\n    return self._error;\n}\n\npub fn getVolume(self: *const Media) f64 {\n    return self._volume;\n}\n\npub fn setVolume(self: *Media, value: f64) void {\n    self._volume = @max(0.0, @min(1.0, value));\n}\n\npub fn getMuted(self: *const Media) bool {\n    return self._muted;\n}\n\npub fn setMuted(self: *Media, value: bool) void {\n    self._muted = value;\n}\n\npub fn getPlaybackRate(self: *const Media) f64 {\n    return self._playback_rate;\n}\n\npub fn setPlaybackRate(self: *Media, value: f64) void {\n    self._playback_rate = value;\n}\n\npub fn setCurrentTime(self: *Media, value: f64) void {\n    self._current_time = value;\n}\n\npub fn getSrc(self: *const Media, page: *Page) ![]const u8 {\n    const element = self.asConstElement();\n    const src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse return \"\";\n    if (src.len == 0) {\n        return \"\";\n    }\n    const URL = @import(\"../../URL.zig\");\n    return URL.resolve(page.call_arena, page.base(), src, .{ .encode = true });\n}\n\npub fn setSrc(self: *Media, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"src\"), .wrap(value), page);\n}\n\npub fn getAutoplay(self: *const Media) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"autoplay\")) != null;\n}\n\npub fn setAutoplay(self: *Media, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"autoplay\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"autoplay\"), page);\n    }\n}\n\npub fn getControls(self: *const Media) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"controls\")) != null;\n}\n\npub fn setControls(self: *Media, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"controls\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"controls\"), page);\n    }\n}\n\npub fn getLoop(self: *const Media) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"loop\")) != null;\n}\n\npub fn setLoop(self: *Media, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"loop\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"loop\"), page);\n    }\n}\n\npub fn getPreload(self: *const Media) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"preload\")) orelse \"auto\";\n}\n\npub fn setPreload(self: *Media, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"preload\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Media);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLMediaElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const NETWORK_EMPTY = bridge.property(@intFromEnum(NetworkState.NETWORK_EMPTY), .{ .template = true });\n    pub const NETWORK_IDLE = bridge.property(@intFromEnum(NetworkState.NETWORK_IDLE), .{ .template = true });\n    pub const NETWORK_LOADING = bridge.property(@intFromEnum(NetworkState.NETWORK_LOADING), .{ .template = true });\n    pub const NETWORK_NO_SOURCE = bridge.property(@intFromEnum(NetworkState.NETWORK_NO_SOURCE), .{ .template = true });\n\n    pub const HAVE_NOTHING = bridge.property(@intFromEnum(ReadyState.HAVE_NOTHING), .{ .template = true });\n    pub const HAVE_METADATA = bridge.property(@intFromEnum(ReadyState.HAVE_METADATA), .{ .template = true });\n    pub const HAVE_CURRENT_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_CURRENT_DATA), .{ .template = true });\n    pub const HAVE_FUTURE_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_FUTURE_DATA), .{ .template = true });\n    pub const HAVE_ENOUGH_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_ENOUGH_DATA), .{ .template = true });\n\n    pub const src = bridge.accessor(Media.getSrc, Media.setSrc, .{});\n    pub const autoplay = bridge.accessor(Media.getAutoplay, Media.setAutoplay, .{});\n    pub const controls = bridge.accessor(Media.getControls, Media.setControls, .{});\n    pub const loop = bridge.accessor(Media.getLoop, Media.setLoop, .{});\n    pub const muted = bridge.accessor(Media.getMuted, Media.setMuted, .{});\n    pub const preload = bridge.accessor(Media.getPreload, Media.setPreload, .{});\n    pub const volume = bridge.accessor(Media.getVolume, Media.setVolume, .{});\n    pub const playbackRate = bridge.accessor(Media.getPlaybackRate, Media.setPlaybackRate, .{});\n    pub const currentTime = bridge.accessor(Media.getCurrentTime, Media.setCurrentTime, .{});\n    pub const duration = bridge.accessor(Media.getDuration, null, .{});\n    pub const paused = bridge.accessor(Media.getPaused, null, .{});\n    pub const ended = bridge.accessor(Media.getEnded, null, .{});\n    pub const seeking = bridge.accessor(Media.getSeeking, null, .{});\n    pub const readyState = bridge.accessor(Media.getReadyState, null, .{});\n    pub const networkState = bridge.accessor(Media.getNetworkState, null, .{});\n    pub const @\"error\" = bridge.accessor(Media.getError, null, .{});\n\n    pub const canPlayType = bridge.function(Media.canPlayType, .{});\n    pub const play = bridge.function(Media.play, .{});\n    pub const pause = bridge.function(Media.pause, .{});\n    pub const load = bridge.function(Media.load, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: Media\" {\n    try testing.htmlRunner(\"element/html/media.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Meta.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Meta = @This();\n// Because we have a JsApi.Meta, \"Meta\" can be ambiguous in some scopes.\n// Create a different alias we can use when in such ambiguous cases.\nconst MetaElement = Meta;\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Meta) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Meta) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getName(self: *Meta) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"name\")) orelse return \"\";\n}\n\npub fn setName(self: *Meta, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(value), page);\n}\n\npub fn getHttpEquiv(self: *Meta) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"http-equiv\")) orelse return \"\";\n}\n\npub fn setHttpEquiv(self: *Meta, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"http-equiv\"), .wrap(value), page);\n}\n\npub fn getContent(self: *Meta) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"content\")) orelse return \"\";\n}\n\npub fn setContent(self: *Meta, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"content\"), .wrap(value), page);\n}\n\npub fn getMedia(self: *Meta) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"media\")) orelse return \"\";\n}\n\npub fn setMedia(self: *Meta, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"media\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MetaElement);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLMetaElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const name = bridge.accessor(MetaElement.getName, MetaElement.setName, .{});\n    pub const httpEquiv = bridge.accessor(MetaElement.getHttpEquiv, MetaElement.setHttpEquiv, .{});\n    pub const content = bridge.accessor(MetaElement.getContent, MetaElement.setContent, .{});\n    pub const media = bridge.accessor(MetaElement.getMedia, MetaElement.setMedia, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Meter.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Meter = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Meter) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Meter) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Meter);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLMeterElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Mod.zig",
    "content": "const String = @import(\"../../../../string.zig\").String;\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Mod = @This();\n\n_tag_name: String,\n_tag: Element.Tag,\n_proto: *HtmlElement,\n\npub fn asElement(self: *Mod) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Mod) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Mod);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLModElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/OL.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst OL = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *OL) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *OL) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getStart(self: *OL) i32 {\n    const attr = self.asElement().getAttributeSafe(comptime .wrap(\"start\")) orelse return 1;\n    return std.fmt.parseInt(i32, attr, 10) catch 1;\n}\n\npub fn setStart(self: *OL, value: i32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"start\"), .wrap(str), page);\n}\n\npub fn getReversed(self: *OL) bool {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"reversed\")) != null;\n}\n\npub fn setReversed(self: *OL, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"reversed\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"reversed\"), page);\n    }\n}\n\npub fn getType(self: *OL) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"type\")) orelse \"1\";\n}\n\npub fn setType(self: *OL, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"type\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(OL);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLOListElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const start = bridge.accessor(OL.getStart, OL.setStart, .{});\n    pub const reversed = bridge.accessor(OL.getReversed, OL.setReversed, .{});\n    pub const @\"type\" = bridge.accessor(OL.getType, OL.setType, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.OL\" {\n    try testing.htmlRunner(\"element/html/ol.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Object.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Object = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Object) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Object) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Object);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLObjectElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/OptGroup.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst OptGroup = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *OptGroup) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *OptGroup) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getDisabled(self: *OptGroup) bool {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *OptGroup, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getLabel(self: *OptGroup) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"label\")) orelse \"\";\n}\n\npub fn setLabel(self: *OptGroup, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"label\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(OptGroup);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLOptGroupElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const disabled = bridge.accessor(OptGroup.getDisabled, OptGroup.setDisabled, .{});\n    pub const label = bridge.accessor(OptGroup.getLabel, OptGroup.setLabel, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.OptGroup\" {\n    try testing.htmlRunner(\"element/html/optgroup.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Option.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Option = @This();\n\n_proto: *HtmlElement,\n_value: ?[]const u8 = null,\n_selected: bool = false,\n_default_selected: bool = false,\n_disabled: bool = false,\n\npub fn asElement(self: *Option) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Option) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Option) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getValue(self: *Option, page: *Page) []const u8 {\n    // If value attribute exists, use that; otherwise use text content (stripped)\n    if (self._value) |v| {\n        return v;\n    }\n\n    const node = self.asNode();\n    const text = node.getTextContentAlloc(page.call_arena) catch return \"\";\n    return std.mem.trim(u8, text, &std.ascii.whitespace);\n}\n\npub fn setValue(self: *Option, value: []const u8, page: *Page) !void {\n    const owned = try page.dupeString(value);\n    try self.asElement().setAttributeSafe(comptime .wrap(\"value\"), .wrap(owned), page);\n    self._value = owned;\n}\n\npub fn getText(self: *const Option, page: *Page) []const u8 {\n    const node: *Node = @constCast(self.asConstElement().asConstNode());\n    return node.getTextContentAlloc(page.call_arena) catch \"\";\n}\n\npub fn setText(self: *Option, value: []const u8, page: *Page) !void {\n    try self.asNode().setTextContent(value, page);\n}\n\npub fn getSelected(self: *const Option) bool {\n    return self._selected;\n}\n\npub fn setSelected(self: *Option, selected: bool, page: *Page) !void {\n    // TODO: When setting selected=true, may need to unselect other options\n    // in the parent <select> if it doesn't have multiple attribute\n    self._selected = selected;\n    page.domChanged();\n}\n\npub fn getDefaultSelected(self: *const Option) bool {\n    return self._default_selected;\n}\n\npub fn getDisabled(self: *const Option) bool {\n    return self._disabled;\n}\n\npub fn setDisabled(self: *Option, disabled: bool, page: *Page) !void {\n    self._disabled = disabled;\n    if (disabled) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getName(self: *const Option) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Option, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Option);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLOptionElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const value = bridge.accessor(Option.getValue, Option.setValue, .{});\n    pub const text = bridge.accessor(Option.getText, Option.setText, .{});\n    pub const selected = bridge.accessor(Option.getSelected, Option.setSelected, .{});\n    pub const defaultSelected = bridge.accessor(Option.getDefaultSelected, null, .{});\n    pub const disabled = bridge.accessor(Option.getDisabled, Option.setDisabled, .{});\n    pub const name = bridge.accessor(Option.getName, Option.setName, .{});\n};\n\npub const Build = struct {\n    pub fn created(node: *Node, _: *Page) !void {\n        var self = node.as(Option);\n        const element = self.asElement();\n\n        // Check for value attribute\n        self._value = element.getAttributeSafe(comptime .wrap(\"value\"));\n\n        // Check for selected attribute\n        self._default_selected = element.getAttributeSafe(comptime .wrap(\"selected\")) != null;\n        self._selected = self._default_selected;\n\n        // Check for disabled attribute\n        self._disabled = element.getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n    }\n\n    pub fn attributeChange(element: *Element, name: String, value: String, _: *Page) !void {\n        const attribute = std.meta.stringToEnum(enum { value, selected }, name.str()) orelse return;\n        const self = element.as(Option);\n        switch (attribute) {\n            .value => self._value = value.str(),\n            .selected => {\n                self._default_selected = true;\n                self._selected = true;\n            },\n        }\n    }\n\n    pub fn attributeRemove(element: *Element, name: String, _: *Page) !void {\n        const attribute = std.meta.stringToEnum(enum { value, selected }, name.str()) orelse return;\n        const self = element.as(Option);\n        switch (attribute) {\n            .value => self._value = null,\n            .selected => {\n                self._default_selected = false;\n                self._selected = false;\n            },\n        }\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Option\" {\n    try testing.htmlRunner(\"element/html/option.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Output.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Output = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Output) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Output) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Output);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLOutputElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Paragraph.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Paragraph = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *Paragraph) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Paragraph) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Paragraph);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLParagraphElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Param.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Param = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Param) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Param) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Param);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLParamElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Picture.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Picture = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Picture) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Picture) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Picture);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLPictureElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: Picture\" {\n    try testing.htmlRunner(\"element/html/picture.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Pre.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Pre = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Pre) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Pre) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Pre);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLPreElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Progress.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Progress = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Progress) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Progress) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Progress);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLProgressElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Quote.zig",
    "content": "const String = @import(\"../../../../string.zig\").String;\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Quote = @This();\n\n_tag_name: String,\n_tag: Element.Tag,\n_proto: *HtmlElement,\n\npub fn asElement(self: *Quote) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Quote) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getCite(self: *Quote, page: *Page) ![]const u8 {\n    const attr = self.asElement().getAttributeSafe(comptime .wrap(\"cite\")) orelse return \"\";\n    if (attr.len == 0) return \"\";\n    const URL = @import(\"../../URL.zig\");\n    return URL.resolve(page.call_arena, page.base(), attr, .{});\n}\n\npub fn setCite(self: *Quote, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"cite\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Quote);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLQuoteElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const cite = bridge.accessor(Quote.getCite, Quote.setCite, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Quote\" {\n    try testing.htmlRunner(\"element/html/quote.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Script.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\nconst std = @import(\"std\");\n\nconst log = @import(\"../../../../log.zig\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst URL = @import(\"../../URL.zig\");\n\nconst Script = @This();\n\n_proto: *HtmlElement,\n_src: []const u8 = \"\",\n_executed: bool = false,\n// dynamic scripts are forced to be async by default\n_force_async: bool = true,\n\npub fn asElement(self: *Script) *Element {\n    return self._proto._proto;\n}\n\npub fn asConstElement(self: *const Script) *const Element {\n    return self._proto._proto;\n}\n\npub fn asNode(self: *Script) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getSrc(self: *const Script, page: *Page) ![]const u8 {\n    if (self._src.len == 0) return \"\";\n    return try URL.resolve(page.call_arena, page.base(), self._src, .{ .encode = true });\n}\n\npub fn setSrc(self: *Script, src: []const u8, page: *Page) !void {\n    const element = self.asElement();\n    try element.setAttributeSafe(comptime .wrap(\"src\"), .wrap(src), page);\n    self._src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse unreachable;\n    if (element.asNode().isConnected()) {\n        try page.scriptAddedCallback(false, self);\n    }\n}\n\npub fn getType(self: *const Script) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"type\")) orelse \"\";\n}\n\npub fn setType(self: *Script, value: []const u8, page: *Page) !void {\n    return self.asElement().setAttributeSafe(comptime .wrap(\"type\"), .wrap(value), page);\n}\n\npub fn getNonce(self: *const Script) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"nonce\")) orelse \"\";\n}\n\npub fn setNonce(self: *Script, value: []const u8, page: *Page) !void {\n    return self.asElement().setAttributeSafe(comptime .wrap(\"nonce\"), .wrap(value), page);\n}\n\npub fn getCharset(self: *const Script) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"charset\")) orelse \"\";\n}\n\npub fn setCharset(self: *Script, value: []const u8, page: *Page) !void {\n    return self.asElement().setAttributeSafe(comptime .wrap(\"charset\"), .wrap(value), page);\n}\n\npub fn getAsync(self: *const Script) bool {\n    return self._force_async or self.asConstElement().getAttributeSafe(comptime .wrap(\"async\")) != null;\n}\n\npub fn setAsync(self: *Script, value: bool, page: *Page) !void {\n    self._force_async = false;\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"async\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"async\"), page);\n    }\n}\n\npub fn getDefer(self: *const Script) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"defer\")) != null;\n}\n\npub fn setDefer(self: *Script, value: bool, page: *Page) !void {\n    if (value) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"defer\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"defer\"), page);\n    }\n}\n\npub fn getNoModule(self: *const Script) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"nomodule\")) != null;\n}\n\npub fn setInnerText(self: *Script, text: []const u8, page: *Page) !void {\n    try self.asNode().setTextContent(text, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Script);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLScriptElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{});\n    pub const @\"defer\" = bridge.accessor(Script.getDefer, Script.setDefer, .{});\n    pub const async = bridge.accessor(Script.getAsync, Script.setAsync, .{});\n    pub const @\"type\" = bridge.accessor(Script.getType, Script.setType, .{});\n    pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{});\n    pub const charset = bridge.accessor(Script.getCharset, Script.setCharset, .{});\n    pub const noModule = bridge.accessor(Script.getNoModule, null, .{});\n    pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{});\n    fn _innerText(self: *Script, page: *const Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.asNode().getTextContent(&buf.writer);\n        return buf.written();\n    }\n    pub const text = bridge.accessor(_text, Script.setInnerText, .{});\n    fn _text(self: *Script, page: *const Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.asNode().getChildTextContent(&buf.writer);\n        return buf.written();\n    }\n};\n\npub const Build = struct {\n    pub fn complete(node: *Node, _: *Page) !void {\n        const self = node.as(Script);\n        const element = self.asElement();\n        self._src = element.getAttributeSafe(comptime .wrap(\"src\")) orelse \"\";\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: Script\" {\n    try testing.htmlRunner(\"element/html/script\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Select.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst collections = @import(\"../../collections.zig\");\nconst Form = @import(\"Form.zig\");\npub const Option = @import(\"Option.zig\");\n\nconst Select = @This();\n\n_proto: *HtmlElement,\n_selected_index_set: bool = false,\n\npub fn asElement(self: *Select) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Select) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Select) *Node {\n    return self.asElement().asNode();\n}\npub fn asConstNode(self: *const Select) *const Node {\n    return self.asConstElement().asConstNode();\n}\n\npub fn getValue(self: *Select, page: *Page) []const u8 {\n    // Return value of first selected option, or first option if none selected\n    var first_option: ?*Option = null;\n    var iter = self.asNode().childrenIterator();\n    while (iter.next()) |child| {\n        const option = child.is(Option) orelse continue;\n        if (option.getDisabled()) {\n            continue;\n        }\n\n        if (option.getSelected()) {\n            return option.getValue(page);\n        }\n        if (first_option == null) {\n            first_option = option;\n        }\n    }\n    // No explicitly selected option, return first option's value\n    if (first_option) |opt| {\n        return opt.getValue(page);\n    }\n    return \"\";\n}\n\npub fn setValue(self: *Select, value: []const u8, page: *Page) !void {\n    // Find option with matching value and select it\n    // Note: This updates the current state (_selected), not the default state (attribute)\n    // Setting value always deselects all others, even for multiple selects\n    var iter = self.asNode().childrenIterator();\n    while (iter.next()) |child| {\n        const option = child.is(Option) orelse continue;\n        option._selected = std.mem.eql(u8, option.getValue(page), value);\n    }\n}\n\npub fn getSelectedIndex(self: *Select) i32 {\n    var index: i32 = 0;\n    var has_options = false;\n    var iter = self.asNode().childrenIterator();\n    while (iter.next()) |child| {\n        const option = child.is(Option) orelse continue;\n        has_options = true;\n        if (option.getSelected()) {\n            return index;\n        }\n        index += 1;\n    }\n    // If selectedIndex was explicitly set and no option is selected, return -1\n    // If selectedIndex was never set, return 0 (first option implicitly selected) if we have options\n    if (self._selected_index_set) {\n        return -1;\n    }\n    return if (has_options) 0 else -1;\n}\n\npub fn setSelectedIndex(self: *Select, index: i32) !void {\n    // Mark that selectedIndex has been explicitly set\n    self._selected_index_set = true;\n\n    // Select option at given index\n    // Note: This updates the current state (_selected), not the default state (attribute)\n    const is_multiple = self.getMultiple();\n    var current_index: i32 = 0;\n    var iter = self.asNode().childrenIterator();\n    while (iter.next()) |child| {\n        const option = child.is(Option) orelse continue;\n        if (current_index == index) {\n            option._selected = true;\n        } else if (!is_multiple) {\n            // Only deselect others if not multiple\n            option._selected = false;\n        }\n        current_index += 1;\n    }\n}\n\npub fn getMultiple(self: *const Select) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"multiple\")) != null;\n}\n\npub fn setMultiple(self: *Select, multiple: bool, page: *Page) !void {\n    if (multiple) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"multiple\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"multiple\"), page);\n    }\n}\n\npub fn getDisabled(self: *const Select) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *Select, disabled: bool, page: *Page) !void {\n    if (disabled) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getName(self: *const Select) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Select, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\npub fn getSize(self: *const Select) u32 {\n    const s = self.asConstElement().getAttributeSafe(comptime .wrap(\"size\")) orelse return 0;\n\n    const trimmed = std.mem.trimLeft(u8, s, &std.ascii.whitespace);\n\n    var end: usize = 0;\n    for (trimmed) |b| {\n        if (!std.ascii.isDigit(b)) {\n            break;\n        }\n        end += 1;\n    }\n    if (end == 0) {\n        return 0;\n    }\n    return std.fmt.parseInt(u32, trimmed[0..end], 10) catch 0;\n}\n\npub fn setSize(self: *Select, size: u32, page: *Page) !void {\n    const size_string = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{size});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"size\"), .wrap(size_string), page);\n}\n\npub fn getRequired(self: *const Select) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"required\")) != null;\n}\n\npub fn setRequired(self: *Select, required: bool, page: *Page) !void {\n    if (required) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"required\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"required\"), page);\n    }\n}\n\npub fn getOptions(self: *Select, page: *Page) !*collections.HTMLOptionsCollection {\n    // For options, we use the child_tag mode to filter only <option> elements\n    const node_live = collections.NodeLive(.child_tag).init(self.asNode(), .option, page);\n    const html_collection = try node_live.runtimeGenericWrap(page);\n\n    // Create and return HTMLOptionsCollection\n    return page._factory.create(collections.HTMLOptionsCollection{\n        ._proto = html_collection,\n        ._select = self,\n    });\n}\n\npub fn getLength(self: *Select) u32 {\n    var i: u32 = 0;\n    var it = self.asNode().childrenIterator();\n    while (it.next()) |child| {\n        if (child.is(Option) != null) {\n            i += 1;\n        }\n    }\n    return i;\n}\n\npub fn getSelectedOptions(self: *Select, page: *Page) !collections.NodeLive(.selected_options) {\n    return collections.NodeLive(.selected_options).init(self.asNode(), {}, page);\n}\n\npub fn getForm(self: *Select, page: *Page) ?*Form {\n    const element = self.asElement();\n\n    // If form attribute exists, ONLY use that (even if it references nothing)\n    if (element.getAttributeSafe(comptime .wrap(\"form\"))) |form_id| {\n        if (page.document.getElementById(form_id, page)) |form_element| {\n            return form_element.is(Form);\n        }\n        // form attribute present but invalid - no form owner\n        return null;\n    }\n\n    // No form attribute - traverse ancestors looking for a <form>\n    var node = element.asNode()._parent;\n    while (node) |n| {\n        if (n.is(Element.Html.Form)) |form| {\n            return form;\n        }\n        node = n._parent;\n    }\n\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Select);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLSelectElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const value = bridge.accessor(Select.getValue, Select.setValue, .{});\n    pub const selectedIndex = bridge.accessor(Select.getSelectedIndex, Select.setSelectedIndex, .{});\n    pub const multiple = bridge.accessor(Select.getMultiple, Select.setMultiple, .{});\n    pub const disabled = bridge.accessor(Select.getDisabled, Select.setDisabled, .{});\n    pub const name = bridge.accessor(Select.getName, Select.setName, .{});\n    pub const required = bridge.accessor(Select.getRequired, Select.setRequired, .{});\n    pub const options = bridge.accessor(Select.getOptions, null, .{});\n    pub const selectedOptions = bridge.accessor(Select.getSelectedOptions, null, .{});\n    pub const form = bridge.accessor(Select.getForm, null, .{});\n    pub const size = bridge.accessor(Select.getSize, Select.setSize, .{});\n    pub const length = bridge.accessor(Select.getLength, null, .{});\n};\n\npub const Build = struct {\n    pub fn created(_: *Node, _: *Page) !void {\n        // No initialization needed - disabled is lazy from attribute\n    }\n};\n\nconst std = @import(\"std\");\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Select\" {\n    try testing.htmlRunner(\"element/html/select.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Slot.zig",
    "content": "const std = @import(\"std\");\n\nconst log = @import(\"../../../../log.zig\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst ShadowRoot = @import(\"../../ShadowRoot.zig\");\n\nconst Slot = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Slot) *Element {\n    return self._proto._proto;\n}\n\npub fn asConstElement(self: *const Slot) *const Element {\n    return self._proto._proto;\n}\n\npub fn asNode(self: *Slot) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getName(self: *const Slot) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *Slot, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\nconst AssignedNodesOptions = struct {\n    flatten: bool = false,\n};\n\npub fn assignedNodes(self: *Slot, opts_: ?AssignedNodesOptions, page: *Page) ![]const *Node {\n    const opts = opts_ orelse AssignedNodesOptions{};\n    var nodes: std.ArrayList(*Node) = .empty;\n    try self.collectAssignedNodes(false, &nodes, opts, page);\n    return nodes.items;\n}\n\npub fn assignedElements(self: *Slot, opts_: ?AssignedNodesOptions, page: *Page) ![]const *Element {\n    const opts = opts_ orelse AssignedNodesOptions{};\n    var elements: std.ArrayList(*Element) = .empty;\n    try self.collectAssignedNodes(true, &elements, opts, page);\n    return elements.items;\n}\n\nfn CollectionType(comptime elements: bool) type {\n    return if (elements) *std.ArrayList(*Element) else *std.ArrayList(*Node);\n}\n\nfn collectAssignedNodes(self: *Slot, comptime elements: bool, coll: CollectionType(elements), opts: AssignedNodesOptions, page: *Page) !void {\n    // Find the shadow root this slot belongs to\n    const shadow_root = self.findShadowRoot() orelse return;\n\n    const slot_name = self.getName();\n    const allocator = page.call_arena;\n\n    const host = shadow_root.getHost();\n    const initial_count = coll.items.len;\n    var it = host.asNode().childrenIterator();\n    while (it.next()) |child| {\n        if (!isAssignedToSlot(child, slot_name)) {\n            continue;\n        }\n\n        if (opts.flatten) {\n            if (child.is(Slot)) |child_slot| {\n                // Only flatten if the child slot is actually in a shadow tree\n                if (child_slot.findShadowRoot()) |_| {\n                    try child_slot.collectAssignedNodes(elements, coll, opts, page);\n                    continue;\n                }\n                // Otherwise, treat it as a regular element and fall through\n            }\n        }\n\n        if (comptime elements) {\n            if (child.is(Element)) |el| {\n                try coll.append(allocator, el);\n            }\n        } else {\n            try coll.append(allocator, child);\n        }\n    }\n\n    // If flatten is true and no assigned nodes were found, return fallback content\n    if (opts.flatten and coll.items.len == initial_count) {\n        var child_it = self.asNode().childrenIterator();\n        while (child_it.next()) |child| {\n            if (comptime elements) {\n                if (child.is(Element)) |el| {\n                    try coll.append(allocator, el);\n                }\n            } else {\n                try coll.append(allocator, child);\n            }\n        }\n    }\n}\n\npub fn assign(self: *Slot, nodes: []const *Node) void {\n    // Imperative slot assignment API\n    // This would require storing manually assigned nodes\n    // For now, this is a placeholder for the API\n    _ = self;\n    _ = nodes;\n\n    // let's see if this is ever actually used\n    log.warn(.not_implemented, \"Slot.assign\", .{});\n}\n\nfn findShadowRoot(self: *Slot) ?*ShadowRoot {\n    // Walk up the parent chain to find the shadow root\n    var parent = self.asNode()._parent;\n    while (parent) |p| {\n        if (p.is(ShadowRoot)) |shadow_root| {\n            return shadow_root;\n        }\n        parent = p._parent;\n    }\n    return null;\n}\n\nfn isAssignedToSlot(node: *Node, slot_name: []const u8) bool {\n    // Check if a node should be assigned to a slot with the given name\n    if (node.is(Element)) |element| {\n        // Get the slot attribute from the element\n        const node_slot = element.getAttributeSafe(comptime .wrap(\"slot\")) orelse \"\";\n\n        // Match if:\n        // - Both are empty (default slot)\n        // - They match exactly\n        return std.mem.eql(u8, node_slot, slot_name);\n    }\n\n    // Text nodes, comments, etc. are only assigned to the default slot\n    // (when they have no preceding/following element siblings with slot attributes)\n    // For simplicity, text nodes go to default slot if slot_name is empty\n    return slot_name.len == 0;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Slot);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLSlotElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const name = bridge.accessor(Slot.getName, Slot.setName, .{});\n    pub const assignedNodes = bridge.function(Slot.assignedNodes, .{});\n    pub const assignedElements = bridge.function(Slot.assignedElements, .{});\n    pub const assign = bridge.function(Slot.assign, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTMLSlotElement\" {\n    try testing.htmlRunner(\"element/html/slot.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Source.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Source = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Source) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Source) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Source);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLSourceElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Span.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Span = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Span) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Span) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Span);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLSpanElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Style.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Style = @This();\n_proto: *HtmlElement,\n_sheet: ?*CSSStyleSheet = null,\n\npub fn asElement(self: *Style) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const Style) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Style) *Node {\n    return self.asElement().asNode();\n}\n\n// Attribute-backed properties\n\npub fn getBlocking(self: *const Style) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"blocking\")) orelse \"\";\n}\n\npub fn setBlocking(self: *Style, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"blocking\"), .wrap(value), page);\n}\n\npub fn getMedia(self: *const Style) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"media\")) orelse \"\";\n}\n\npub fn setMedia(self: *Style, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"media\"), .wrap(value), page);\n}\n\npub fn getType(self: *const Style) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"type\")) orelse \"text/css\";\n}\n\npub fn setType(self: *Style, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"type\"), .wrap(value), page);\n}\n\npub fn getDisabled(self: *const Style) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *Style, disabled: bool, page: *Page) !void {\n    if (disabled) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\nconst CSSStyleSheet = @import(\"../../css/CSSStyleSheet.zig\");\npub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {\n    // Per spec, sheet is null for disconnected elements or non-CSS types.\n    // Valid types: absent (defaults to \"text/css\"), empty string, or\n    // case-insensitive match for \"text/css\".\n    if (!self.asNode().isConnected()) {\n        self._sheet = null;\n        return null;\n    }\n    const t = self.getType();\n    if (t.len != 0 and !std.ascii.eqlIgnoreCase(t, \"text/css\")) {\n        self._sheet = null;\n        return null;\n    }\n\n    if (self._sheet) |sheet| return sheet;\n    const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);\n    self._sheet = sheet;\n    return sheet;\n}\n\npub fn styleAddedCallback(self: *Style, page: *Page) !void {\n    // if we're planning on navigating to another page, don't trigger load event.\n    if (page.isGoingAway()) {\n        return;\n    }\n\n    try page._to_load.append(page.arena, self._proto);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Style);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLStyleElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const blocking = bridge.accessor(Style.getBlocking, Style.setBlocking, .{});\n    pub const media = bridge.accessor(Style.getMedia, Style.setMedia, .{});\n    pub const @\"type\" = bridge.accessor(Style.getType, Style.setType, .{});\n    pub const disabled = bridge.accessor(Style.getDisabled, Style.setDisabled, .{});\n    pub const sheet = bridge.accessor(Style.getSheet, null, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: Style\" {\n    try testing.htmlRunner(\"element/html/style.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Table.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Table = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Table) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Table) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Table);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTableElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/TableCaption.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst TableCaption = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *TableCaption) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *TableCaption) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TableCaption);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTableCaptionElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/TableCell.zig",
    "content": "const std = @import(\"std\");\nconst String = @import(\"../../../../string.zig\").String;\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst TableCell = @This();\n\n_tag_name: String,\n_tag: Element.Tag,\n_proto: *HtmlElement,\n\npub fn asElement(self: *TableCell) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *TableCell) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getColSpan(self: *TableCell) u32 {\n    const attr = self.asElement().getAttributeSafe(comptime .wrap(\"colspan\")) orelse return 1;\n    const v = std.fmt.parseUnsigned(u32, attr, 10) catch return 1;\n    if (v == 0) return 1;\n    return @min(v, 1000);\n}\n\npub fn setColSpan(self: *TableCell, value: u32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"colspan\"), .wrap(str), page);\n}\n\npub fn getRowSpan(self: *TableCell) u32 {\n    const attr = self.asElement().getAttributeSafe(comptime .wrap(\"rowspan\")) orelse return 1;\n    const v = std.fmt.parseUnsigned(u32, attr, 10) catch return 1;\n    return @min(v, 65534);\n}\n\npub fn setRowSpan(self: *TableCell, value: u32, page: *Page) !void {\n    const str = try std.fmt.allocPrint(page.call_arena, \"{d}\", .{value});\n    try self.asElement().setAttributeSafe(comptime .wrap(\"rowspan\"), .wrap(str), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TableCell);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTableCellElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const colSpan = bridge.accessor(TableCell.getColSpan, TableCell.setColSpan, .{});\n    pub const rowSpan = bridge.accessor(TableCell.getRowSpan, TableCell.setRowSpan, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.TableCell\" {\n    try testing.htmlRunner(\"element/html/tablecell.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/TableCol.zig",
    "content": "const String = @import(\"../../../../string.zig\").String;\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst TableCol = @This();\n\n_tag_name: String,\n_tag: Element.Tag,\n_proto: *HtmlElement,\n\npub fn asElement(self: *TableCol) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *TableCol) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TableCol);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTableColElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/TableRow.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst TableRow = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *TableRow) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *TableRow) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TableRow);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTableRowElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/TableSection.zig",
    "content": "const String = @import(\"../../../../string.zig\").String;\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst TableSection = @This();\n\n_tag_name: String,\n_tag: Element.Tag,\n_proto: *HtmlElement,\n\npub fn asElement(self: *TableSection) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *TableSection) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TableSection);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTableSectionElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Template.zig",
    "content": "const std = @import(\"std\");\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst DocumentFragment = @import(\"../../DocumentFragment.zig\");\n\nconst Template = @This();\n\n_proto: *HtmlElement,\n_content: *DocumentFragment,\n\npub fn asElement(self: *Template) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Template) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getContent(self: *Template) *DocumentFragment {\n    return self._content;\n}\n\npub fn setInnerHTML(self: *Template, html: []const u8, page: *Page) !void {\n    return self._content.setInnerHTML(html, page);\n}\n\npub fn getOuterHTML(self: *Template, writer: *std.Io.Writer, page: *Page) !void {\n    const dump = @import(\"../../../dump.zig\");\n    const el = self.asElement();\n\n    try el.format(writer);\n    try dump.children(self._content.asNode(), .{ .shadow = .skip }, writer, page);\n    try writer.writeAll(\"</\");\n    try writer.writeAll(el.getTagNameDump());\n    try writer.writeByte('>');\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Template);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTemplateElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const content = bridge.accessor(Template.getContent, null, .{});\n    pub const innerHTML = bridge.accessor(_getInnerHTML, Template.setInnerHTML, .{});\n    pub const outerHTML = bridge.accessor(_getOuterHTML, null, .{});\n\n    fn _getInnerHTML(self: *Template, page: *Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self._content.getInnerHTML(&buf.writer, page);\n        return buf.written();\n    }\n\n    fn _getOuterHTML(self: *Template, page: *Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.getOuterHTML(&buf.writer, page);\n        return buf.written();\n    }\n};\n\npub const Build = struct {\n    pub fn created(node: *Node, page: *Page) !void {\n        const self = node.as(Template);\n        // Create the template content DocumentFragment\n        self._content = try DocumentFragment.init(page);\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: Template\" {\n    try testing.htmlRunner(\"element/html/template.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/TextArea.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\nconst Form = @import(\"Form.zig\");\nconst Selection = @import(\"../../Selection.zig\");\nconst Event = @import(\"../../Event.zig\");\nconst InputEvent = @import(\"../../event/InputEvent.zig\");\n\nconst TextArea = @This();\n\n_proto: *HtmlElement,\n_value: ?[]const u8 = null,\n\n_selection_start: u32 = 0,\n_selection_end: u32 = 0,\n_selection_direction: Selection.SelectionDirection = .none,\n\n_on_selectionchange: ?js.Function.Global = null,\n\npub fn getOnSelectionChange(self: *TextArea) ?js.Function.Global {\n    return self._on_selectionchange;\n}\n\npub fn setOnSelectionChange(self: *TextArea, listener: ?js.Function) !void {\n    if (listener) |listen| {\n        self._on_selectionchange = try listen.persistWithThis(self);\n    } else {\n        self._on_selectionchange = null;\n    }\n}\n\nfn dispatchSelectionChangeEvent(self: *TextArea, page: *Page) !void {\n    const event = try Event.init(\"selectionchange\", .{ .bubbles = true }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event);\n}\n\nfn dispatchInputEvent(self: *TextArea, data: ?[]const u8, input_type: []const u8, page: *Page) !void {\n    const event = try InputEvent.initTrusted(comptime .wrap(\"input\"), .{ .data = data, .inputType = input_type }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());\n}\n\npub fn asElement(self: *TextArea) *Element {\n    return self._proto._proto;\n}\npub fn asConstElement(self: *const TextArea) *const Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *TextArea) *Node {\n    return self.asElement().asNode();\n}\npub fn asConstNode(self: *const TextArea) *const Node {\n    return self.asConstElement().asConstNode();\n}\n\npub fn getValue(self: *const TextArea) []const u8 {\n    return self._value orelse self.getDefaultValue();\n}\n\npub fn setValue(self: *TextArea, value: []const u8, page: *Page) !void {\n    const owned = try page.arena.dupe(u8, value);\n    self._value = owned;\n}\n\npub fn getDefaultValue(self: *const TextArea) []const u8 {\n    const node = self.asConstNode();\n    if (node.firstChild()) |child| {\n        if (child.is(Node.CData.Text)) |txt| {\n            return txt.getWholeText();\n        }\n    }\n    return \"\";\n}\n\npub fn setDefaultValue(self: *TextArea, value: []const u8, page: *Page) !void {\n    const node = self.asNode();\n    if (node.firstChild()) |child| {\n        if (child.is(Node.CData.Text)) |txt| {\n            txt._proto._data = try page.dupeSSO(value);\n            return;\n        }\n    }\n\n    // No text child exists, create one\n    const text_node = try page.createTextNode(value);\n    _ = try node.appendChild(text_node, page);\n}\n\npub fn getDisabled(self: *const TextArea) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n}\n\npub fn setDisabled(self: *TextArea, disabled: bool, page: *Page) !void {\n    if (disabled) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"disabled\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"disabled\"), page);\n    }\n}\n\npub fn getName(self: *const TextArea) []const u8 {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"name\")) orelse \"\";\n}\n\npub fn setName(self: *TextArea, name: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"name\"), .wrap(name), page);\n}\n\npub fn getRequired(self: *const TextArea) bool {\n    return self.asConstElement().getAttributeSafe(comptime .wrap(\"required\")) != null;\n}\n\npub fn setRequired(self: *TextArea, required: bool, page: *Page) !void {\n    if (required) {\n        try self.asElement().setAttributeSafe(comptime .wrap(\"required\"), .wrap(\"\"), page);\n    } else {\n        try self.asElement().removeAttribute(comptime .wrap(\"required\"), page);\n    }\n}\n\npub fn select(self: *TextArea, page: *Page) !void {\n    const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;\n    try self.setSelectionRange(0, len, null, page);\n    const event = try Event.init(\"select\", .{ .bubbles = true }, page);\n    try page._event_manager.dispatch(self.asElement().asEventTarget(), event);\n}\n\nconst HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };\n\nfn howSelected(self: *const TextArea) HowSelected {\n    const value = self._value orelse return .none;\n\n    if (self._selection_start == self._selection_end) return .none;\n    if (self._selection_start == 0 and self._selection_end == value.len) return .full;\n    return .{ .partial = .{ self._selection_start, self._selection_end } };\n}\n\npub fn innerInsert(self: *TextArea, str: []const u8, page: *Page) !void {\n    const arena = page.arena;\n\n    switch (self.howSelected()) {\n        .full => {\n            // if the text area is fully selected, replace the content.\n            const new_value = try arena.dupe(u8, str);\n            try self.setValue(new_value, page);\n            self._selection_start = @intCast(new_value.len);\n            self._selection_end = @intCast(new_value.len);\n            self._selection_direction = .none;\n            try self.dispatchSelectionChangeEvent(page);\n        },\n        .partial => |range| {\n            // if the text area is partially selected, replace the selected content.\n            const current_value = self.getValue();\n            const before = current_value[0..range[0]];\n            const remaining = current_value[range[1]..];\n\n            const new_value = try std.mem.concat(\n                arena,\n                u8,\n                &.{ before, str, remaining },\n            );\n            try self.setValue(new_value, page);\n\n            const new_pos = range[0] + str.len;\n            self._selection_start = @intCast(new_pos);\n            self._selection_end = @intCast(new_pos);\n            self._selection_direction = .none;\n            try self.dispatchSelectionChangeEvent(page);\n        },\n        .none => {\n            // if the text area is not selected, just insert at cursor.\n            const current_value = self.getValue();\n            const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });\n            try self.setValue(new_value, page);\n        },\n    }\n    try self.dispatchInputEvent(str, \"insertText\", page);\n}\n\npub fn getSelectionDirection(self: *const TextArea) []const u8 {\n    return @tagName(self._selection_direction);\n}\n\npub fn getSelectionStart(self: *const TextArea) u32 {\n    return self._selection_start;\n}\n\npub fn setSelectionStart(self: *TextArea, value: u32, page: *Page) !void {\n    self._selection_start = value;\n    try self.dispatchSelectionChangeEvent(page);\n}\n\npub fn getSelectionEnd(self: *const TextArea) u32 {\n    return self._selection_end;\n}\n\npub fn setSelectionEnd(self: *TextArea, value: u32, page: *Page) !void {\n    self._selection_end = value;\n    try self.dispatchSelectionChangeEvent(page);\n}\n\npub fn setSelectionRange(\n    self: *TextArea,\n    selection_start: u32,\n    selection_end: u32,\n    selection_dir: ?[]const u8,\n    page: *Page,\n) !void {\n    const direction = blk: {\n        if (selection_dir) |sd| {\n            break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;\n        } else break :blk .none;\n    };\n\n    const value = self._value orelse {\n        self._selection_start = 0;\n        self._selection_end = 0;\n        self._selection_direction = .none;\n        return;\n    };\n\n    const len_u32: u32 = @intCast(value.len);\n    var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;\n    const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;\n\n    // If end is less than start, both are equal to end.\n    if (end < start) {\n        start = end;\n    }\n\n    self._selection_direction = direction;\n    self._selection_start = start;\n    self._selection_end = end;\n\n    try self.dispatchSelectionChangeEvent(page);\n}\n\npub fn getForm(self: *TextArea, page: *Page) ?*Form {\n    const element = self.asElement();\n\n    // If form attribute exists, ONLY use that (even if it references nothing)\n    if (element.getAttributeSafe(comptime .wrap(\"form\"))) |form_id| {\n        if (page.document.getElementById(form_id, page)) |form_element| {\n            return form_element.is(Form);\n        }\n        // form attribute present but invalid - no form owner\n        return null;\n    }\n\n    // No form attribute - traverse ancestors looking for a <form>\n    var node = element.asNode()._parent;\n    while (node) |n| {\n        if (n.is(Element.Html.Form)) |form| {\n            return form;\n        }\n        node = n._parent;\n    }\n\n    return null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextArea);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTextAreaElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const onselectionchange = bridge.accessor(TextArea.getOnSelectionChange, TextArea.setOnSelectionChange, .{});\n    pub const value = bridge.accessor(TextArea.getValue, TextArea.setValue, .{});\n    pub const defaultValue = bridge.accessor(TextArea.getDefaultValue, TextArea.setDefaultValue, .{});\n    pub const disabled = bridge.accessor(TextArea.getDisabled, TextArea.setDisabled, .{});\n    pub const name = bridge.accessor(TextArea.getName, TextArea.setName, .{});\n    pub const required = bridge.accessor(TextArea.getRequired, TextArea.setRequired, .{});\n    pub const form = bridge.accessor(TextArea.getForm, null, .{});\n    pub const select = bridge.function(TextArea.select, .{});\n\n    pub const selectionStart = bridge.accessor(TextArea.getSelectionStart, TextArea.setSelectionStart, .{});\n    pub const selectionEnd = bridge.accessor(TextArea.getSelectionEnd, TextArea.setSelectionEnd, .{});\n    pub const selectionDirection = bridge.accessor(TextArea.getSelectionDirection, null, .{});\n    pub const setSelectionRange = bridge.function(TextArea.setSelectionRange, .{ .dom_exception = true });\n};\n\npub const Build = struct {\n    pub fn cloned(source_element: *Element, cloned_element: *Element, _: *Page) !void {\n        const source = source_element.as(TextArea);\n        const clone = cloned_element.as(TextArea);\n        clone._value = source._value;\n    }\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.TextArea\" {\n    try testing.htmlRunner(\"element/html/textarea.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Time.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Time = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Time) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Time) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getDateTime(self: *Time) []const u8 {\n    return self.asElement().getAttributeSafe(comptime .wrap(\"datetime\")) orelse \"\";\n}\n\npub fn setDateTime(self: *Time, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"datetime\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Time);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTimeElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const dateTime = bridge.accessor(Time.getDateTime, Time.setDateTime, .{});\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Time\" {\n    try testing.htmlRunner(\"element/html/time.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/Title.zig",
    "content": "const js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Title = @This();\n\n_proto: *HtmlElement,\n\npub fn asElement(self: *Title) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Title) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Title);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTitleElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Track.zig",
    "content": "// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../../js/js.zig\");\nconst String = @import(\"../../../../string.zig\").String;\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Track = @This();\n\n_proto: *HtmlElement,\n_kind: String,\n_ready_state: ReadyState,\n\nconst ReadyState = enum(u8) { none, loading, loaded, @\"error\" };\n\npub fn asElement(self: *Track) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Track) *Node {\n    return self.asElement().asNode();\n}\n\npub fn setKind(self: *Track, maybe_kind: ?String) void {\n    const kind = maybe_kind orelse {\n        self._kind = comptime .wrap(\"metadata\");\n        return;\n    };\n\n    // Special case, for some reason, FF does this case-insensitive.\n    if (std.ascii.eqlIgnoreCase(kind.str(), \"subtitles\")) {\n        self._kind = comptime .wrap(\"subtitles\");\n        return;\n    }\n    if (kind.eql(comptime .wrap(\"captions\"))) {\n        self._kind = comptime .wrap(\"captions\");\n        return;\n    }\n    if (kind.eql(comptime .wrap(\"descriptions\"))) {\n        self._kind = comptime .wrap(\"descriptions\");\n        return;\n    }\n    if (kind.eql(comptime .wrap(\"chapters\"))) {\n        self._kind = comptime .wrap(\"chapters\");\n        return;\n    }\n\n    // Anything else must be considered as `metadata`.\n    self._kind = comptime .wrap(\"metadata\");\n}\n\npub fn getKind(self: *const Track) String {\n    return self._kind;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Track);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLTrackElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const kind = bridge.accessor(Track.getKind, Track.setKind, .{});\n\n    pub const NONE = bridge.property(@as(u16, @intFromEnum(ReadyState.none)), .{ .template = true });\n    pub const LOADING = bridge.property(@as(u16, @intFromEnum(ReadyState.loading)), .{ .template = true });\n    pub const LOADED = bridge.property(@as(u16, @intFromEnum(ReadyState.loaded)), .{ .template = true });\n    pub const ERROR = bridge.property(@as(u16, @intFromEnum(ReadyState.@\"error\")), .{ .template = true });\n};\n\nconst testing = @import(\"../../../../testing.zig\");\ntest \"WebApi: HTML.Track\" {\n    try testing.htmlRunner(\"element/html/track.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/element/html/UL.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst UL = @This();\n_proto: *HtmlElement,\n\npub fn asElement(self: *UL) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *UL) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(UL);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLUListElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Unknown.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../../string.zig\").String;\n\nconst js = @import(\"../../../js/js.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst HtmlElement = @import(\"../Html.zig\");\n\nconst Unknown = @This();\n_proto: *HtmlElement,\n_tag_name: String,\n\npub fn asElement(self: *Unknown) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Unknown) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Unknown);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLUnknownElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/html/Video.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Page = @import(\"../../../Page.zig\");\n\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst Media = @import(\"Media.zig\");\n\nconst Video = @This();\n\n_proto: *Media,\n\npub fn asMedia(self: *Video) *Media {\n    return self._proto;\n}\n\npub fn asElement(self: *Video) *Element {\n    return self._proto.asElement();\n}\n\npub fn asConstElement(self: *const Video) *const Element {\n    return self._proto.asConstElement();\n}\n\npub fn asNode(self: *Video) *Node {\n    return self.asElement().asNode();\n}\n\npub fn getVideoWidth(_: *const Video) u32 {\n    return 0;\n}\n\npub fn getVideoHeight(_: *const Video) u32 {\n    return 0;\n}\n\npub fn getPoster(self: *const Video, page: *Page) ![]const u8 {\n    const element = self.asConstElement();\n    const poster = element.getAttributeSafe(comptime .wrap(\"poster\")) orelse return \"\";\n    if (poster.len == 0) {\n        return \"\";\n    }\n\n    const URL = @import(\"../../URL.zig\");\n    return URL.resolve(page.call_arena, page.base(), poster, .{ .encode = true });\n}\n\npub fn setPoster(self: *Video, value: []const u8, page: *Page) !void {\n    try self.asElement().setAttributeSafe(comptime .wrap(\"poster\"), .wrap(value), page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Video);\n\n    pub const Meta = struct {\n        pub const name = \"HTMLVideoElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const poster = bridge.accessor(Video.getPoster, Video.setPoster, .{});\n    pub const videoWidth = bridge.accessor(Video.getVideoWidth, null, .{});\n    pub const videoHeight = bridge.accessor(Video.getVideoHeight, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/element/svg/Generic.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst Svg = @import(\"../Svg.zig\");\n\nconst Generic = @This();\n_proto: *Svg,\n_tag: Element.Tag,\n\npub fn asElement(self: *Generic) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Generic) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Generic);\n\n    pub const Meta = struct {\n        pub const name = \"SVGGenericElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/element/svg/Rect.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../../js/js.zig\");\nconst Node = @import(\"../../Node.zig\");\nconst Element = @import(\"../../Element.zig\");\nconst Svg = @import(\"../Svg.zig\");\n\nconst Rect = @This();\n_proto: *Svg,\n\npub fn asElement(self: *Rect) *Element {\n    return self._proto._proto;\n}\npub fn asNode(self: *Rect) *Node {\n    return self.asElement().asNode();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Rect);\n\n    pub const Meta = struct {\n        pub const name = \"SVGRectElement\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/encoding/TextDecoder.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst TextDecoder = @This();\n\n_fatal: bool,\n_arena: Allocator,\n_ignore_bom: bool,\n_stream: std.ArrayList(u8),\n\nconst Label = enum {\n    utf8,\n    @\"utf-8\",\n    @\"unicode-1-1-utf-8\",\n};\n\nconst InitOpts = struct {\n    fatal: bool = false,\n    ignoreBOM: bool = false,\n};\n\npub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {\n    if (label_) |label| {\n        _ = std.meta.stringToEnum(Label, label) orelse return error.RangeError;\n    }\n\n    const arena = try page.getArena(.{ .debug = \"TextDecoder\" });\n    errdefer page.releaseArena(arena);\n\n    const opts = opts_ orelse InitOpts{};\n    const self = try arena.create(TextDecoder);\n    self.* = .{\n        ._arena = arena,\n        ._stream = .empty,\n        ._fatal = opts.fatal,\n        ._ignore_bom = opts.ignoreBOM,\n    };\n    return self;\n}\n\npub fn deinit(self: *TextDecoder, _: bool, session: *Session) void {\n    session.releaseArena(self._arena);\n}\n\npub fn getIgnoreBOM(self: *const TextDecoder) bool {\n    return self._ignore_bom;\n}\n\npub fn getFatal(self: *const TextDecoder) bool {\n    return self._fatal;\n}\n\nconst DecodeOpts = struct {\n    stream: bool = false,\n};\npub fn decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOpts) ![]const u8 {\n    var input = input_ orelse return \"\";\n    const opts: DecodeOpts = opts_ orelse .{};\n\n    if (self._stream.items.len > 0) {\n        try self._stream.appendSlice(self._arena, input);\n        input = self._stream.items;\n    }\n\n    if (self._fatal and !std.unicode.utf8ValidateSlice(input)) {\n        if (opts.stream) {\n            if (self._stream.items.len == 0) {\n                try self._stream.appendSlice(self._arena, input);\n            }\n            return \"\";\n        }\n        return error.InvalidUtf8;\n    }\n\n    self._stream.clearRetainingCapacity();\n    if (self._ignore_bom == false and std.mem.startsWith(u8, input, &.{ 0xEF, 0xBB, 0xBF })) {\n        return input[3..];\n    }\n\n    return input;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextDecoder);\n\n    pub const Meta = struct {\n        pub const name = \"TextDecoder\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(TextDecoder.deinit);\n    };\n\n    pub const constructor = bridge.constructor(TextDecoder.init, .{});\n    pub const decode = bridge.function(TextDecoder.decode, .{});\n    pub const encoding = bridge.property(\"utf-8\", .{ .template = false });\n    pub const fatal = bridge.accessor(TextDecoder.getFatal, null, .{});\n    pub const ignoreBOM = bridge.accessor(TextDecoder.getIgnoreBOM, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: TextDecoder\" {\n    try testing.htmlRunner(\"encoding/text_decoder.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/encoding/TextDecoderStream.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ReadableStream = @import(\"../streams/ReadableStream.zig\");\nconst WritableStream = @import(\"../streams/WritableStream.zig\");\nconst TransformStream = @import(\"../streams/TransformStream.zig\");\n\nconst TextDecoderStream = @This();\n\n_transform: *TransformStream,\n_fatal: bool,\n_ignore_bom: bool,\n\nconst Label = enum {\n    utf8,\n    @\"utf-8\",\n    @\"unicode-1-1-utf-8\",\n};\n\nconst InitOpts = struct {\n    fatal: bool = false,\n    ignoreBOM: bool = false,\n};\n\npub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !TextDecoderStream {\n    if (label_) |label| {\n        _ = std.meta.stringToEnum(Label, label) orelse return error.RangeError;\n    }\n\n    const opts = opts_ orelse InitOpts{};\n    const decodeFn: TransformStream.ZigTransformFn = blk: {\n        if (opts.ignoreBOM) {\n            break :blk struct {\n                fn decode(controller: *TransformStream.DefaultController, chunk: js.Value) !void {\n                    return decodeTransform(controller, chunk, true);\n                }\n            }.decode;\n        } else {\n            break :blk struct {\n                fn decode(controller: *TransformStream.DefaultController, chunk: js.Value) !void {\n                    return decodeTransform(controller, chunk, false);\n                }\n            }.decode;\n        }\n    };\n\n    const transform = try TransformStream.initWithZigTransform(decodeFn, page);\n\n    return .{\n        ._transform = transform,\n        ._fatal = opts.fatal,\n        ._ignore_bom = opts.ignoreBOM,\n    };\n}\n\nfn decodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value, ignoreBOM: bool) !void {\n    // chunk should be a Uint8Array; decode it as UTF-8 string\n    const typed_array = try chunk.toZig(js.TypedArray(u8));\n    var input = typed_array.values;\n\n    // Strip UTF-8 BOM if present\n    if (ignoreBOM == false and std.mem.startsWith(u8, input, &.{ 0xEF, 0xBB, 0xBF })) {\n        input = input[3..];\n    }\n\n    // Per spec, empty chunks produce no output\n    if (input.len == 0) return;\n\n    try controller.enqueue(.{ .string = input });\n}\n\npub fn getReadable(self: *const TextDecoderStream) *ReadableStream {\n    return self._transform.getReadable();\n}\n\npub fn getWritable(self: *const TextDecoderStream) *WritableStream {\n    return self._transform.getWritable();\n}\n\npub fn getFatal(self: *const TextDecoderStream) bool {\n    return self._fatal;\n}\n\npub fn getIgnoreBOM(self: *const TextDecoderStream) bool {\n    return self._ignore_bom;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextDecoderStream);\n\n    pub const Meta = struct {\n        pub const name = \"TextDecoderStream\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(TextDecoderStream.init, .{});\n    pub const encoding = bridge.property(\"utf-8\", .{ .template = false });\n    pub const readable = bridge.accessor(TextDecoderStream.getReadable, null, .{});\n    pub const writable = bridge.accessor(TextDecoderStream.getWritable, null, .{});\n    pub const fatal = bridge.accessor(TextDecoderStream.getFatal, null, .{});\n    pub const ignoreBOM = bridge.accessor(TextDecoderStream.getIgnoreBOM, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: TextDecoderStream\" {\n    try testing.htmlRunner(\"streams/text_decoder_stream.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/encoding/TextEncoder.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst TextEncoder = @This();\n_pad: bool = false,\n\npub fn init() TextEncoder {\n    return .{};\n}\n\npub fn encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) {\n    if (!std.unicode.utf8ValidateSlice(v)) {\n        return error.InvalidUtf8;\n    }\n\n    return .{ .values = v };\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextEncoder);\n\n    pub const Meta = struct {\n        pub const name = \"TextEncoder\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const empty_with_no_proto = true;\n    };\n\n    pub const constructor = bridge.constructor(TextEncoder.init, .{});\n    pub const encode = bridge.function(TextEncoder.encode, .{ .as_typed_array = true });\n    pub const encoding = bridge.property(\"utf-8\", .{ .template = false });\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: TextEncoder\" {\n    try testing.htmlRunner(\"encoding/text_encoder.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/encoding/TextEncoderStream.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ReadableStream = @import(\"../streams/ReadableStream.zig\");\nconst WritableStream = @import(\"../streams/WritableStream.zig\");\nconst TransformStream = @import(\"../streams/TransformStream.zig\");\n\nconst TextEncoderStream = @This();\n\n_transform: *TransformStream,\n\npub fn init(page: *Page) !TextEncoderStream {\n    const transform = try TransformStream.initWithZigTransform(&encodeTransform, page);\n    return .{\n        ._transform = transform,\n    };\n}\n\nfn encodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value) !void {\n    // chunk should be a JS string; encode it as UTF-8 bytes (Uint8Array)\n    const str = chunk.isString() orelse return error.InvalidChunk;\n    const slice = try str.toSlice();\n    try controller.enqueue(.{ .uint8array = .{ .values = slice } });\n}\n\npub fn getReadable(self: *const TextEncoderStream) *ReadableStream {\n    return self._transform.getReadable();\n}\n\npub fn getWritable(self: *const TextEncoderStream) *WritableStream {\n    return self._transform.getWritable();\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextEncoderStream);\n\n    pub const Meta = struct {\n        pub const name = \"TextEncoderStream\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(TextEncoderStream.init, .{});\n    pub const encoding = bridge.property(\"utf-8\", .{ .template = false });\n    pub const readable = bridge.accessor(TextEncoderStream.getReadable, null, .{});\n    pub const writable = bridge.accessor(TextEncoderStream.getWritable, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: TextEncoderStream\" {\n    try testing.htmlRunner(\"streams/transform_stream.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/CompositionEvent.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst CompositionEvent = @This();\n\n_proto: *Event,\n_data: []const u8 = \"\",\n\nconst CompositionEventOptions = struct {\n    data: ?[]const u8 = null,\n};\n\nconst Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions);\n\npub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent {\n    const arena = try page.getArena(.{ .debug = \"CompositionEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = opts_ orelse Options{};\n    const event = try page._factory.event(\n        arena,\n        type_string,\n        CompositionEvent{\n            ._proto = undefined,\n            ._data = if (opts.data) |str| try arena.dupe(u8, str) else \"\",\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn deinit(self: *CompositionEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *CompositionEvent) *Event {\n    return self._proto;\n}\n\npub fn getData(self: *const CompositionEvent) []const u8 {\n    return self._data;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CompositionEvent);\n\n    pub const Meta = struct {\n        pub const name = \"CompositionEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(CompositionEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(CompositionEvent.init, .{});\n    pub const data = bridge.accessor(CompositionEvent.getData, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: CompositionEvent\" {\n    try testing.htmlRunner(\"event/composition.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/CustomEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst CustomEvent = @This();\n\n_proto: *Event,\n_detail: ?js.Value.Temp = null,\n_arena: Allocator,\n\nconst CustomEventOptions = struct {\n    detail: ?js.Value.Temp = null,\n};\n\nconst Options = Event.inheritOptions(CustomEvent, CustomEventOptions);\n\npub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent {\n    const arena = try page.getArena(.{ .debug = \"CustomEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = opts_ orelse Options{};\n    const event = try page._factory.event(\n        arena,\n        type_string,\n        CustomEvent{\n            ._arena = arena,\n            ._proto = undefined,\n            ._detail = opts.detail,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn initCustomEvent(\n    self: *CustomEvent,\n    event_string: []const u8,\n    bubbles: ?bool,\n    cancelable: ?bool,\n    detail_: ?js.Value.Temp,\n) !void {\n    // This function can only be called after the constructor has called.\n    // So we assume proto is initialized already by constructor.\n    self._proto._type_string = try String.init(self._proto._arena, event_string, .{});\n    self._proto._bubbles = bubbles orelse false;\n    self._proto._cancelable = cancelable orelse false;\n    // Detail is stored separately.\n    self._detail = detail_;\n}\n\npub fn deinit(self: *CustomEvent, shutdown: bool, session: *Session) void {\n    if (self._detail) |d| {\n        d.release();\n    }\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *CustomEvent) *Event {\n    return self._proto;\n}\n\npub fn getDetail(self: *const CustomEvent) ?js.Value.Temp {\n    return self._detail;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(CustomEvent);\n\n    pub const Meta = struct {\n        pub const name = \"CustomEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(CustomEvent.deinit);\n        pub const enumerable = false;\n    };\n\n    pub const constructor = bridge.constructor(CustomEvent.init, .{});\n    pub const detail = bridge.accessor(CustomEvent.getDetail, null, .{});\n    pub const initCustomEvent = bridge.function(CustomEvent.initCustomEvent, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: CustomEvent\" {\n    try testing.htmlRunner(\"event/custom_event.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/ErrorEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst ErrorEvent = @This();\n\n_proto: *Event,\n_message: []const u8 = \"\",\n_filename: []const u8 = \"\",\n_line_number: u32 = 0,\n_column_number: u32 = 0,\n_error: ?js.Value.Temp = null,\n_arena: Allocator,\n\npub const ErrorEventOptions = struct {\n    message: ?[]const u8 = null,\n    filename: ?[]const u8 = null,\n    lineno: u32 = 0,\n    colno: u32 = 0,\n    @\"error\": ?js.Value.Temp = null,\n};\n\nconst Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);\n\npub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {\n    const arena = try page.getArena(.{ .debug = \"ErrorEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, opts_, false, page);\n}\n\npub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent {\n    const arena = try page.getArena(.{ .debug = \"ErrorEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, opts_, true, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {\n    const opts = opts_ orelse Options{};\n\n    const event = try page._factory.event(\n        arena,\n        typ,\n        ErrorEvent{\n            ._arena = arena,\n            ._proto = undefined,\n            ._message = if (opts.message) |str| try arena.dupe(u8, str) else \"\",\n            ._filename = if (opts.filename) |str| try arena.dupe(u8, str) else \"\",\n            ._line_number = opts.lineno,\n            ._column_number = opts.colno,\n            ._error = opts.@\"error\",\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *ErrorEvent, shutdown: bool, session: *Session) void {\n    if (self._error) |e| {\n        e.release();\n    }\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *ErrorEvent) *Event {\n    return self._proto;\n}\n\npub fn getMessage(self: *const ErrorEvent) []const u8 {\n    return self._message;\n}\n\npub fn getFilename(self: *const ErrorEvent) []const u8 {\n    return self._filename;\n}\n\npub fn getLineNumber(self: *const ErrorEvent) u32 {\n    return self._line_number;\n}\n\npub fn getColumnNumber(self: *const ErrorEvent) u32 {\n    return self._column_number;\n}\n\npub fn getError(self: *const ErrorEvent) ?js.Value.Temp {\n    return self._error;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ErrorEvent);\n\n    pub const Meta = struct {\n        pub const name = \"ErrorEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(ErrorEvent.deinit);\n    };\n\n    // Start API\n    pub const constructor = bridge.constructor(ErrorEvent.init, .{});\n    pub const message = bridge.accessor(ErrorEvent.getMessage, null, .{});\n    pub const filename = bridge.accessor(ErrorEvent.getFilename, null, .{});\n    pub const lineno = bridge.accessor(ErrorEvent.getLineNumber, null, .{});\n    pub const colno = bridge.accessor(ErrorEvent.getColumnNumber, null, .{});\n    pub const @\"error\" = bridge.accessor(ErrorEvent.getError, null, .{ .null_as_undefined = true });\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: ErrorEvent\" {\n    try testing.htmlRunner(\"event/error.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/FocusEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst String = @import(\"../../../string.zig\").String;\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\nconst UIEvent = @import(\"UIEvent.zig\");\n\nconst FocusEvent = @This();\n\n_proto: *UIEvent,\n_related_target: ?*EventTarget = null,\n\npub const FocusEventOptions = struct {\n    relatedTarget: ?*EventTarget = null,\n};\n\npub const Options = Event.inheritOptions(\n    FocusEvent,\n    FocusEventOptions,\n);\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*FocusEvent {\n    const arena = try page.getArena(.{ .debug = \"FocusEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*FocusEvent {\n    const arena = try page.getArena(.{ .debug = \"FocusEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*FocusEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.uiEvent(\n        arena,\n        typ,\n        FocusEvent{\n            ._proto = undefined,\n            ._related_target = opts.relatedTarget,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *FocusEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *FocusEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn getRelatedTarget(self: *const FocusEvent) ?*EventTarget {\n    return self._related_target;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FocusEvent);\n\n    pub const Meta = struct {\n        pub const name = \"FocusEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(FocusEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(FocusEvent.init, .{});\n    pub const relatedTarget = bridge.accessor(FocusEvent.getRelatedTarget, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: FocusEvent\" {\n    try testing.htmlRunner(\"event/focus.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/InputEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst UIEvent = @import(\"UIEvent.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst InputEvent = @This();\n\n_proto: *UIEvent,\n_data: ?[]const u8,\n// TODO: add dataTransfer\n_input_type: []const u8,\n_is_composing: bool,\n\npub const InputEventOptions = struct {\n    data: ?[]const u8 = null,\n    inputType: ?[]const u8 = null,\n    isComposing: bool = false,\n};\n\nconst Options = Event.inheritOptions(\n    InputEvent,\n    InputEventOptions,\n);\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*InputEvent {\n    const arena = try page.getArena(.{ .debug = \"InputEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*InputEvent {\n    const arena = try page.getArena(.{ .debug = \"InputEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*InputEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.uiEvent(\n        arena,\n        typ,\n        InputEvent{\n            ._proto = undefined,\n            ._data = if (opts.data) |d| try arena.dupe(u8, d) else null,\n            ._input_type = if (opts.inputType) |it| try arena.dupe(u8, it) else \"\",\n            ._is_composing = opts.isComposing,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n\n    // https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event\n    const rootevt = event._proto._proto;\n    rootevt._bubbles = true;\n    rootevt._cancelable = false;\n    rootevt._composed = true;\n\n    return event;\n}\n\npub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *InputEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn getData(self: *const InputEvent) ?[]const u8 {\n    return self._data;\n}\n\npub fn getInputType(self: *const InputEvent) []const u8 {\n    return self._input_type;\n}\n\npub fn getIsComposing(self: *const InputEvent) bool {\n    return self._is_composing;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(InputEvent);\n\n    pub const Meta = struct {\n        pub const name = \"InputEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(InputEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(InputEvent.init, .{});\n    pub const data = bridge.accessor(InputEvent.getData, null, .{});\n    pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{});\n    pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/event/KeyboardEvent.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst UIEvent = @import(\"UIEvent.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst KeyboardEvent = @This();\n\n_proto: *UIEvent,\n_key: Key,\n_code: []const u8,\n_ctrl_key: bool,\n_shift_key: bool,\n_alt_key: bool,\n_meta_key: bool,\n_location: Location,\n_repeat: bool,\n_is_composing: bool,\n\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\npub const Key = union(enum) {\n    // Special Key Values\n    Dead,\n    Undefined,\n    Unidentified,\n\n    // Modifier Keys\n    Alt,\n    AltGraph,\n    CapsLock,\n    Control,\n    Fn,\n    FnLock,\n    Hyper,\n    Meta,\n    NumLock,\n    ScrollLock,\n    Shift,\n    Super,\n    Symbol,\n    SymbolLock,\n\n    // Whitespace Keys\n    Enter,\n    Tab,\n\n    // Navigation Keys\n    ArrowDown,\n    ArrowLeft,\n    ArrowRight,\n    ArrowUp,\n    End,\n    Home,\n    PageDown,\n    PageUp,\n\n    // Editing Keys\n    Backspace,\n    Clear,\n    Copy,\n    CrSel,\n    Cut,\n    Delete,\n    EraseEof,\n    ExSel,\n    Insert,\n    Paste,\n    Redo,\n    Undo,\n\n    // UI Keys\n    Accept,\n    Again,\n    Attn,\n    Cancel,\n    ContextMenu,\n    Escape,\n    Execute,\n    Find,\n    Finish,\n    Help,\n    Pause,\n    Play,\n    Props,\n    Select,\n    ZoomIn,\n    ZoomOut,\n\n    // Function Keys\n    F1,\n    F2,\n    F3,\n    F4,\n    F5,\n    F6,\n    F7,\n    F8,\n    F9,\n    F10,\n    F11,\n    F12,\n\n    // Printable keys (single character, space, etc.)\n    standard: []const u8,\n\n    pub fn fromString(allocator: std.mem.Allocator, str: []const u8) !Key {\n        const key_type_info = @typeInfo(Key);\n        inline for (key_type_info.@\"union\".fields) |field| {\n            if (comptime std.mem.eql(u8, field.name, \"standard\")) continue;\n\n            if (std.mem.eql(u8, field.name, str)) {\n                return @unionInit(Key, field.name, {});\n            }\n        }\n\n        const duped = try allocator.dupe(u8, str);\n        return .{ .standard = duped };\n    }\n\n    /// Returns true if this key represents a printable character that should be\n    /// inserted into text input elements. This includes alphanumeric characters,\n    /// punctuation, symbols, and space.\n    pub fn isPrintable(self: Key) bool {\n        return switch (self) {\n            .standard => |s| s.len > 0,\n            else => false,\n        };\n    }\n\n    /// Returns the string representation that should be inserted into text input.\n    /// For most keys this is just the key itself, but some keys like Enter need\n    /// special handling (e.g., newline for textarea, form submission for input).\n    pub fn asString(self: Key) []const u8 {\n        return switch (self) {\n            .standard => |s| s,\n            else => |k| @tagName(k),\n        };\n    }\n};\n\npub const Location = enum(i32) {\n    DOM_KEY_LOCATION_STANDARD = 0,\n    DOM_KEY_LOCATION_LEFT = 1,\n    DOM_KEY_LOCATION_RIGHT = 2,\n    DOM_KEY_LOCATION_NUMPAD = 3,\n};\n\npub const KeyboardEventOptions = struct {\n    key: []const u8 = \"\",\n    code: ?[]const u8 = null,\n    location: i32 = 0,\n    repeat: bool = false,\n    isComposing: bool = false,\n    ctrlKey: bool = false,\n    shiftKey: bool = false,\n    altKey: bool = false,\n    metaKey: bool = false,\n};\n\nconst Options = Event.inheritOptions(\n    KeyboardEvent,\n    KeyboardEventOptions,\n);\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*KeyboardEvent {\n    const arena = try page.getArena(.{ .debug = \"KeyboardEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {\n    const arena = try page.getArena(.{ .debug = \"KeyboardEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*KeyboardEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.uiEvent(\n        arena,\n        typ,\n        KeyboardEvent{\n            ._proto = undefined,\n            ._key = try Key.fromString(arena, opts.key),\n            ._location = std.meta.intToEnum(Location, opts.location) catch return error.TypeError,\n            ._code = if (opts.code) |c| try arena.dupe(u8, c) else \"\",\n            ._repeat = opts.repeat,\n            ._is_composing = opts.isComposing,\n            ._ctrl_key = opts.ctrlKey,\n            ._shift_key = opts.shiftKey,\n            ._alt_key = opts.altKey,\n            ._meta_key = opts.metaKey,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n\n    // https://w3c.github.io/uievents/#event-type-keyup\n    const rootevt = event._proto._proto;\n    rootevt._bubbles = true;\n    rootevt._cancelable = true;\n    rootevt._composed = true;\n\n    return event;\n}\n\npub fn deinit(self: *KeyboardEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *KeyboardEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn getAltKey(self: *const KeyboardEvent) bool {\n    return self._alt_key;\n}\n\npub fn getCtrlKey(self: *const KeyboardEvent) bool {\n    return self._ctrl_key;\n}\n\npub fn getIsComposing(self: *const KeyboardEvent) bool {\n    return self._is_composing;\n}\n\npub fn getKey(self: *const KeyboardEvent) Key {\n    return self._key;\n}\n\npub fn getCode(self: *const KeyboardEvent) []const u8 {\n    return self._code;\n}\n\npub fn getLocation(self: *const KeyboardEvent) i32 {\n    return @intFromEnum(self._location);\n}\n\npub fn getMetaKey(self: *const KeyboardEvent) bool {\n    return self._meta_key;\n}\n\npub fn getRepeat(self: *const KeyboardEvent) bool {\n    return self._repeat;\n}\n\npub fn getShiftKey(self: *const KeyboardEvent) bool {\n    return self._shift_key;\n}\n\npub fn getModifierState(self: *const KeyboardEvent, str: []const u8) !bool {\n    const key = try Key.fromString(self._proto._proto._arena, str);\n\n    switch (key) {\n        .Alt, .AltGraph => return self._alt_key,\n        .Shift => return self._shift_key,\n        .Control => return self._ctrl_key,\n        .Meta => return self._meta_key,\n        .standard => |s| if (std.mem.eql(u8, s, \"Accel\")) {\n            return self._ctrl_key or self._meta_key;\n        },\n        else => {},\n    }\n    return false;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(KeyboardEvent);\n\n    pub const Meta = struct {\n        pub const name = \"KeyboardEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(KeyboardEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(KeyboardEvent.init, .{});\n    pub const altKey = bridge.accessor(KeyboardEvent.getAltKey, null, .{});\n    pub const ctrlKey = bridge.accessor(KeyboardEvent.getCtrlKey, null, .{});\n    pub const isComposing = bridge.accessor(KeyboardEvent.getIsComposing, null, .{});\n    pub const key = bridge.accessor(struct {\n        fn keyAsString(self: *const KeyboardEvent) []const u8 {\n            return self._key.asString();\n        }\n    }.keyAsString, null, .{});\n    pub const code = bridge.accessor(KeyboardEvent.getCode, null, .{});\n    pub const location = bridge.accessor(KeyboardEvent.getLocation, null, .{});\n    pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{});\n    pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{});\n    pub const shiftKey = bridge.accessor(KeyboardEvent.getShiftKey, null, .{});\n    pub const getModifierState = bridge.function(KeyboardEvent.getModifierState, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: KeyboardEvent\" {\n    try testing.htmlRunner(\"event/keyboard.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/MessageEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst String = @import(\"../../../string.zig\").String;\nconst js = @import(\"../../js/js.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Window = @import(\"../Window.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst MessageEvent = @This();\n\n_proto: *Event,\n_data: ?js.Value.Temp = null,\n_origin: []const u8 = \"\",\n_source: ?*Window = null,\n\nconst MessageEventOptions = struct {\n    data: ?js.Value.Temp = null,\n    origin: ?[]const u8 = null,\n    source: ?*Window = null,\n};\n\nconst Options = Event.inheritOptions(MessageEvent, MessageEventOptions);\n\npub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {\n    const arena = try page.getArena(.{ .debug = \"MessageEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, opts_, false, page);\n}\n\npub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*MessageEvent {\n    const arena = try page.getArena(.{ .debug = \"MessageEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, opts_, true, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent {\n    const opts = opts_ orelse Options{};\n\n    const event = try page._factory.event(\n        arena,\n        typ,\n        MessageEvent{\n            ._proto = undefined,\n            ._data = opts.data,\n            ._origin = if (opts.origin) |str| try arena.dupe(u8, str) else \"\",\n            ._source = opts.source,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *MessageEvent, shutdown: bool, session: *Session) void {\n    if (self._data) |d| {\n        d.release();\n    }\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *MessageEvent) *Event {\n    return self._proto;\n}\n\npub fn getData(self: *const MessageEvent) ?js.Value.Temp {\n    return self._data;\n}\n\npub fn getOrigin(self: *const MessageEvent) []const u8 {\n    return self._origin;\n}\n\npub fn getSource(self: *const MessageEvent) ?*Window {\n    return self._source;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MessageEvent);\n\n    pub const Meta = struct {\n        pub const name = \"MessageEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(MessageEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(MessageEvent.init, .{});\n    pub const data = bridge.accessor(MessageEvent.getData, null, .{});\n    pub const origin = bridge.accessor(MessageEvent.getOrigin, null, .{});\n    pub const source = bridge.accessor(MessageEvent.getSource, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: MessageEvent\" {\n    try testing.htmlRunner(\"event/message.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/MouseEvent.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\n\nconst UIEvent = @import(\"UIEvent.zig\");\nconst PointerEvent = @import(\"PointerEvent.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst MouseEvent = @This();\n\npub const MouseButton = enum(u8) {\n    main = 0,\n    auxillary = 1,\n    secondary = 2,\n    fourth = 3,\n    fifth = 4,\n};\n\npub const Type = union(enum) {\n    generic,\n    pointer_event: *PointerEvent,\n    wheel_event: *@import(\"WheelEvent.zig\"),\n};\n\n_type: Type,\n_proto: *UIEvent,\n\n_alt_key: bool,\n_button: MouseButton,\n_buttons: u16,\n_client_x: f64,\n_client_y: f64,\n_ctrl_key: bool,\n_meta_key: bool,\n// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget\n_related_target: ?*EventTarget = null,\n_screen_x: f64,\n_screen_y: f64,\n_shift_key: bool,\n\npub const MouseEventOptions = struct {\n    screenX: f64 = 0.0,\n    screenY: f64 = 0.0,\n    clientX: f64 = 0.0,\n    clientY: f64 = 0.0,\n    ctrlKey: bool = false,\n    shiftKey: bool = false,\n    altKey: bool = false,\n    metaKey: bool = false,\n    button: i32 = 0,\n    buttons: u16 = 0,\n    relatedTarget: ?*EventTarget = null,\n};\n\npub const Options = Event.inheritOptions(\n    MouseEvent,\n    MouseEventOptions,\n);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {\n    const arena = try page.getArena(.{ .debug = \"MouseEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*MouseEvent {\n    const arena = try page.getArena(.{ .debug = \"MouseEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*MouseEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.uiEvent(\n        arena,\n        typ,\n        MouseEvent{\n            ._type = .generic,\n            ._proto = undefined,\n            ._screen_x = opts.screenX,\n            ._screen_y = opts.screenY,\n            ._client_x = opts.clientX,\n            ._client_y = opts.clientY,\n            ._ctrl_key = opts.ctrlKey,\n            ._shift_key = opts.shiftKey,\n            ._alt_key = opts.altKey,\n            ._meta_key = opts.metaKey,\n            ._button = std.meta.intToEnum(MouseButton, opts.button) catch return error.TypeError,\n            ._buttons = opts.buttons,\n            ._related_target = opts.relatedTarget,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *MouseEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *MouseEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn as(self: *MouseEvent, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn is(self: *MouseEvent, comptime T: type) ?*T {\n    switch (self._type) {\n        .generic => return if (T == MouseEvent) self else null,\n        .pointer_event => |e| return if (T == PointerEvent) e else null,\n        .wheel_event => |e| return if (T == @import(\"WheelEvent.zig\")) e else null,\n    }\n    return null;\n}\n\npub fn getAltKey(self: *const MouseEvent) bool {\n    return self._alt_key;\n}\n\npub fn getButton(self: *const MouseEvent) u8 {\n    return @intFromEnum(self._button);\n}\n\npub fn getButtons(self: *const MouseEvent) u16 {\n    return self._buttons;\n}\n\npub fn getClientX(self: *const MouseEvent) f64 {\n    return self._client_x;\n}\n\npub fn getClientY(self: *const MouseEvent) f64 {\n    return self._client_y;\n}\n\npub fn getCtrlKey(self: *const MouseEvent) bool {\n    return self._ctrl_key;\n}\n\npub fn getMetaKey(self: *const MouseEvent) bool {\n    return self._meta_key;\n}\n\npub fn getPageX(self: *const MouseEvent) f64 {\n    // this should be clientX + window.scrollX\n    return self._client_x;\n}\n\npub fn getPageY(self: *const MouseEvent) f64 {\n    // this should be clientY + window.scrollY\n    return self._client_y;\n}\n\npub fn getRelatedTarget(self: *const MouseEvent) ?*EventTarget {\n    return self._related_target;\n}\n\npub fn getScreenX(self: *const MouseEvent) f64 {\n    return self._screen_x;\n}\n\npub fn getScreenY(self: *const MouseEvent) f64 {\n    return self._screen_y;\n}\n\npub fn getShiftKey(self: *const MouseEvent) bool {\n    return self._shift_key;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MouseEvent);\n\n    pub const Meta = struct {\n        pub const name = \"MouseEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(MouseEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(MouseEvent.init, .{});\n    pub const altKey = bridge.accessor(getAltKey, null, .{});\n    pub const button = bridge.accessor(getButton, null, .{});\n    pub const buttons = bridge.accessor(getButtons, null, .{});\n    pub const clientX = bridge.accessor(getClientX, null, .{});\n    pub const clientY = bridge.accessor(getClientY, null, .{});\n    pub const ctrlKey = bridge.accessor(getCtrlKey, null, .{});\n    pub const metaKey = bridge.accessor(getMetaKey, null, .{});\n    pub const offsetX = bridge.property(0.0, .{ .template = false });\n    pub const offsetY = bridge.property(0.0, .{ .template = false });\n    pub const pageX = bridge.accessor(getPageX, null, .{});\n    pub const pageY = bridge.accessor(getPageY, null, .{});\n    pub const relatedTarget = bridge.accessor(getRelatedTarget, null, .{});\n    pub const screenX = bridge.accessor(getScreenX, null, .{});\n    pub const screenY = bridge.accessor(getScreenY, null, .{});\n    pub const shiftKey = bridge.accessor(getShiftKey, null, .{});\n    pub const x = bridge.accessor(getClientX, null, .{});\n    pub const y = bridge.accessor(getClientY, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: MouseEvent\" {\n    try testing.htmlRunner(\"event/mouse.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst NavigationHistoryEntry = @import(\"../navigation/NavigationHistoryEntry.zig\");\nconst NavigationType = @import(\"../navigation/root.zig\").NavigationType;\nconst Allocator = std.mem.Allocator;\n\nconst NavigationCurrentEntryChangeEvent = @This();\n\n_proto: *Event,\n_from: *NavigationHistoryEntry,\n_navigation_type: ?NavigationType,\n\nconst NavigationCurrentEntryChangeEventOptions = struct {\n    from: *NavigationHistoryEntry,\n    navigationType: ?[]const u8 = null,\n};\n\nconst Options = Event.inheritOptions(\n    NavigationCurrentEntryChangeEvent,\n    NavigationCurrentEntryChangeEventOptions,\n);\n\npub fn init(typ: []const u8, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {\n    const arena = try page.getArena(.{ .debug = \"NavigationCurrentEntryChangeEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, opts, false, page);\n}\n\npub fn initTrusted(typ: String, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {\n    const arena = try page.getArena(.{ .debug = \"NavigationCurrentEntryChangeEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, opts, true, page);\n}\n\nfn initWithTrusted(\n    arena: Allocator,\n    typ: String,\n    opts: Options,\n    trusted: bool,\n    page: *Page,\n) !*NavigationCurrentEntryChangeEvent {\n    const navigation_type = if (opts.navigationType) |nav_type_str|\n        std.meta.stringToEnum(NavigationType, nav_type_str)\n    else\n        null;\n\n    const event = try page._factory.event(\n        arena,\n        typ,\n        NavigationCurrentEntryChangeEvent{\n            ._proto = undefined,\n            ._from = opts.from,\n            ._navigation_type = navigation_type,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {\n    return self._proto;\n}\n\npub fn getFrom(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry {\n    return self._from;\n}\n\npub fn getNavigationType(self: *const NavigationCurrentEntryChangeEvent) ?[]const u8 {\n    return if (self._navigation_type) |nav_type| @tagName(nav_type) else null;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(NavigationCurrentEntryChangeEvent);\n\n    pub const Meta = struct {\n        pub const name = \"NavigationCurrentEntryChangeEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(NavigationCurrentEntryChangeEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{});\n    pub const from = bridge.accessor(NavigationCurrentEntryChangeEvent.getFrom, null, .{});\n    pub const navigationType = bridge.accessor(NavigationCurrentEntryChangeEvent.getNavigationType, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/event/PageTransitionEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\n// https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent\nconst PageTransitionEvent = @This();\n\n_proto: *Event,\n_persisted: bool,\n\nconst PageTransitionEventOptions = struct {\n    persisted: ?bool = false,\n};\n\nconst Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent {\n    const arena = try page.getArena(.{ .debug = \"PageTransitionEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*PageTransitionEvent {\n    const arena = try page.getArena(.{ .debug = \"PageTransitionEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*PageTransitionEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.event(\n        arena,\n        typ,\n        PageTransitionEvent{\n            ._proto = undefined,\n            ._persisted = opts.persisted orelse false,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *PageTransitionEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *PageTransitionEvent) *Event {\n    return self._proto;\n}\n\npub fn getPersisted(self: *PageTransitionEvent) bool {\n    return self._persisted;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(PageTransitionEvent);\n\n    pub const Meta = struct {\n        pub const name = \"PageTransitionEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(PageTransitionEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(PageTransitionEvent.init, .{});\n    pub const persisted = bridge.accessor(PageTransitionEvent.getPersisted, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/event/PointerEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst MouseEvent = @import(\"MouseEvent.zig\");\n\nconst PointerEvent = @This();\n\nconst PointerType = enum {\n    empty,\n    mouse,\n    pen,\n    touch,\n\n    fn fromString(s: []const u8) PointerType {\n        if (std.mem.eql(u8, s, \"\")) return .empty;\n        if (std.mem.eql(u8, s, \"mouse\")) return .mouse;\n        if (std.mem.eql(u8, s, \"pen\")) return .pen;\n        if (std.mem.eql(u8, s, \"touch\")) return .touch;\n        return .empty;\n    }\n\n    fn toString(self: PointerType) []const u8 {\n        return switch (self) {\n            .empty => \"\",\n            inline else => |pt| @tagName(pt),\n        };\n    }\n};\n\n_proto: *MouseEvent,\n_pointer_id: i32,\n_pointer_type: PointerType,\n_width: f64,\n_height: f64,\n_pressure: f64,\n_tangential_pressure: f64,\n_tilt_x: i32,\n_tilt_y: i32,\n_twist: i32,\n_altitude_angle: f64,\n_azimuth_angle: f64,\n_is_primary: bool,\n\npub const PointerEventOptions = struct {\n    pointerId: i32 = 0,\n    pointerType: []const u8 = \"\",\n    width: f64 = 1.0,\n    height: f64 = 1.0,\n    pressure: f64 = 0.0,\n    tangentialPressure: f64 = 0.0,\n    tiltX: i32 = 0,\n    tiltY: i32 = 0,\n    twist: i32 = 0,\n    altitudeAngle: f64 = std.math.pi / 2.0,\n    azimuthAngle: f64 = 0.0,\n    isPrimary: bool = false,\n};\n\nconst Options = Event.inheritOptions(\n    PointerEvent,\n    PointerEventOptions,\n);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {\n    const arena = try page.getArena(.{ .debug = \"UIEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = _opts orelse Options{};\n    const event = try page._factory.mouseEvent(\n        arena,\n        type_string,\n        MouseEvent{\n            ._type = .{ .pointer_event = undefined },\n            ._proto = undefined,\n            ._screen_x = opts.screenX,\n            ._screen_y = opts.screenY,\n            ._client_x = opts.clientX,\n            ._client_y = opts.clientY,\n            ._ctrl_key = opts.ctrlKey,\n            ._shift_key = opts.shiftKey,\n            ._alt_key = opts.altKey,\n            ._meta_key = opts.metaKey,\n            ._button = std.meta.intToEnum(MouseEvent.MouseButton, opts.button) catch return error.TypeError,\n            ._buttons = opts.buttons,\n            ._related_target = opts.relatedTarget,\n        },\n        PointerEvent{\n            ._proto = undefined,\n            ._pointer_id = opts.pointerId,\n            ._pointer_type = PointerType.fromString(opts.pointerType),\n            ._width = opts.width,\n            ._height = opts.height,\n            ._pressure = opts.pressure,\n            ._tangential_pressure = opts.tangentialPressure,\n            ._tilt_x = opts.tiltX,\n            ._tilt_y = opts.tiltY,\n            ._twist = opts.twist,\n            ._altitude_angle = opts.altitudeAngle,\n            ._azimuth_angle = opts.azimuthAngle,\n            ._is_primary = opts.isPrimary,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn deinit(self: *PointerEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *PointerEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn getPointerId(self: *const PointerEvent) i32 {\n    return self._pointer_id;\n}\n\npub fn getPointerType(self: *const PointerEvent) []const u8 {\n    return self._pointer_type.toString();\n}\n\npub fn getWidth(self: *const PointerEvent) f64 {\n    return self._width;\n}\n\npub fn getHeight(self: *const PointerEvent) f64 {\n    return self._height;\n}\n\npub fn getPressure(self: *const PointerEvent) f64 {\n    return self._pressure;\n}\n\npub fn getTangentialPressure(self: *const PointerEvent) f64 {\n    return self._tangential_pressure;\n}\n\npub fn getTiltX(self: *const PointerEvent) i32 {\n    return self._tilt_x;\n}\n\npub fn getTiltY(self: *const PointerEvent) i32 {\n    return self._tilt_y;\n}\n\npub fn getTwist(self: *const PointerEvent) i32 {\n    return self._twist;\n}\n\npub fn getAltitudeAngle(self: *const PointerEvent) f64 {\n    return self._altitude_angle;\n}\n\npub fn getAzimuthAngle(self: *const PointerEvent) f64 {\n    return self._azimuth_angle;\n}\n\npub fn getIsPrimary(self: *const PointerEvent) bool {\n    return self._is_primary;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(PointerEvent);\n\n    pub const Meta = struct {\n        pub const name = \"PointerEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(PointerEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(PointerEvent.init, .{});\n    pub const pointerId = bridge.accessor(PointerEvent.getPointerId, null, .{});\n    pub const pointerType = bridge.accessor(PointerEvent.getPointerType, null, .{});\n    pub const width = bridge.accessor(PointerEvent.getWidth, null, .{});\n    pub const height = bridge.accessor(PointerEvent.getHeight, null, .{});\n    pub const pressure = bridge.accessor(PointerEvent.getPressure, null, .{});\n    pub const tangentialPressure = bridge.accessor(PointerEvent.getTangentialPressure, null, .{});\n    pub const tiltX = bridge.accessor(PointerEvent.getTiltX, null, .{});\n    pub const tiltY = bridge.accessor(PointerEvent.getTiltY, null, .{});\n    pub const twist = bridge.accessor(PointerEvent.getTwist, null, .{});\n    pub const altitudeAngle = bridge.accessor(PointerEvent.getAltitudeAngle, null, .{});\n    pub const azimuthAngle = bridge.accessor(PointerEvent.getAzimuthAngle, null, .{});\n    pub const isPrimary = bridge.accessor(PointerEvent.getIsPrimary, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: PointerEvent\" {\n    try testing.htmlRunner(\"event/pointer.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/PopStateEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\n// https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent\nconst PopStateEvent = @This();\n\n_proto: *Event,\n_state: ?[]const u8,\n\nconst PopStateEventOptions = struct {\n    state: ?[]const u8 = null,\n};\n\nconst Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent {\n    const arena = try page.getArena(.{ .debug = \"PopStateEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*PopStateEvent {\n    const arena = try page.getArena(.{ .debug = \"PopStateEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*PopStateEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.event(\n        arena,\n        typ,\n        PopStateEvent{\n            ._proto = undefined,\n            ._state = opts.state,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *PopStateEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *PopStateEvent) *Event {\n    return self._proto;\n}\n\npub fn getState(self: *PopStateEvent, page: *Page) !?js.Value {\n    const s = self._state orelse return null;\n    return try page.js.local.?.parseJSON(s);\n}\n\npub fn hasUAVisualTransition(_: *PopStateEvent) bool {\n    // Not currently supported  so we always return false;\n    return false;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(PopStateEvent);\n\n    pub const Meta = struct {\n        pub const name = \"PopStateEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(PopStateEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(PopStateEvent.init, .{});\n    pub const state = bridge.accessor(PopStateEvent.getState, null, .{});\n    pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.hasUAVisualTransition, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/event/ProgressEvent.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst ProgressEvent = @This();\n_proto: *Event,\n_total: usize = 0,\n_loaded: usize = 0,\n_length_computable: bool = false,\n\nconst ProgressEventOptions = struct {\n    total: usize = 0,\n    loaded: usize = 0,\n    lengthComputable: bool = false,\n};\n\nconst Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent {\n    const arena = try page.getArena(.{ .debug = \"ProgressEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n    return initWithTrusted(arena, type_string, _opts, false, page);\n}\n\npub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*ProgressEvent {\n    const arena = try page.getArena(.{ .debug = \"ProgressEvent.trusted\" });\n    errdefer page.releaseArena(arena);\n    return initWithTrusted(arena, typ, _opts, true, page);\n}\n\nfn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*ProgressEvent {\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.event(\n        arena,\n        typ,\n        ProgressEvent{\n            ._proto = undefined,\n            ._total = opts.total,\n            ._loaded = opts.loaded,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, trusted);\n    return event;\n}\n\npub fn deinit(self: *ProgressEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *ProgressEvent) *Event {\n    return self._proto;\n}\n\npub fn getTotal(self: *const ProgressEvent) usize {\n    return self._total;\n}\n\npub fn getLoaded(self: *const ProgressEvent) usize {\n    return self._loaded;\n}\n\npub fn getLengthComputable(self: *const ProgressEvent) bool {\n    return self._length_computable;\n}\n\npub const JsApi = struct {\n    const js = @import(\"../../js/js.zig\");\n    pub const bridge = js.Bridge(ProgressEvent);\n\n    pub const Meta = struct {\n        pub const name = \"ProgressEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(ProgressEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(ProgressEvent.init, .{});\n    pub const total = bridge.accessor(ProgressEvent.getTotal, null, .{});\n    pub const loaded = bridge.accessor(ProgressEvent.getLoaded, null, .{});\n    pub const lengthComputable = bridge.accessor(ProgressEvent.getLengthComputable, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/event/PromiseRejectionEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst PromiseRejectionEvent = @This();\n\n_proto: *Event,\n_reason: ?js.Value.Temp = null,\n_promise: ?js.Promise.Temp = null,\n\nconst PromiseRejectionEventOptions = struct {\n    reason: ?js.Value.Temp = null,\n    promise: ?js.Promise.Temp = null,\n};\n\nconst Options = Event.inheritOptions(PromiseRejectionEvent, PromiseRejectionEventOptions);\n\npub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEvent {\n    const arena = try page.getArena(.{ .debug = \"PromiseRejectionEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = opts_ orelse Options{};\n    const event = try page._factory.event(\n        arena,\n        type_string,\n        PromiseRejectionEvent{\n            ._proto = undefined,\n            ._reason = opts.reason,\n            ._promise = opts.promise,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, session: *Session) void {\n    if (self._reason) |r| {\n        r.release();\n    }\n    if (self._promise) |p| {\n        p.release();\n    }\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *PromiseRejectionEvent) *Event {\n    return self._proto;\n}\n\npub fn getReason(self: *const PromiseRejectionEvent) ?js.Value.Temp {\n    return self._reason;\n}\n\npub fn getPromise(self: *const PromiseRejectionEvent) ?js.Promise.Temp {\n    return self._promise;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(PromiseRejectionEvent);\n\n    pub const Meta = struct {\n        pub const name = \"PromiseRejectionEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(PromiseRejectionEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{});\n    pub const reason = bridge.accessor(PromiseRejectionEvent.getReason, null, .{});\n    pub const promise = bridge.accessor(PromiseRejectionEvent.getPromise, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: PromiseRejectionEvent\" {\n    try testing.htmlRunner(\"event/promise_rejection.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/TextEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst UIEvent = @import(\"UIEvent.zig\");\n\nconst TextEvent = @This();\n\n_proto: *UIEvent,\n_data: []const u8 = \"\",\n\npub const TextEventOptions = struct {\n    data: ?[]const u8 = null,\n};\n\npub const Options = Event.inheritOptions(\n    TextEvent,\n    TextEventOptions,\n);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent {\n    const arena = try page.getArena(.{ .debug = \"TextEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.uiEvent(\n        arena,\n        type_string,\n        TextEvent{\n            ._proto = undefined,\n            ._data = if (opts.data) |str| try arena.dupe(u8, str) else \"\",\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn deinit(self: *TextEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *TextEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn getData(self: *const TextEvent) []const u8 {\n    return self._data;\n}\n\npub fn initTextEvent(\n    self: *TextEvent,\n    typ: []const u8,\n    bubbles: bool,\n    cancelable: bool,\n    view: ?*@import(\"../Window.zig\"),\n    data: []const u8,\n) !void {\n    _ = view; // view parameter is ignored in modern implementations\n\n    const event = self._proto._proto;\n    if (event._event_phase != .none) {\n        // Only allow initialization if event hasn't been dispatched\n        return;\n    }\n\n    const arena = event._arena;\n    event._type_string = try String.init(arena, typ, .{});\n    event._bubbles = bubbles;\n    event._cancelable = cancelable;\n    self._data = try arena.dupe(u8, data);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextEvent);\n\n    pub const Meta = struct {\n        pub const name = \"TextEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(TextEvent.deinit);\n    };\n\n    // No constructor - TextEvent is created via document.createEvent('TextEvent')\n    pub const data = bridge.accessor(TextEvent.getData, null, .{});\n    pub const initTextEvent = bridge.function(TextEvent.initTextEvent, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: TextEvent\" {\n    try testing.htmlRunner(\"event/text.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/UIEvent.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst String = @import(\"../../../string.zig\").String;\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst Window = @import(\"../Window.zig\");\n\nconst UIEvent = @This();\n\n_type: Type,\n_proto: *Event,\n_detail: u32 = 0,\n_view: ?*Window = null,\n\npub const Type = union(enum) {\n    generic,\n    mouse_event: *@import(\"MouseEvent.zig\"),\n    keyboard_event: *@import(\"KeyboardEvent.zig\"),\n    focus_event: *@import(\"FocusEvent.zig\"),\n    text_event: *@import(\"TextEvent.zig\"),\n    input_event: *@import(\"InputEvent.zig\"),\n};\n\npub const UIEventOptions = struct {\n    detail: u32 = 0,\n    view: ?*Window = null,\n};\n\npub const Options = Event.inheritOptions(\n    UIEvent,\n    UIEventOptions,\n);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {\n    const arena = try page.getArena(.{ .debug = \"UIEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = _opts orelse Options{};\n    const event = try page._factory.event(\n        arena,\n        type_string,\n        UIEvent{\n            ._type = .generic,\n            ._proto = undefined,\n            ._detail = opts.detail,\n            ._view = opts.view orelse page.window,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn deinit(self: *UIEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn as(self: *UIEvent, comptime T: type) *T {\n    return self.is(T).?;\n}\n\npub fn is(self: *UIEvent, comptime T: type) ?*T {\n    switch (self._type) {\n        .generic => return if (T == UIEvent) self else null,\n        .mouse_event => |e| {\n            if (T == @import(\"MouseEvent.zig\")) return e;\n            return e.is(T);\n        },\n        .keyboard_event => |e| return if (T == @import(\"KeyboardEvent.zig\")) e else null,\n        .focus_event => |e| return if (T == @import(\"FocusEvent.zig\")) e else null,\n        .text_event => |e| return if (T == @import(\"TextEvent.zig\")) e else null,\n        .input_event => |e| return if (T == @import(\"InputEvent.zig\")) e else null,\n    }\n    return null;\n}\n\npub fn populateFromOptions(self: *UIEvent, opts: anytype) void {\n    self._detail = opts.detail;\n    self._view = opts.view;\n}\n\npub fn asEvent(self: *UIEvent) *Event {\n    return self._proto;\n}\n\npub fn getDetail(self: *UIEvent) u32 {\n    return self._detail;\n}\n\n// sourceCapabilities not implemented\n\npub fn getView(self: *UIEvent, page: *Page) *Window {\n    return self._view orelse page.window;\n}\n\n// deprecated `initUIEvent()` not implemented\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(UIEvent);\n\n    pub const Meta = struct {\n        pub const name = \"UIEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(UIEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(UIEvent.init, .{});\n    pub const detail = bridge.accessor(UIEvent.getDetail, null, .{});\n    pub const view = bridge.accessor(UIEvent.getView, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: UIEvent\" {\n    try testing.htmlRunner(\"event/ui.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/event/WheelEvent.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst String = @import(\"../../../string.zig\").String;\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst MouseEvent = @import(\"MouseEvent.zig\");\n\nconst WheelEvent = @This();\n\n_proto: *MouseEvent,\n_delta_x: f64,\n_delta_y: f64,\n_delta_z: f64,\n_delta_mode: u32,\n\npub const DOM_DELTA_PIXEL: u32 = 0x00;\npub const DOM_DELTA_LINE: u32 = 0x01;\npub const DOM_DELTA_PAGE: u32 = 0x02;\n\npub const WheelEventOptions = struct {\n    deltaX: f64 = 0.0,\n    deltaY: f64 = 0.0,\n    deltaZ: f64 = 0.0,\n    deltaMode: u32 = 0,\n};\n\npub const Options = Event.inheritOptions(\n    WheelEvent,\n    WheelEventOptions,\n);\n\npub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent {\n    const arena = try page.getArena(.{ .debug = \"WheelEvent\" });\n    errdefer page.releaseArena(arena);\n    const type_string = try String.init(arena, typ, .{});\n\n    const opts = _opts orelse Options{};\n\n    const event = try page._factory.mouseEvent(\n        arena,\n        type_string,\n        MouseEvent{\n            ._type = .{ .wheel_event = undefined },\n            ._proto = undefined,\n            ._screen_x = opts.screenX,\n            ._screen_y = opts.screenY,\n            ._client_x = opts.clientX,\n            ._client_y = opts.clientY,\n            ._ctrl_key = opts.ctrlKey,\n            ._shift_key = opts.shiftKey,\n            ._alt_key = opts.altKey,\n            ._meta_key = opts.metaKey,\n            ._button = std.meta.intToEnum(MouseEvent.MouseButton, opts.button) catch return error.TypeError,\n            ._buttons = opts.buttons,\n            ._related_target = opts.relatedTarget,\n        },\n        WheelEvent{\n            ._proto = undefined,\n            ._delta_x = opts.deltaX,\n            ._delta_y = opts.deltaY,\n            ._delta_z = opts.deltaZ,\n            ._delta_mode = opts.deltaMode,\n        },\n    );\n\n    Event.populatePrototypes(event, opts, false);\n    return event;\n}\n\npub fn deinit(self: *WheelEvent, shutdown: bool, session: *Session) void {\n    self._proto.deinit(shutdown, session);\n}\n\npub fn asEvent(self: *WheelEvent) *Event {\n    return self._proto.asEvent();\n}\n\npub fn getDeltaX(self: *const WheelEvent) f64 {\n    return self._delta_x;\n}\n\npub fn getDeltaY(self: *const WheelEvent) f64 {\n    return self._delta_y;\n}\n\npub fn getDeltaZ(self: *const WheelEvent) f64 {\n    return self._delta_z;\n}\n\npub fn getDeltaMode(self: *const WheelEvent) u32 {\n    return self._delta_mode;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(WheelEvent);\n\n    pub const Meta = struct {\n        pub const name = \"WheelEvent\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(WheelEvent.deinit);\n    };\n\n    pub const constructor = bridge.constructor(WheelEvent.init, .{});\n    pub const deltaX = bridge.accessor(WheelEvent.getDeltaX, null, .{});\n    pub const deltaY = bridge.accessor(WheelEvent.getDeltaY, null, .{});\n    pub const deltaZ = bridge.accessor(WheelEvent.getDeltaZ, null, .{});\n    pub const deltaMode = bridge.accessor(WheelEvent.getDeltaMode, null, .{});\n    pub const DOM_DELTA_PIXEL = bridge.property(WheelEvent.DOM_DELTA_PIXEL, .{ .template = true });\n    pub const DOM_DELTA_LINE = bridge.property(WheelEvent.DOM_DELTA_LINE, .{ .template = true });\n    pub const DOM_DELTA_PAGE = bridge.property(WheelEvent.DOM_DELTA_PAGE, .{ .template = true });\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: WheelEvent\" {\n    try testing.htmlRunner(\"event/wheel.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/global_event_handlers.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst js = @import(\"../js/js.zig\");\n\nconst EventTarget = @import(\"EventTarget.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Key = struct {\n    target: *EventTarget,\n    handler: Handler,\n\n    /// Fuses `target` pointer and `handler` enum; used at hashing.\n    /// NEVER use a fusion to retrieve a pointer back. Portability is not guaranteed.\n    /// See `Context.hash`.\n    fn fuse(self: *const Key) u64 {\n        // Check if we have 3 bits available from alignment of 8.\n        if (comptime IS_DEBUG) {\n            lp.assert(@alignOf(EventTarget) == 8, \"Key.fuse: incorrect alignment\", .{\n                .event_target_alignment = @alignOf(EventTarget),\n            });\n        }\n\n        const ptr = @intFromPtr(self.target) >> 3;\n        if (comptime IS_DEBUG) {\n            lp.assert(ptr < (1 << 57), \"Key.fuse: pointer overflow\", .{ .ptr = ptr });\n        }\n        return ptr | (@as(u64, @intFromEnum(self.handler)) << 57);\n    }\n};\n\nconst Context = struct {\n    pub fn hash(_: @This(), key: Key) u64 {\n        return std.hash.int(key.fuse());\n    }\n\n    pub fn eql(_: @This(), a: Key, b: Key) bool {\n        return a.fuse() == b.fuse();\n    }\n};\n\npub const Lookup = std.HashMapUnmanaged(\n    Key,\n    js.Function.Global,\n    Context,\n    std.hash_map.default_max_load_percentage,\n);\n\n/// Enum of known event listeners; increasing the size of it (u7)\n/// can cause `Key` to behave incorrectly.\npub const Handler = enum(u7) {\n    onabort,\n    onanimationcancel,\n    onanimationend,\n    onanimationiteration,\n    onanimationstart,\n    onauxclick,\n    onbeforeinput,\n    onbeforematch,\n    onbeforetoggle,\n    onblur,\n    oncancel,\n    oncanplay,\n    oncanplaythrough,\n    onchange,\n    onclick,\n    onclose,\n    oncommand,\n    oncontentvisibilityautostatechange,\n    oncontextlost,\n    oncontextmenu,\n    oncontextrestored,\n    oncopy,\n    oncuechange,\n    oncut,\n    ondblclick,\n    ondrag,\n    ondragend,\n    ondragenter,\n    ondragexit,\n    ondragleave,\n    ondragover,\n    ondragstart,\n    ondrop,\n    ondurationchange,\n    onemptied,\n    onended,\n    onerror,\n    onfocus,\n    onformdata,\n    onfullscreenchange,\n    onfullscreenerror,\n    ongotpointercapture,\n    oninput,\n    oninvalid,\n    onkeydown,\n    onkeypress,\n    onkeyup,\n    onload,\n    onloadeddata,\n    onloadedmetadata,\n    onloadstart,\n    onlostpointercapture,\n    onmousedown,\n    onmousemove,\n    onmouseout,\n    onmouseover,\n    onmouseup,\n    onpaste,\n    onpause,\n    onplay,\n    onplaying,\n    onpointercancel,\n    onpointerdown,\n    onpointerenter,\n    onpointerleave,\n    onpointermove,\n    onpointerout,\n    onpointerover,\n    onpointerrawupdate,\n    onpointerup,\n    onprogress,\n    onratechange,\n    onreset,\n    onresize,\n    onscroll,\n    onscrollend,\n    onsecuritypolicyviolation,\n    onseeked,\n    onseeking,\n    onselect,\n    onselectionchange,\n    onselectstart,\n    onslotchange,\n    onstalled,\n    onsubmit,\n    onsuspend,\n    ontimeupdate,\n    ontoggle,\n    ontransitioncancel,\n    ontransitionend,\n    ontransitionrun,\n    ontransitionstart,\n    onvolumechange,\n    onwaiting,\n    onwheel,\n};\n\nconst typeToHandler = std.StaticStringMap(Handler).initComptime(blk: {\n    const fields = std.meta.fields(Handler);\n    var entries: [fields.len]struct { []const u8, Handler } = undefined;\n    for (fields, 0..) |field, i| {\n        entries[i] = .{ field.name[2..], @enumFromInt(field.value) };\n    }\n    break :blk entries;\n});\n\npub fn fromEventType(typ: []const u8) ?Handler {\n    return typeToHandler.get(typ);\n}\n\nconst testing = @import(\"../../testing.zig\");\ntest \"GlobalEventHandlers: fromEventType\" {\n    try testing.expectEqual(.onabort, fromEventType(\"abort\"));\n    try testing.expectEqual(.onselect, fromEventType(\"select\"));\n    try testing.expectEqual(null, fromEventType(\"\"));\n    try testing.expectEqual(null, fromEventType(\"unknown\"));\n}\n"
  },
  {
    "path": "src/browser/webapi/media/MediaError.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst MediaError = @This();\n\n_code: u16,\n_message: []const u8 = \"\",\n\npub fn init(code: u16, message: []const u8, page: *Page) !*MediaError {\n    return page.arena.create(MediaError{\n        ._code = code,\n        ._message = try page.dupeString(message),\n    });\n}\n\npub fn getCode(self: *const MediaError) u16 {\n    return self._code;\n}\n\npub fn getMessage(self: *const MediaError) []const u8 {\n    return self._message;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(MediaError);\n\n    pub const Meta = struct {\n        pub const name = \"MediaError\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    // Error code constants\n    pub const MEDIA_ERR_ABORTED = bridge.property(1, .{ .template = true });\n    pub const MEDIA_ERR_NETWORK = bridge.property(2, .{ .template = true });\n    pub const MEDIA_ERR_DECODE = bridge.property(3, .{ .template = true });\n    pub const MEDIA_ERR_SRC_NOT_SUPPORTED = bridge.property(4, .{ .template = true });\n\n    pub const code = bridge.accessor(MediaError.getCode, null, .{});\n    pub const message = bridge.accessor(MediaError.getMessage, null, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: MediaError\" {\n    try testing.htmlRunner(\"media/mediaerror.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/media/TextTrackCue.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\n\nconst TextTrackCue = @This();\n\n_type: Type,\n_proto: *EventTarget,\n_id: []const u8 = \"\",\n_start_time: f64 = 0,\n_end_time: f64 = 0,\n_pause_on_exit: bool = false,\n_on_enter: ?js.Function.Global = null,\n_on_exit: ?js.Function.Global = null,\n\npub const Type = union(enum) {\n    vtt: *@import(\"VTTCue.zig\"),\n};\n\npub fn asEventTarget(self: *TextTrackCue) *EventTarget {\n    return self._proto;\n}\n\npub fn getId(self: *const TextTrackCue) []const u8 {\n    return self._id;\n}\n\npub fn setId(self: *TextTrackCue, value: []const u8, page: *Page) !void {\n    self._id = try page.dupeString(value);\n}\n\npub fn getStartTime(self: *const TextTrackCue) f64 {\n    return self._start_time;\n}\n\npub fn setStartTime(self: *TextTrackCue, value: f64) void {\n    self._start_time = value;\n}\n\npub fn getEndTime(self: *const TextTrackCue) f64 {\n    return self._end_time;\n}\n\npub fn setEndTime(self: *TextTrackCue, value: f64) void {\n    self._end_time = value;\n}\n\npub fn getPauseOnExit(self: *const TextTrackCue) bool {\n    return self._pause_on_exit;\n}\n\npub fn setPauseOnExit(self: *TextTrackCue, value: bool) void {\n    self._pause_on_exit = value;\n}\n\npub fn getOnEnter(self: *const TextTrackCue) ?js.Function.Global {\n    return self._on_enter;\n}\n\npub fn setOnEnter(self: *TextTrackCue, cb: ?js.Function.Global) !void {\n    self._on_enter = cb;\n}\n\npub fn getOnExit(self: *const TextTrackCue) ?js.Function.Global {\n    return self._on_exit;\n}\n\npub fn setOnExit(self: *TextTrackCue, cb: ?js.Function.Global) !void {\n    self._on_exit = cb;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TextTrackCue);\n\n    pub const Meta = struct {\n        pub const name = \"TextTrackCue\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const Prototype = EventTarget;\n\n    pub const id = bridge.accessor(TextTrackCue.getId, TextTrackCue.setId, .{});\n    pub const startTime = bridge.accessor(TextTrackCue.getStartTime, TextTrackCue.setStartTime, .{});\n    pub const endTime = bridge.accessor(TextTrackCue.getEndTime, TextTrackCue.setEndTime, .{});\n    pub const pauseOnExit = bridge.accessor(TextTrackCue.getPauseOnExit, TextTrackCue.setPauseOnExit, .{});\n    pub const onenter = bridge.accessor(TextTrackCue.getOnEnter, TextTrackCue.setOnEnter, .{});\n    pub const onexit = bridge.accessor(TextTrackCue.getOnExit, TextTrackCue.setOnExit, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/media/VTTCue.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst TextTrackCue = @import(\"TextTrackCue.zig\");\n\nconst VTTCue = @This();\n\n_proto: *TextTrackCue,\n_text: []const u8 = \"\",\n_region: ?js.Object.Global = null,\n_vertical: []const u8 = \"\",\n_snap_to_lines: bool = true,\n_line: ?f64 = null, // null represents \"auto\"\n_position: ?f64 = null, // null represents \"auto\"\n_size: f64 = 100,\n_align: []const u8 = \"center\",\n\npub fn constructor(start_time: f64, end_time: f64, text: []const u8, page: *Page) !*VTTCue {\n    const cue = try page._factory.textTrackCue(VTTCue{\n        ._proto = undefined,\n        ._text = try page.dupeString(text),\n        ._region = null,\n        ._vertical = \"\",\n        ._snap_to_lines = true,\n        ._line = null, // \"auto\"\n        ._position = null, // \"auto\"\n        ._size = 100,\n        ._align = \"center\",\n    });\n\n    cue._proto._start_time = start_time;\n    cue._proto._end_time = end_time;\n\n    return cue;\n}\n\npub fn asTextTrackCue(self: *VTTCue) *TextTrackCue {\n    return self._proto;\n}\n\npub fn getText(self: *const VTTCue) []const u8 {\n    return self._text;\n}\n\npub fn setText(self: *VTTCue, value: []const u8, page: *Page) !void {\n    self._text = try page.dupeString(value);\n}\n\npub fn getRegion(self: *const VTTCue) ?js.Object.Global {\n    return self._region;\n}\n\npub fn setRegion(self: *VTTCue, value: ?js.Object.Global) !void {\n    self._region = value;\n}\n\npub fn getVertical(self: *const VTTCue) []const u8 {\n    return self._vertical;\n}\n\npub fn setVertical(self: *VTTCue, value: []const u8, page: *Page) !void {\n    // Valid values: \"\", \"rl\", \"lr\"\n    self._vertical = try page.dupeString(value);\n}\n\npub fn getSnapToLines(self: *const VTTCue) bool {\n    return self._snap_to_lines;\n}\n\npub fn setSnapToLines(self: *VTTCue, value: bool) void {\n    self._snap_to_lines = value;\n}\n\npub const LineAndPositionSetting = union(enum) {\n    number: f64,\n    auto: []const u8,\n};\n\npub fn getLine(self: *const VTTCue) LineAndPositionSetting {\n    if (self._line) |num| {\n        return .{ .number = num };\n    }\n    return .{ .auto = \"auto\" };\n}\n\npub fn setLine(self: *VTTCue, value: LineAndPositionSetting) void {\n    switch (value) {\n        .number => |num| self._line = num,\n        .auto => self._line = null,\n    }\n}\n\npub fn getPosition(self: *const VTTCue) LineAndPositionSetting {\n    if (self._position) |num| {\n        return .{ .number = num };\n    }\n    return .{ .auto = \"auto\" };\n}\n\npub fn setPosition(self: *VTTCue, value: LineAndPositionSetting) void {\n    switch (value) {\n        .number => |num| self._position = num,\n        .auto => self._position = null,\n    }\n}\n\npub fn getSize(self: *const VTTCue) f64 {\n    return self._size;\n}\n\npub fn setSize(self: *VTTCue, value: f64) void {\n    self._size = value;\n}\n\npub fn getAlign(self: *const VTTCue) []const u8 {\n    return self._align;\n}\n\npub fn setAlign(self: *VTTCue, value: []const u8, page: *Page) !void {\n    // Valid values: \"start\", \"center\", \"end\", \"left\", \"right\"\n    self._align = try page.dupeString(value);\n}\n\npub fn getCueAsHTML(self: *const VTTCue, page: *Page) !js.Object {\n    // Minimal implementation: return a document fragment\n    // In a full implementation, this would parse the VTT text into HTML nodes\n    _ = self;\n    _ = page;\n    return error.NotImplemented;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(VTTCue);\n\n    pub const Meta = struct {\n        pub const name = \"VTTCue\";\n\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const Prototype = TextTrackCue;\n\n    pub const constructor = bridge.constructor(VTTCue.constructor, .{});\n    pub const text = bridge.accessor(VTTCue.getText, VTTCue.setText, .{});\n    pub const region = bridge.accessor(VTTCue.getRegion, VTTCue.setRegion, .{});\n    pub const vertical = bridge.accessor(VTTCue.getVertical, VTTCue.setVertical, .{});\n    pub const snapToLines = bridge.accessor(VTTCue.getSnapToLines, VTTCue.setSnapToLines, .{});\n    pub const line = bridge.accessor(VTTCue.getLine, VTTCue.setLine, .{});\n    pub const position = bridge.accessor(VTTCue.getPosition, VTTCue.setPosition, .{});\n    pub const size = bridge.accessor(VTTCue.getSize, VTTCue.setSize, .{});\n    pub const @\"align\" = bridge.accessor(VTTCue.getAlign, VTTCue.setAlign, .{});\n    pub const getCueAsHTML = bridge.function(VTTCue.getCueAsHTML, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: VTTCue\" {\n    try testing.htmlRunner(\"media/vttcue.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/navigation/Navigation.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"../../../log.zig\");\nconst URL = @import(\"../URL.zig\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst Event = @import(\"../Event.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Navigation\nconst Navigation = @This();\n\nconst NavigationKind = @import(\"root.zig\").NavigationKind;\nconst NavigationActivation = @import(\"NavigationActivation.zig\");\nconst NavigationTransition = @import(\"root.zig\").NavigationTransition;\nconst NavigationState = @import(\"root.zig\").NavigationState;\n\nconst NavigationHistoryEntry = @import(\"NavigationHistoryEntry.zig\");\nconst NavigationCurrentEntryChangeEvent = @import(\"../event/NavigationCurrentEntryChangeEvent.zig\");\n\n_proto: *EventTarget,\n_on_currententrychange: ?js.Function.Global = null,\n\n_current_navigation_kind: ?NavigationKind = null,\n\n_index: usize = 0,\n// Need to be stable pointers, because Events can reference entries.\n_entries: std.ArrayList(*NavigationHistoryEntry) = .empty,\n_next_entry_id: usize = 0,\n_activation: ?NavigationActivation = null,\n\nfn asEventTarget(self: *Navigation) *EventTarget {\n    return self._proto;\n}\n\npub fn onRemovePage(self: *Navigation) void {\n    self._proto = undefined;\n}\n\npub fn onNewPage(self: *Navigation, page: *Page) !void {\n    self._proto = try page._factory.standaloneEventTarget(self);\n}\n\npub fn getActivation(self: *const Navigation) ?NavigationActivation {\n    return self._activation;\n}\n\npub fn getCanGoBack(self: *const Navigation) bool {\n    return self._index > 0;\n}\n\npub fn getCanGoForward(self: *const Navigation) bool {\n    return self._entries.items.len > self._index + 1;\n}\n\npub fn getCurrentEntryOrNull(self: *Navigation) ?*NavigationHistoryEntry {\n    if (self._entries.items.len > self._index) {\n        return self._entries.items[self._index];\n    } else return null;\n}\n\npub fn getCurrentEntry(self: *Navigation) *NavigationHistoryEntry {\n    // This should never fail. An entry should always be created before\n    // we run the scripts on the page we are loading.\n    const len = self._entries.items.len;\n    lp.assert(len > 0, \"Navigation.getCurrentEntry\", .{ .len = len });\n\n    return self.getCurrentEntryOrNull().?;\n}\n\npub fn getTransition(_: *const Navigation) ?NavigationTransition {\n    // For now, all transitions are just considered complete.\n    return null;\n}\n\nconst NavigationReturn = struct {\n    committed: js.Promise.Global,\n    finished: js.Promise.Global,\n};\n\npub fn back(self: *Navigation, page: *Page) !NavigationReturn {\n    if (!self.getCanGoBack()) {\n        return error.InvalidStateError;\n    }\n\n    const new_index = self._index - 1;\n    const next_entry = self._entries.items[new_index];\n\n    return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page);\n}\n\npub fn entries(self: *const Navigation) []*NavigationHistoryEntry {\n    return self._entries.items;\n}\n\npub fn forward(self: *Navigation, page: *Page) !NavigationReturn {\n    if (!self.getCanGoForward()) {\n        return error.InvalidStateError;\n    }\n\n    const new_index = self._index + 1;\n    const next_entry = self._entries.items[new_index];\n\n    return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page);\n}\n\npub fn updateEntries(\n    self: *Navigation,\n    url: [:0]const u8,\n    kind: NavigationKind,\n    page: *Page,\n    should_dispatch: bool,\n) !void {\n    switch (kind) {\n        .replace => |state| {\n            _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, should_dispatch);\n        },\n        .push => |state| {\n            _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, should_dispatch);\n        },\n        .traverse => |index| {\n            self._index = index;\n        },\n        .reload => {},\n    }\n}\n\n// This is for after true navigation processing, where we need to ensure that our entries are up to date.\n//\n// This is only really safe to run in the `pageDoneCallback`\n// where we can guarantee that the URL and NavigationKind are correct.\npub fn commitNavigation(self: *Navigation, page: *Page) !void {\n    const url = page.url;\n\n    const kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null };\n    defer self._current_navigation_kind = null;\n\n    const from_entry = self.getCurrentEntryOrNull();\n    try self.updateEntries(url, kind, page, false);\n\n    self._activation = NavigationActivation{\n        ._from = from_entry,\n        ._entry = self.getCurrentEntry(),\n        ._type = kind.toNavigationType(),\n    };\n}\n\n/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.\n/// For that, use `navigate`.\npub fn pushEntry(\n    self: *Navigation,\n    _url: [:0]const u8,\n    state: NavigationState,\n    page: *Page,\n    should_dispatch: bool,\n) !*NavigationHistoryEntry {\n    const arena = page._session.arena;\n    const url = try arena.dupeZ(u8, _url);\n\n    // truncates our history here.\n    if (self._entries.items.len > self._index + 1) {\n        self._entries.shrinkRetainingCapacity(self._index + 1);\n    }\n\n    const index = self._entries.items.len;\n\n    const id = self._next_entry_id;\n    self._next_entry_id += 1;\n\n    const id_str = try std.fmt.allocPrint(arena, \"{d}\", .{id});\n\n    const entry = try arena.create(NavigationHistoryEntry);\n    entry.* = NavigationHistoryEntry{\n        ._id = id_str,\n        ._key = id_str,\n        ._url = url,\n        ._state = state,\n    };\n\n    // we don't always have a current entry...\n    const previous = if (self._entries.items.len > 0) self.getCurrentEntry() else null;\n    try self._entries.append(arena, entry);\n    self._index = index;\n\n    if (previous == null or should_dispatch == false) {\n        return entry;\n    }\n\n    if (self._on_currententrychange) |cec| {\n        const event = (try NavigationCurrentEntryChangeEvent.initTrusted(\n            .wrap(\"currententrychange\"),\n            .{ .from = previous.?, .navigationType = @tagName(.push) },\n            page,\n        )).asEvent();\n        try self.dispatch(cec, event, page);\n    }\n\n    return entry;\n}\n\npub fn replaceEntry(\n    self: *Navigation,\n    _url: [:0]const u8,\n    state: NavigationState,\n    page: *Page,\n    should_dispatch: bool,\n) !*NavigationHistoryEntry {\n    const arena = page._session.arena;\n    const url = try arena.dupeZ(u8, _url);\n\n    const previous = self.getCurrentEntry();\n\n    const id = self._next_entry_id;\n    self._next_entry_id += 1;\n    const id_str = try std.fmt.allocPrint(arena, \"{d}\", .{id});\n\n    const entry = try arena.create(NavigationHistoryEntry);\n    entry.* = NavigationHistoryEntry{\n        ._id = id_str,\n        ._key = previous._key,\n        ._url = url,\n        ._state = state,\n    };\n\n    self._entries.items[self._index] = entry;\n\n    if (should_dispatch == false) {\n        return entry;\n    }\n\n    if (self._on_currententrychange) |cec| {\n        const event = (try NavigationCurrentEntryChangeEvent.initTrusted(\n            .wrap(\"currententrychange\"),\n            .{ .from = previous, .navigationType = @tagName(.replace) },\n            page,\n        )).asEvent();\n        try self.dispatch(cec, event, page);\n    }\n\n    return entry;\n}\n\nconst NavigateOptions = struct {\n    state: ?js.Value = null,\n    info: ?js.Value = null,\n    history: ?[]const u8 = null,\n};\n\npub fn navigateInner(\n    self: *Navigation,\n    _url: ?[:0]const u8,\n    kind: NavigationKind,\n    page: *Page,\n) !NavigationReturn {\n    const arena = page._session.arena;\n    const url = _url orelse return error.MissingURL;\n\n    // https://github.com/WICG/navigation-api/issues/95\n    //\n    // These will only settle on same-origin navigation (mostly intended for SPAs).\n    // It is fine (and expected) for these to not settle on cross-origin requests :)\n    const local = page.js.local.?;\n    const committed = local.createPromiseResolver();\n    const finished = local.createPromiseResolver();\n\n    var new_url = try URL.resolve(arena, page.url, url, .{});\n    const is_same_document = URL.eqlDocument(new_url, page.url);\n\n    // In case of navigation to the same document, we force an url duplication.\n    // Keeping the same url generates a crash during WPT test navigate-history-push-same-url.html.\n    // When building a script's src, script's base and page url overlap.\n    if (is_same_document) {\n        new_url = try arena.dupeZ(u8, new_url);\n    }\n\n    const previous = self.getCurrentEntry();\n\n    switch (kind) {\n        .push => |state| {\n            if (is_same_document) {\n                page.url = new_url;\n\n                committed.resolve(\"navigation push\", {});\n                // todo: Fire navigate event\n                finished.resolve(\"navigation push\", {});\n\n                _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);\n            } else {\n                try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });\n            }\n        },\n        .replace => |state| {\n            if (is_same_document) {\n                page.url = new_url;\n\n                committed.resolve(\"navigation replace\", {});\n                // todo: Fire navigate event\n                finished.resolve(\"navigation replace\", {});\n\n                _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);\n            } else {\n                try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });\n            }\n        },\n        .traverse => |index| {\n            self._index = index;\n\n            if (is_same_document) {\n                page.url = new_url;\n\n                committed.resolve(\"navigation traverse\", {});\n                // todo: Fire navigate event\n                finished.resolve(\"navigation traverse\", {});\n            } else {\n                try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });\n            }\n        },\n        .reload => {\n            try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });\n        },\n    }\n\n    if (self._on_currententrychange) |cec| {\n        // If we haven't navigated off, let us fire off an a currententrychange.\n        const event = (try NavigationCurrentEntryChangeEvent.initTrusted(\n            .wrap(\"currententrychange\"),\n            .{ .from = previous, .navigationType = @tagName(kind) },\n            page,\n        )).asEvent();\n        try self.dispatch(cec, event, page);\n    }\n\n    _ = try committed.persist();\n    _ = try finished.persist();\n    return .{\n        .committed = try committed.promise().persist(),\n        .finished = try finished.promise().persist(),\n    };\n}\n\npub fn navigate(self: *Navigation, _url: [:0]const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {\n    const arena = page._session.arena;\n    const opts = _opts orelse NavigateOptions{};\n    const json = if (opts.state) |state| state.toJson(arena) catch return error.DataClone else null;\n\n    const kind: NavigationKind = if (opts.history) |history|\n        if (std.mem.eql(u8, \"replace\", history)) .{ .replace = json } else .{ .push = json }\n    else\n        .{ .push = json };\n\n    return try self.navigateInner(_url, kind, page);\n}\n\npub const ReloadOptions = struct {\n    state: ?js.Value = null,\n    info: ?js.Value = null,\n};\n\npub fn reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn {\n    const arena = page._session.arena;\n\n    const opts = _opts orelse ReloadOptions{};\n    const entry = self.getCurrentEntry();\n    if (opts.state) |state| {\n        const previous = entry;\n        entry._state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };\n\n        const event = try NavigationCurrentEntryChangeEvent.initTrusted(\n            .wrap(\"currententrychange\"),\n            .{ .from = previous, .navigationType = @tagName(.reload) },\n            page,\n        );\n        try self.dispatch(.{ .currententrychange = event }, page);\n    }\n\n    return self.navigateInner(entry._url, .reload, page);\n}\n\npub const TraverseToOptions = struct {\n    info: ?js.Value = null,\n};\n\npub fn traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn {\n    if (_opts != null) {\n        log.warn(.not_implemented, \"Navigation.traverseTo\", .{ .has_options = true });\n    }\n\n    for (self._entries.items, 0..) |entry, i| {\n        if (std.mem.eql(u8, key, entry._key)) {\n            return try self.navigateInner(entry._url, .{ .traverse = i }, page);\n        }\n    }\n\n    return error.InvalidStateError;\n}\n\npub const UpdateCurrentEntryOptions = struct {\n    state: js.Value,\n};\n\npub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void {\n    const arena = page._session.arena;\n\n    const previous = self.getCurrentEntry();\n    self.getCurrentEntry()._state = .{\n        .source = .navigation,\n        .value = options.state.toJson(arena) catch return error.DataClone,\n    };\n\n    if (self._on_currententrychange) |cec| {\n        const event = (try NavigationCurrentEntryChangeEvent.initTrusted(\n            .wrap(\"currententrychange\"),\n            .{ .from = previous, .navigationType = null },\n            page,\n        )).asEvent();\n        try self.dispatch(cec, event, page);\n    }\n}\n\npub fn dispatch(self: *Navigation, func: js.Function.Global, event: *Event, page: *Page) !void {\n    return page._event_manager.dispatchDirect(\n        self.asEventTarget(),\n        event,\n        func,\n        .{ .context = \"Navigation\" },\n    );\n}\n\nfn getOnCurrentEntryChange(self: *Navigation) ?js.Function.Global {\n    return self._on_currententrychange;\n}\n\npub fn setOnCurrentEntryChange(self: *Navigation, listener: ?js.Function) !void {\n    if (listener) |listen| {\n        self._on_currententrychange = try listen.persistWithThis(self);\n    } else {\n        self._on_currententrychange = null;\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Navigation);\n\n    pub const Meta = struct {\n        pub const name = \"Navigation\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const activation = bridge.accessor(Navigation.getActivation, null, .{});\n    pub const canGoBack = bridge.accessor(Navigation.getCanGoBack, null, .{});\n    pub const canGoForward = bridge.accessor(Navigation.getCanGoForward, null, .{});\n    pub const currentEntry = bridge.accessor(Navigation.getCurrentEntry, null, .{});\n    pub const transition = bridge.accessor(Navigation.getTransition, null, .{});\n    pub const back = bridge.function(Navigation.back, .{ .dom_exception = true });\n    pub const entries = bridge.function(Navigation.entries, .{});\n    pub const forward = bridge.function(Navigation.forward, .{ .dom_exception = true });\n    pub const navigate = bridge.function(Navigation.navigate, .{ .dom_exception = true });\n    pub const traverseTo = bridge.function(Navigation.traverseTo, .{ .dom_exception = true });\n    pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{ .dom_exception = true });\n\n    pub const oncurrententrychange = bridge.accessor(\n        Navigation.getOnCurrentEntryChange,\n        Navigation.setOnCurrentEntryChange,\n        .{},\n    );\n};\n"
  },
  {
    "path": "src/browser/webapi/navigation/NavigationActivation.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst NavigationType = @import(\"root.zig\").NavigationType;\nconst NavigationHistoryEntry = @import(\"NavigationHistoryEntry.zig\");\n\n// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation\nconst NavigationActivation = @This();\n\n_entry: *NavigationHistoryEntry,\n_from: ?*NavigationHistoryEntry = null,\n_type: NavigationType,\n\npub fn getEntry(self: *const NavigationActivation) *NavigationHistoryEntry {\n    return self._entry;\n}\n\npub fn getFrom(self: *const NavigationActivation) ?*NavigationHistoryEntry {\n    return self._from;\n}\n\npub fn getNavigationType(self: *const NavigationActivation) []const u8 {\n    return @tagName(self._type);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(NavigationActivation);\n\n    pub const Meta = struct {\n        pub const name = \"NavigationActivation\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const entry = bridge.accessor(NavigationActivation.getEntry, null, .{});\n    pub const from = bridge.accessor(NavigationActivation.getFrom, null, .{});\n    pub const navigationType = bridge.accessor(NavigationActivation.getNavigationType, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/navigation/NavigationHistoryEntry.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst URL = @import(\"../URL.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\nconst NavigationState = @import(\"root.zig\").NavigationState;\nconst Page = @import(\"../../Page.zig\");\nconst js = @import(\"../../js/js.zig\");\n\nconst NavigationHistoryEntry = @This();\n\n// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry\n// no proto for now\n// _proto: ?*EventTarget,\n_id: []const u8,\n_key: []const u8,\n_url: ?[:0]const u8,\n_state: NavigationState,\n\npub fn id(self: *const NavigationHistoryEntry) []const u8 {\n    return self._id;\n}\n\npub fn index(self: *const NavigationHistoryEntry, page: *Page) i32 {\n    const navigation = &page._session.navigation;\n\n    for (navigation._entries.items, 0..) |entry, i| {\n        if (std.mem.eql(u8, entry._id, self._id)) {\n            return @intCast(i);\n        }\n    }\n\n    return -1;\n}\n\npub fn key(self: *const NavigationHistoryEntry) []const u8 {\n    return self._key;\n}\n\npub fn sameDocument(self: *const NavigationHistoryEntry, page: *Page) bool {\n    const got_url = self._url orelse return false;\n    return URL.eqlDocument(got_url, page.base());\n}\n\npub fn url(self: *const NavigationHistoryEntry) ?[:0]const u8 {\n    return self._url;\n}\n\npub const StateReturn = union(enum) { value: ?js.Value, undefined: void };\n\npub fn getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {\n    if (self._state.source == .navigation) {\n        if (self._state.value) |value| {\n            return .{ .value = try page.js.local.?.parseJSON(value) };\n        }\n    }\n\n    return .undefined;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(NavigationHistoryEntry);\n\n    pub const Meta = struct {\n        pub const name = \"NavigationHistoryEntry\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const id = bridge.accessor(NavigationHistoryEntry.id, null, .{});\n    pub const index = bridge.accessor(NavigationHistoryEntry.index, null, .{});\n    pub const key = bridge.accessor(NavigationHistoryEntry.key, null, .{});\n    pub const sameDocument = bridge.accessor(NavigationHistoryEntry.sameDocument, null, .{});\n    pub const url = bridge.accessor(NavigationHistoryEntry.url, null, .{});\n    pub const getState = bridge.function(NavigationHistoryEntry.getState, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/navigation/root.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\n\nconst NavigationHistoryEntry = @import(\"NavigationHistoryEntry.zig\");\n\npub const NavigationType = enum {\n    push,\n    replace,\n    traverse,\n    reload,\n};\n\npub const NavigationKind = union(NavigationType) {\n    push: ?[]const u8,\n    replace: ?[]const u8,\n    traverse: usize,\n    reload,\n\n    pub fn toNavigationType(self: NavigationKind) NavigationType {\n        return std.meta.activeTag(self);\n    }\n};\n\npub const NavigationState = struct {\n    source: enum { history, navigation },\n    value: ?[]const u8,\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition\npub const NavigationTransition = struct {\n    finished: js.Promise.Global,\n    from: NavigationHistoryEntry,\n    navigation_type: NavigationType,\n};\n"
  },
  {
    "path": "src/browser/webapi/net/Fetch.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst log = @import(\"../../../log.zig\");\nconst HttpClient = @import(\"../../HttpClient.zig\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst URL = @import(\"../../URL.zig\");\n\nconst Blob = @import(\"../Blob.zig\");\nconst Request = @import(\"Request.zig\");\nconst Response = @import(\"Response.zig\");\nconst AbortSignal = @import(\"../AbortSignal.zig\");\nconst DOMException = @import(\"../DOMException.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Fetch = @This();\n\n_page: *Page,\n_url: []const u8,\n_buf: std.ArrayList(u8),\n_response: *Response,\n_resolver: js.PromiseResolver.Global,\n_owns_response: bool,\n_signal: ?*AbortSignal,\n\npub const Input = Request.Input;\npub const InitOpts = Request.InitOpts;\n\npub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {\n    const request = try Request.init(input, options, page);\n    const resolver = page.js.local.?.createPromiseResolver();\n\n    if (request._signal) |signal| {\n        if (signal._aborted) {\n            resolver.reject(\"fetch aborted\", DOMException.init(\"The operation was aborted.\", \"AbortError\"));\n            return resolver.promise();\n        }\n    }\n\n    if (std.mem.startsWith(u8, request._url, \"blob:\")) {\n        return handleBlobUrl(request._url, resolver, page);\n    }\n\n    const response = try Response.init(null, .{ .status = 0 }, page);\n    errdefer response.deinit(true, page._session);\n\n    const fetch = try response._arena.create(Fetch);\n    fetch.* = .{\n        ._page = page,\n        ._buf = .empty,\n        ._url = try response._arena.dupe(u8, request._url),\n        ._resolver = try resolver.persist(),\n        ._response = response,\n        ._owns_response = true,\n        ._signal = request._signal,\n    };\n\n    const http_client = page._session.browser.http_client;\n    var headers = try http_client.newHeaders();\n    if (request._headers) |h| {\n        try h.populateHttpHeader(page.call_arena, &headers);\n    }\n    try page.headersForRequest(page.arena, request._url, &headers);\n\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"fetch\", .{ .url = request._url });\n    }\n\n    try http_client.request(.{\n        .ctx = fetch,\n        .url = request._url,\n        .method = request._method,\n        .frame_id = page._frame_id,\n        .body = request._body,\n        .headers = headers,\n        .resource_type = .fetch,\n        .cookie_jar = &page._session.cookie_jar,\n        .notification = page._session.notification,\n        .start_callback = httpStartCallback,\n        .header_callback = httpHeaderDoneCallback,\n        .data_callback = httpDataCallback,\n        .done_callback = httpDoneCallback,\n        .error_callback = httpErrorCallback,\n        .shutdown_callback = httpShutdownCallback,\n    });\n    return resolver.promise();\n}\n\nfn handleBlobUrl(url: []const u8, resolver: js.PromiseResolver, page: *Page) !js.Promise {\n    const blob: *Blob = page.lookupBlobUrl(url) orelse {\n        resolver.rejectError(\"fetch blob error\", .{ .type_error = \"BlobNotFound\" });\n        return resolver.promise();\n    };\n\n    const response = try Response.init(null, .{ .status = 200 }, page);\n    response._body = try response._arena.dupe(u8, blob._slice);\n    response._url = try response._arena.dupeZ(u8, url);\n    response._type = .basic;\n\n    if (blob._mime.len > 0) {\n        try response._headers.append(\"Content-Type\", blob._mime, page);\n    }\n\n    const js_val = try page.js.local.?.zigValueToJs(response, .{});\n    resolver.resolve(\"fetch blob done\", js_val);\n    return resolver.promise();\n}\n\nfn httpStartCallback(transfer: *HttpClient.Transfer) !void {\n    const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"request start\", .{ .url = self._url, .source = \"fetch\" });\n    }\n    self._response._transfer = transfer;\n}\n\nfn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {\n    const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));\n\n    if (self._signal) |signal| {\n        if (signal._aborted) {\n            return false;\n        }\n    }\n\n    const arena = self._response._arena;\n    if (transfer.getContentLength()) |cl| {\n        try self._buf.ensureTotalCapacity(arena, cl);\n    }\n\n    const res = self._response;\n    const header = transfer.response_header.?;\n\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"request header\", .{\n            .source = \"fetch\",\n            .url = self._url,\n            .status = header.status,\n        });\n    }\n\n    res._status = header.status;\n    res._status_text = std.http.Status.phrase(@enumFromInt(header.status)) orelse \"\";\n    res._url = try arena.dupeZ(u8, std.mem.span(header.url));\n    res._is_redirected = header.redirect_count > 0;\n\n    // Determine response type based on origin comparison\n    const page_origin = URL.getOrigin(arena, self._page.url) catch null;\n    const response_origin = URL.getOrigin(arena, res._url) catch null;\n\n    if (page_origin) |po| {\n        if (response_origin) |ro| {\n            if (std.mem.eql(u8, po, ro)) {\n                res._type = .basic; // Same-origin\n            } else {\n                res._type = .cors; // Cross-origin (for simplicity, assume CORS passed)\n            }\n        } else {\n            res._type = .basic;\n        }\n    } else {\n        res._type = .basic;\n    }\n\n    var it = transfer.responseHeaderIterator();\n    while (it.next()) |hdr| {\n        try res._headers.append(hdr.name, hdr.value, self._page);\n    }\n\n    return true;\n}\n\nfn httpDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {\n    const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));\n\n    // Check if aborted\n    if (self._signal) |signal| {\n        if (signal._aborted) {\n            return error.Abort;\n        }\n    }\n\n    try self._buf.appendSlice(self._response._arena, data);\n}\n\nfn httpDoneCallback(ctx: *anyopaque) !void {\n    const self: *Fetch = @ptrCast(@alignCast(ctx));\n    var response = self._response;\n    response._transfer = null;\n    response._body = self._buf.items;\n\n    log.info(.http, \"request complete\", .{\n        .source = \"fetch\",\n        .url = self._url,\n        .status = response._status,\n        .len = self._buf.items.len,\n    });\n\n    var ls: js.Local.Scope = undefined;\n    self._page.js.localScope(&ls);\n    defer ls.deinit();\n\n    const js_val = try ls.local.zigValueToJs(self._response, .{});\n    self._owns_response = false;\n    return ls.toLocal(self._resolver).resolve(\"fetch done\", js_val);\n}\n\nfn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {\n    const self: *Fetch = @ptrCast(@alignCast(ctx));\n\n    var response = self._response;\n    response._transfer = null;\n    // the response is only passed on v8 on success, if we're here, it's safe to\n    // clear this. (defer since `self is in the response's arena).\n\n    defer if (self._owns_response) {\n        response.deinit(err == error.Abort, self._page._session);\n        self._owns_response = false;\n    };\n\n    var ls: js.Local.Scope = undefined;\n    self._page.js.localScope(&ls);\n    defer ls.deinit();\n\n    // fetch() must reject with a TypeError on network errors per spec\n    ls.toLocal(self._resolver).rejectError(\"fetch error\", .{ .type_error = @errorName(err) });\n}\n\nfn httpShutdownCallback(ctx: *anyopaque) void {\n    const self: *Fetch = @ptrCast(@alignCast(ctx));\n    if (comptime IS_DEBUG) {\n        // should always be true\n        std.debug.assert(self._owns_response);\n    }\n\n    if (self._owns_response) {\n        var response = self._response;\n        response._transfer = null;\n        response.deinit(true, self._page._session);\n        // Do not access `self` after this point: the Fetch struct was\n        // allocated from response._arena which has been released.\n    }\n}\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: fetch\" {\n    try testing.htmlRunner(\"net/fetch.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/FormData.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst log = @import(\"../../../log.zig\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Node = @import(\"../Node.zig\");\nconst Form = @import(\"../element/html/Form.zig\");\nconst Element = @import(\"../Element.zig\");\nconst KeyValueList = @import(\"../KeyValueList.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst FormData = @This();\n\n_arena: Allocator,\n_list: KeyValueList,\n\npub fn init(form: ?*Form, submitter: ?*Element, page: *Page) !*FormData {\n    return page._factory.create(FormData{\n        ._arena = page.arena,\n        ._list = try collectForm(page.arena, form, submitter, page),\n    });\n}\n\npub fn get(self: *const FormData, name: []const u8) ?[]const u8 {\n    return self._list.get(name);\n}\n\npub fn getAll(self: *const FormData, name: []const u8, page: *Page) ![]const []const u8 {\n    return self._list.getAll(name, page);\n}\n\npub fn has(self: *const FormData, name: []const u8) bool {\n    return self._list.has(name);\n}\n\npub fn set(self: *FormData, name: []const u8, value: []const u8) !void {\n    return self._list.set(self._arena, name, value);\n}\n\npub fn append(self: *FormData, name: []const u8, value: []const u8) !void {\n    return self._list.append(self._arena, name, value);\n}\n\npub fn delete(self: *FormData, name: []const u8) void {\n    self._list.delete(name, null);\n}\n\npub fn keys(self: *FormData, page: *Page) !*KeyValueList.KeyIterator {\n    return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page);\n}\n\npub fn values(self: *FormData, page: *Page) !*KeyValueList.ValueIterator {\n    return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page);\n}\n\npub fn entries(self: *FormData, page: *Page) !*KeyValueList.EntryIterator {\n    return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page);\n}\n\npub fn forEach(self: *FormData, cb_: js.Function, js_this_: ?js.Object) !void {\n    const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;\n\n    for (self._list._entries.items) |entry| {\n        cb.call(void, .{ entry.value.str(), entry.name.str(), self }) catch |err| {\n            // this is a non-JS error\n            log.warn(.js, \"FormData.forEach\", .{ .err = err });\n        };\n    }\n}\n\npub fn write(self: *const FormData, encoding_: ?[]const u8, writer: *std.Io.Writer) !void {\n    const encoding = encoding_ orelse {\n        return self._list.urlEncode(.form, writer);\n    };\n\n    if (std.ascii.eqlIgnoreCase(encoding, \"application/x-www-form-urlencoded\")) {\n        return self._list.urlEncode(.form, writer);\n    }\n\n    log.warn(.not_implemented, \"FormData.encoding\", .{\n        .encoding = encoding,\n    });\n}\n\npub const Iterator = struct {\n    index: u32 = 0,\n    list: *const FormData,\n\n    const Entry = struct { []const u8, []const u8 };\n\n    pub fn next(self: *Iterator, _: *Page) !?Iterator.Entry {\n        const index = self.index;\n        const items = self.list._list.items();\n        if (index >= items.len) {\n            return null;\n        }\n        self.index = index + 1;\n\n        const e = &items[index];\n        return .{ e.name.str(), e.value.str() };\n    }\n};\n\nfn collectForm(arena: Allocator, form_: ?*Form, submitter_: ?*Element, page: *Page) !KeyValueList {\n    var list: KeyValueList = .empty;\n    const form = form_ orelse return list;\n\n    const form_node = form.asNode();\n\n    var elements = try form.getElements(page);\n    var it = try elements.iterator();\n    while (it.next()) |element| {\n        if (element.getAttributeSafe(comptime .wrap(\"disabled\")) != null) {\n            continue;\n        }\n        if (isDisabledByFieldset(element, form_node)) {\n            continue;\n        }\n\n        // Handle image submitters first - they can submit without a name\n        if (element.is(Form.Input)) |input| {\n            if (input._input_type == .image) {\n                const submitter = submitter_ orelse continue;\n                if (submitter != element) {\n                    continue;\n                }\n\n                const name = element.getAttributeSafe(comptime .wrap(\"name\"));\n                const x_key = if (name) |n| try std.fmt.allocPrint(arena, \"{s}.x\", .{n}) else \"x\";\n                const y_key = if (name) |n| try std.fmt.allocPrint(arena, \"{s}.y\", .{n}) else \"y\";\n                try list.append(arena, x_key, \"0\");\n                try list.append(arena, y_key, \"0\");\n                continue;\n            }\n        }\n\n        const name = element.getAttributeSafe(comptime .wrap(\"name\")) orelse continue;\n        const value = blk: {\n            if (element.is(Form.Input)) |input| {\n                const input_type = input._input_type;\n                if (input_type == .checkbox or input_type == .radio) {\n                    if (!input.getChecked()) {\n                        continue;\n                    }\n                }\n                if (input_type == .submit) {\n                    const submitter = submitter_ orelse continue;\n                    if (submitter != element) {\n                        continue;\n                    }\n                }\n                break :blk input.getValue();\n            }\n\n            if (element.is(Form.Select)) |select| {\n                if (select.getMultiple() == false) {\n                    break :blk select.getValue(page);\n                }\n\n                var options = try select.getSelectedOptions(page);\n                while (options.next()) |option| {\n                    try list.append(arena, name, option.as(Form.Select.Option).getValue(page));\n                }\n                continue;\n            }\n\n            if (element.is(Form.TextArea)) |textarea| {\n                break :blk textarea.getValue();\n            }\n\n            if (submitter_) |submitter| {\n                if (submitter == element) {\n                    // The form iterator only yields form controls. If we're here\n                    // all other control types have been handled. So the cast is safe.\n                    break :blk element.as(Form.Button).getValue();\n                }\n            }\n            continue;\n        };\n        try list.append(arena, name, value);\n    }\n    return list;\n}\n\n// Returns true if `element` is disabled by an ancestor <fieldset disabled>,\n// stopping the upward walk when the form node is reached.\n// Per spec, elements inside the first <legend> child of a disabled fieldset\n// are NOT disabled by that fieldset.\nfn isDisabledByFieldset(element: *Element, form_node: *Node) bool {\n    const element_node = element.asNode();\n    var current: ?*Node = element_node._parent;\n    while (current) |node| {\n        // Stop at the form boundary (common case optimisation)\n        if (node == form_node) {\n            return false;\n        }\n\n        current = node._parent;\n        const el = node.is(Element) orelse continue;\n\n        if (el.getTag() == .fieldset and el.getAttributeSafe(comptime .wrap(\"disabled\")) != null) {\n            // Check if `element` is inside the first <legend> child of this fieldset\n            var child = el.firstElementChild();\n            while (child) |c| {\n                if (c.getTag() == .legend) {\n                    // Found the first legend; exempt if element is a descendant\n                    if (c.asNode().contains(element_node)) {\n                        return false;\n                    }\n                    break;\n                }\n                child = c.nextElementSibling();\n            }\n            return true;\n        }\n    }\n    return false;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(FormData);\n\n    pub const Meta = struct {\n        pub const name = \"FormData\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(FormData.init, .{});\n    pub const has = bridge.function(FormData.has, .{});\n    pub const get = bridge.function(FormData.get, .{});\n    pub const set = bridge.function(FormData.set, .{});\n    pub const append = bridge.function(FormData.append, .{});\n    pub const getAll = bridge.function(FormData.getAll, .{});\n    pub const delete = bridge.function(FormData.delete, .{});\n    pub const keys = bridge.function(FormData.keys, .{});\n    pub const values = bridge.function(FormData.values, .{});\n    pub const entries = bridge.function(FormData.entries, .{});\n    pub const symbol_iterator = bridge.iterator(FormData.entries, .{});\n    pub const forEach = bridge.function(FormData.forEach, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: FormData\" {\n    try testing.htmlRunner(\"net/form_data.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/Headers.zig",
    "content": "const std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst log = @import(\"../../../log.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst KeyValueList = @import(\"../KeyValueList.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst Headers = @This();\n\n_list: KeyValueList,\n\npub const InitOpts = union(enum) {\n    obj: *Headers,\n    strings: []const [2][]const u8,\n    js_obj: js.Object,\n};\n\npub fn init(opts_: ?InitOpts, page: *Page) !*Headers {\n    const list = if (opts_) |opts| switch (opts) {\n        .obj => |obj| try KeyValueList.copy(page.arena, obj._list),\n        .js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj, normalizeHeaderName, page),\n        .strings => |kvs| try KeyValueList.fromArray(page.arena, kvs, normalizeHeaderName, page),\n    } else KeyValueList.init();\n\n    return page._factory.create(Headers{\n        ._list = list,\n    });\n}\n\npub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {\n    const normalized_name = normalizeHeaderName(name, page);\n    try self._list.append(page.arena, normalized_name, value);\n}\n\npub fn delete(self: *Headers, name: []const u8, page: *Page) void {\n    const normalized_name = normalizeHeaderName(name, page);\n    self._list.delete(normalized_name, null);\n}\n\npub fn get(self: *const Headers, name: []const u8, page: *Page) !?[]const u8 {\n    const normalized_name = normalizeHeaderName(name, page);\n    const all_values = try self._list.getAll(normalized_name, page);\n\n    if (all_values.len == 0) {\n        return null;\n    }\n    if (all_values.len == 1) {\n        return all_values[0];\n    }\n    return try std.mem.join(page.call_arena, \", \", all_values);\n}\n\npub fn has(self: *const Headers, name: []const u8, page: *Page) bool {\n    const normalized_name = normalizeHeaderName(name, page);\n    return self._list.has(normalized_name);\n}\n\npub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {\n    const normalized_name = normalizeHeaderName(name, page);\n    try self._list.set(page.arena, normalized_name, value);\n}\n\npub fn keys(self: *Headers, page: *Page) !*KeyValueList.KeyIterator {\n    return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page);\n}\n\npub fn values(self: *Headers, page: *Page) !*KeyValueList.ValueIterator {\n    return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page);\n}\n\npub fn entries(self: *Headers, page: *Page) !*KeyValueList.EntryIterator {\n    return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page);\n}\n\npub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {\n    const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;\n\n    for (self._list._entries.items) |entry| {\n        var caught: js.TryCatch.Caught = undefined;\n        cb.tryCall(void, .{ entry.value.str(), entry.name.str(), self }, &caught) catch {\n            log.debug(.js, \"forEach callback\", .{ .caught = caught, .source = \"headers\" });\n        };\n    }\n}\n\n// TODO: do we really need 2 different header structs??\nconst net_http = @import(\"../../../network/http.zig\");\npub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *net_http.Headers) !void {\n    for (self._list._entries.items) |entry| {\n        const merged = try std.mem.concatWithSentinel(allocator, u8, &.{ entry.name.str(), \": \", entry.value.str() }, 0);\n        try http_headers.add(merged);\n    }\n}\n\nfn normalizeHeaderName(name: []const u8, page: *Page) []const u8 {\n    if (name.len > page.buf.len) {\n        return name;\n    }\n    return std.ascii.lowerString(&page.buf, name);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Headers);\n\n    pub const Meta = struct {\n        pub const name = \"Headers\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(Headers.init, .{});\n    pub const append = bridge.function(Headers.append, .{});\n    pub const delete = bridge.function(Headers.delete, .{});\n    pub const get = bridge.function(Headers.get, .{});\n    pub const has = bridge.function(Headers.has, .{});\n    pub const set = bridge.function(Headers.set, .{});\n    pub const keys = bridge.function(Headers.keys, .{});\n    pub const values = bridge.function(Headers.values, .{});\n    pub const entries = bridge.function(Headers.entries, .{});\n    pub const forEach = bridge.function(Headers.forEach, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: Headers\" {\n    try testing.htmlRunner(\"net/headers.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/Request.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst js = @import(\"../../js/js.zig\");\nconst net_http = @import(\"../../../network/http.zig\");\n\nconst URL = @import(\"../URL.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Headers = @import(\"Headers.zig\");\nconst Blob = @import(\"../Blob.zig\");\nconst AbortSignal = @import(\"../AbortSignal.zig\");\nconst Allocator = std.mem.Allocator;\n\nconst Request = @This();\n\n_url: [:0]const u8,\n_method: net_http.Method,\n_headers: ?*Headers,\n_body: ?[]const u8,\n_arena: Allocator,\n_cache: Cache,\n_credentials: Credentials,\n_signal: ?*AbortSignal,\n\npub const Input = union(enum) {\n    request: *Request,\n    url: [:0]const u8,\n};\n\npub const InitOpts = struct {\n    method: ?[]const u8 = null,\n    headers: ?Headers.InitOpts = null,\n    body: ?[]const u8 = null,\n    cache: Cache = .default,\n    credentials: Credentials = .@\"same-origin\",\n    signal: ?*AbortSignal = null,\n};\n\nconst Credentials = enum {\n    omit,\n    include,\n    @\"same-origin\",\n    pub const js_enum_from_string = true;\n};\n\nconst Cache = enum {\n    default,\n    @\"no-store\",\n    reload,\n    @\"no-cache\",\n    @\"force-cache\",\n    @\"only-if-cached\",\n    pub const js_enum_from_string = true;\n};\n\npub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {\n    const arena = page.arena;\n    const url = switch (input) {\n        .url => |u| try URL.resolve(arena, page.base(), u, .{ .always_dupe = true }),\n        .request => |r| try arena.dupeZ(u8, r._url),\n    };\n\n    const opts = opts_ orelse InitOpts{};\n    const method = if (opts.method) |m|\n        try parseMethod(m, page)\n    else switch (input) {\n        .url => .GET,\n        .request => |r| r._method,\n    };\n\n    const headers = if (opts.headers) |headers_init| switch (headers_init) {\n        .obj => |h| h,\n        else => try Headers.init(headers_init, page),\n    } else switch (input) {\n        .url => null,\n        .request => |r| r._headers,\n    };\n\n    const body = if (opts.body) |b|\n        try arena.dupe(u8, b)\n    else switch (input) {\n        .url => null,\n        .request => |r| r._body,\n    };\n\n    const signal = if (opts.signal) |s|\n        s\n    else switch (input) {\n        .url => null,\n        .request => |r| r._signal,\n    };\n\n    return page._factory.create(Request{\n        ._url = url,\n        ._arena = arena,\n        ._method = method,\n        ._headers = headers,\n        ._cache = opts.cache,\n        ._credentials = opts.credentials,\n        ._body = body,\n        ._signal = signal,\n    });\n}\n\nfn parseMethod(method: []const u8, page: *Page) !net_http.Method {\n    if (method.len > \"propfind\".len) {\n        return error.InvalidMethod;\n    }\n\n    const lower = std.ascii.lowerString(&page.buf, method);\n\n    const method_lookup = std.StaticStringMap(net_http.Method).initComptime(.{\n        .{ \"get\", .GET },\n        .{ \"post\", .POST },\n        .{ \"delete\", .DELETE },\n        .{ \"put\", .PUT },\n        .{ \"patch\", .PATCH },\n        .{ \"head\", .HEAD },\n        .{ \"options\", .OPTIONS },\n        .{ \"propfind\", .PROPFIND },\n    });\n    return method_lookup.get(lower) orelse return error.InvalidMethod;\n}\n\npub fn getUrl(self: *const Request) []const u8 {\n    return self._url;\n}\n\npub fn getMethod(self: *const Request) []const u8 {\n    return @tagName(self._method);\n}\n\npub fn getCache(self: *const Request) []const u8 {\n    return @tagName(self._cache);\n}\n\npub fn getCredentials(self: *const Request) []const u8 {\n    return @tagName(self._credentials);\n}\n\npub fn getSignal(self: *const Request) ?*AbortSignal {\n    return self._signal;\n}\n\npub fn getHeaders(self: *Request, page: *Page) !*Headers {\n    if (self._headers) |headers| {\n        return headers;\n    }\n\n    const headers = try Headers.init(null, page);\n    self._headers = headers;\n    return headers;\n}\n\npub fn blob(self: *Request, page: *Page) !js.Promise {\n    const body = self._body orelse \"\";\n    const headers = try self.getHeaders(page);\n    const content_type = try headers.get(\"content-type\", page) orelse \"\";\n\n    const b = try Blob.initWithMimeValidation(\n        &.{body},\n        .{ .type = content_type },\n        true,\n        page,\n    );\n\n    return page.js.local.?.resolvePromise(b);\n}\n\npub fn text(self: *const Request, page: *Page) !js.Promise {\n    const body = self._body orelse \"\";\n    return page.js.local.?.resolvePromise(body);\n}\n\npub fn json(self: *const Request, page: *Page) !js.Promise {\n    const body = self._body orelse \"\";\n    const local = page.js.local.?;\n    const value = local.parseJSON(body) catch |err| {\n        return local.rejectPromise(.{@errorName(err)});\n    };\n    return local.resolvePromise(try value.persist());\n}\n\npub fn arrayBuffer(self: *const Request, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse \"\" });\n}\n\npub fn bytes(self: *const Request, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse \"\" });\n}\n\npub fn clone(self: *const Request, page: *Page) !*Request {\n    return page._factory.create(Request{\n        ._url = self._url,\n        ._arena = self._arena,\n        ._method = self._method,\n        ._headers = self._headers,\n        ._cache = self._cache,\n        ._credentials = self._credentials,\n        ._body = self._body,\n        ._signal = self._signal,\n    });\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Request);\n\n    pub const Meta = struct {\n        pub const name = \"Request\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(Request.init, .{});\n    pub const url = bridge.accessor(Request.getUrl, null, .{});\n    pub const method = bridge.accessor(Request.getMethod, null, .{});\n    pub const headers = bridge.accessor(Request.getHeaders, null, .{});\n    pub const cache = bridge.accessor(Request.getCache, null, .{});\n    pub const credentials = bridge.accessor(Request.getCredentials, null, .{});\n    pub const signal = bridge.accessor(Request.getSignal, null, .{});\n    pub const blob = bridge.function(Request.blob, .{});\n    pub const text = bridge.function(Request.text, .{});\n    pub const json = bridge.function(Request.json, .{});\n    pub const arrayBuffer = bridge.function(Request.arrayBuffer, .{});\n    pub const bytes = bridge.function(Request.bytes, .{});\n    pub const clone = bridge.function(Request.clone, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: Request\" {\n    try testing.htmlRunner(\"net/request.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/Response.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst HttpClient = @import(\"../../HttpClient.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\nconst Headers = @import(\"Headers.zig\");\nconst ReadableStream = @import(\"../streams/ReadableStream.zig\");\nconst Blob = @import(\"../Blob.zig\");\n\nconst Allocator = std.mem.Allocator;\n\nconst Response = @This();\n\npub const Type = enum {\n    basic,\n    cors,\n    @\"error\",\n    @\"opaque\",\n    opaqueredirect,\n};\n\n_status: u16,\n_arena: Allocator,\n_headers: *Headers,\n_body: ?[]const u8,\n_type: Type,\n_status_text: []const u8,\n_url: [:0]const u8,\n_is_redirected: bool,\n_transfer: ?*HttpClient.Transfer = null,\n\nconst InitOpts = struct {\n    status: u16 = 200,\n    headers: ?Headers.InitOpts = null,\n    statusText: ?[]const u8 = null,\n};\n\npub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {\n    const arena = try page.getArena(.{ .debug = \"Response\" });\n    errdefer page.releaseArena(arena);\n\n    const opts = opts_ orelse InitOpts{};\n\n    // Store empty string as empty string, not null\n    const body = if (body_) |b| try arena.dupe(u8, b) else null;\n    const status_text = if (opts.statusText) |st| try arena.dupe(u8, st) else \"\";\n\n    const self = try arena.create(Response);\n    self.* = .{\n        ._arena = arena,\n        ._status = opts.status,\n        ._status_text = status_text,\n        ._url = \"\",\n        ._body = body,\n        ._type = .basic,\n        ._is_redirected = false,\n        ._headers = try Headers.init(opts.headers, page),\n    };\n    return self;\n}\n\npub fn deinit(self: *Response, shutdown: bool, session: *Session) void {\n    if (self._transfer) |transfer| {\n        if (shutdown) {\n            transfer.terminate();\n        } else {\n            transfer.abort(error.Abort);\n        }\n        self._transfer = null;\n    }\n    session.releaseArena(self._arena);\n}\n\npub fn getStatus(self: *const Response) u16 {\n    return self._status;\n}\n\npub fn getStatusText(self: *const Response) []const u8 {\n    return self._status_text;\n}\n\npub fn getURL(self: *const Response) []const u8 {\n    return self._url;\n}\n\npub fn isRedirected(self: *const Response) bool {\n    return self._is_redirected;\n}\n\npub fn getHeaders(self: *const Response) *Headers {\n    return self._headers;\n}\n\npub fn getType(self: *const Response) []const u8 {\n    return @tagName(self._type);\n}\n\npub fn getBody(self: *const Response, page: *Page) !?*ReadableStream {\n    const body = self._body orelse return null;\n\n    // Empty string should create a closed stream with no data\n    if (body.len == 0) {\n        const stream = try ReadableStream.init(null, null, page);\n        try stream._controller.close();\n        return stream;\n    }\n\n    return ReadableStream.initWithData(body, page);\n}\n\npub fn isOK(self: *const Response) bool {\n    return self._status >= 200 and self._status <= 299;\n}\n\npub fn getText(self: *const Response, page: *Page) !js.Promise {\n    const body = self._body orelse \"\";\n    return page.js.local.?.resolvePromise(body);\n}\n\npub fn getJson(self: *Response, page: *Page) !js.Promise {\n    const body = self._body orelse \"\";\n    const local = page.js.local.?;\n    const value = local.parseJSON(body) catch |err| {\n        return local.rejectPromise(.{@errorName(err)});\n    };\n    return local.resolvePromise(try value.persist());\n}\n\npub fn arrayBuffer(self: *const Response, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse \"\" });\n}\n\npub fn blob(self: *const Response, page: *Page) !js.Promise {\n    const body = self._body orelse \"\";\n    const content_type = try self._headers.get(\"content-type\", page) orelse \"\";\n\n    const b = try Blob.initWithMimeValidation(\n        &.{body},\n        .{ .type = content_type },\n        true,\n        page,\n    );\n\n    return page.js.local.?.resolvePromise(b);\n}\n\npub fn bytes(self: *const Response, page: *Page) !js.Promise {\n    return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse \"\" });\n}\n\npub fn clone(self: *const Response, page: *Page) !*Response {\n    const arena = try page.getArena(.{ .debug = \"Response.clone\" });\n    errdefer page.releaseArena(arena);\n\n    const body = if (self._body) |b| try arena.dupe(u8, b) else null;\n    const status_text = try arena.dupe(u8, self._status_text);\n    const url = try arena.dupeZ(u8, self._url);\n\n    const cloned = try arena.create(Response);\n    cloned.* = .{\n        ._arena = arena,\n        ._status = self._status,\n        ._status_text = status_text,\n        ._url = url,\n        ._body = body,\n        ._type = self._type,\n        ._is_redirected = self._is_redirected,\n        ._headers = try Headers.init(.{ .obj = self._headers }, page),\n        ._transfer = null,\n    };\n    return cloned;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(Response);\n\n    pub const Meta = struct {\n        pub const name = \"Response\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(Response.deinit);\n    };\n\n    pub const constructor = bridge.constructor(Response.init, .{});\n    pub const ok = bridge.accessor(Response.isOK, null, .{});\n    pub const status = bridge.accessor(Response.getStatus, null, .{});\n    pub const statusText = bridge.accessor(Response.getStatusText, null, .{});\n    pub const @\"type\" = bridge.accessor(Response.getType, null, .{});\n    pub const text = bridge.function(Response.getText, .{});\n    pub const json = bridge.function(Response.getJson, .{});\n    pub const headers = bridge.accessor(Response.getHeaders, null, .{});\n    pub const body = bridge.accessor(Response.getBody, null, .{});\n    pub const url = bridge.accessor(Response.getURL, null, .{});\n    pub const redirected = bridge.accessor(Response.isRedirected, null, .{});\n    pub const arrayBuffer = bridge.function(Response.arrayBuffer, .{});\n    pub const blob = bridge.function(Response.blob, .{});\n    pub const bytes = bridge.function(Response.bytes, .{});\n    pub const clone = bridge.function(Response.clone, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: Response\" {\n    try testing.htmlRunner(\"net/response.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/URLSearchParams.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst log = @import(\"../../../log.zig\");\nconst String = @import(\"../../../string.zig\").String;\nconst Allocator = std.mem.Allocator;\n\nconst Page = @import(\"../../Page.zig\");\nconst FormData = @import(\"FormData.zig\");\nconst KeyValueList = @import(\"../KeyValueList.zig\");\n\nconst URLSearchParams = @This();\n\n_arena: Allocator,\n_params: KeyValueList,\n\nconst InitOpts = union(enum) {\n    form_data: *FormData,\n    value: js.Value,\n    query_string: []const u8,\n};\n\npub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {\n    const arena = page.arena;\n    const params: KeyValueList = blk: {\n        const opts = opts_ orelse break :blk .empty;\n        switch (opts) {\n            .query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf),\n            .form_data => |fd| break :blk try KeyValueList.copy(arena, fd._list),\n            .value => |js_val| {\n                if (js_val.isObject()) {\n                    break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page);\n                }\n                if (js_val.isString()) |js_str| {\n                    break :blk try paramsFromString(arena, try js_str.toSliceWithAlloc(arena), &page.buf);\n                }\n                return error.InvalidArgument;\n            },\n        }\n    };\n\n    return page._factory.create(URLSearchParams{\n        ._arena = arena,\n        ._params = params,\n    });\n}\n\npub fn updateFromString(self: *URLSearchParams, query_string: []const u8, page: *Page) !void {\n    self._params = try paramsFromString(self._arena, query_string, &page.buf);\n}\n\npub fn getSize(self: *const URLSearchParams) usize {\n    return self._params.len();\n}\n\npub fn get(self: *const URLSearchParams, name: []const u8) ?[]const u8 {\n    return self._params.get(name);\n}\n\npub fn getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 {\n    return self._params.getAll(name, page);\n}\n\npub fn has(self: *const URLSearchParams, name: []const u8) bool {\n    return self._params.has(name);\n}\n\npub fn set(self: *URLSearchParams, name: []const u8, value: []const u8) !void {\n    return self._params.set(self._arena, name, value);\n}\n\npub fn append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {\n    return self._params.append(self._arena, name, value);\n}\n\npub fn delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) void {\n    self._params.delete(name, value);\n}\n\npub fn keys(self: *URLSearchParams, page: *Page) !*KeyValueList.KeyIterator {\n    return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._params }, page);\n}\n\npub fn values(self: *URLSearchParams, page: *Page) !*KeyValueList.ValueIterator {\n    return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._params }, page);\n}\n\npub fn entries(self: *URLSearchParams, page: *Page) !*KeyValueList.EntryIterator {\n    return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._params }, page);\n}\n\npub fn toString(self: *const URLSearchParams, writer: *std.Io.Writer) !void {\n    return self._params.urlEncode(.query, writer);\n}\n\npub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {\n    return self.toString(writer);\n}\n\npub fn forEach(self: *URLSearchParams, cb_: js.Function, js_this_: ?js.Object) !void {\n    const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;\n\n    for (self._params._entries.items) |entry| {\n        cb.call(void, .{ entry.value.str(), entry.name.str(), self }) catch |err| {\n            // this is a non-JS error\n            log.warn(.js, \"URLSearchParams.forEach\", .{ .err = err });\n        };\n    }\n}\n\npub fn sort(self: *URLSearchParams) void {\n    std.mem.sort(KeyValueList.Entry, self._params._entries.items, {}, struct {\n        fn cmp(_: void, a: KeyValueList.Entry, b: KeyValueList.Entry) bool {\n            return std.mem.order(u8, a.name.str(), b.name.str()) == .lt;\n        }\n    }.cmp);\n}\n\nfn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyValueList {\n    if (input_.len == 0) {\n        return .empty;\n    }\n\n    var input = input_;\n    if (input[0] == '?') {\n        input = input[1..];\n    }\n\n    // After stripping '?', check if string is empty\n    if (input.len == 0) {\n        return .empty;\n    }\n\n    var params = KeyValueList.init();\n\n    var it = std.mem.splitScalar(u8, input, '&');\n    while (it.next()) |entry| {\n        // Skip empty entries (from trailing &, or &&)\n        if (entry.len == 0) continue;\n\n        var name: String = undefined;\n        var value: String = undefined;\n\n        if (std.mem.indexOfScalarPos(u8, entry, 0, '=')) |idx| {\n            name = try unescape(allocator, entry[0..idx], buf);\n            value = try unescape(allocator, entry[idx + 1 ..], buf);\n        } else {\n            name = try unescape(allocator, entry, buf);\n            value = String.init(undefined, \"\", .{}) catch unreachable;\n        }\n\n        // optimized, unescape returns a String directly (Because unescape may\n        // have to dupe itself, so it knows how best to create the String)\n        try params._entries.append(allocator, .{\n            .name = name,\n            .value = value,\n        });\n    }\n\n    return params;\n}\n\nfn unescape(arena: Allocator, value: []const u8, buf: []u8) !String {\n    if (value.len == 0) {\n        return String.init(undefined, \"\", .{});\n    }\n\n    var has_plus = false;\n    var unescaped_len = value.len;\n\n    var in_i: usize = 0;\n    while (in_i < value.len) {\n        const b = value[in_i];\n        if (b == '%') {\n            if (in_i + 2 >= value.len or !std.ascii.isHex(value[in_i + 1]) or !std.ascii.isHex(value[in_i + 2])) {\n                return error.InvalidEscapeSequence;\n            }\n            in_i += 3;\n            unescaped_len -= 2;\n        } else if (b == '+') {\n            has_plus = true;\n            in_i += 1;\n        } else {\n            in_i += 1;\n        }\n    }\n\n    // no encoding, and no plus. nothing to unescape\n    if (unescaped_len == value.len and !has_plus) {\n        return String.init(arena, value, .{});\n    }\n\n    var out = buf;\n    var duped = false;\n    if (buf.len < unescaped_len) {\n        out = try arena.alloc(u8, unescaped_len);\n        duped = true;\n    }\n\n    in_i = 0;\n    for (0..unescaped_len) |i| {\n        const b = value[in_i];\n        if (b == '%') {\n            out[i] = decodeHex(value[in_i + 1]) << 4 | decodeHex(value[in_i + 2]);\n            in_i += 3;\n        } else if (b == '+') {\n            out[i] = ' ';\n            in_i += 1;\n        } else {\n            out[i] = b;\n            in_i += 1;\n        }\n    }\n\n    return String.init(arena, out[0..unescaped_len], .{ .dupe = !duped });\n}\n\nconst HEX_DECODE_ARRAY = blk: {\n    var all: ['f' - '0' + 1]u8 = undefined;\n    for ('0'..('9' + 1)) |b| all[b - '0'] = b - '0';\n    for ('A'..('F' + 1)) |b| all[b - '0'] = b - 'A' + 10;\n    for ('a'..('f' + 1)) |b| all[b - '0'] = b - 'a' + 10;\n    break :blk all;\n};\n\ninline fn decodeHex(char: u8) u8 {\n    return @as([*]const u8, @ptrFromInt((@intFromPtr(&HEX_DECODE_ARRAY) - @as(usize, '0'))))[char];\n}\n\nfn escape(input: []const u8, writer: *std.Io.Writer) !void {\n    for (input) |c| {\n        if (isUnreserved(c)) {\n            try writer.writeByte(c);\n        } else if (c == ' ') {\n            try writer.writeByte('+');\n        } else if (c == '*') {\n            try writer.writeByte('*');\n        } else if (c >= 0x80) {\n            // Double-encode: treat byte as Latin-1 code point, encode to UTF-8, then percent-encode\n            // For bytes 0x80-0xFF (U+0080 to U+00FF), UTF-8 encoding is 2 bytes:\n            // [0xC0 | (c >> 6), 0x80 | (c & 0x3F)]\n            const byte1 = 0xC0 | (c >> 6);\n            const byte2 = 0x80 | (c & 0x3F);\n            try writer.print(\"%{X:0>2}%{X:0>2}\", .{ byte1, byte2 });\n        } else {\n            try writer.print(\"%{X:0>2}\", .{c});\n        }\n    }\n}\n\nfn isUnreserved(c: u8) bool {\n    return switch (c) {\n        'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true,\n        else => false,\n    };\n}\n\npub const Iterator = struct {\n    index: u32 = 0,\n    list: *const URLSearchParams,\n\n    const Entry = struct { []const u8, []const u8 };\n\n    pub fn next(self: *Iterator, _: *Page) !?Iterator.Entry {\n        const index = self.index;\n        const items = self.list._params.items;\n        if (index >= items.len) {\n            return null;\n        }\n        self.index = index + 1;\n\n        const e = &items[index];\n        return .{ e.name.str(), e.value.str() };\n    }\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(URLSearchParams);\n\n    pub const Meta = struct {\n        pub const name = \"URLSearchParams\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(URLSearchParams.init, .{});\n    pub const has = bridge.function(URLSearchParams.has, .{});\n    pub const get = bridge.function(URLSearchParams.get, .{});\n    pub const set = bridge.function(URLSearchParams.set, .{});\n    pub const append = bridge.function(URLSearchParams.append, .{});\n    pub const getAll = bridge.function(URLSearchParams.getAll, .{});\n    pub const delete = bridge.function(URLSearchParams.delete, .{});\n    pub const size = bridge.accessor(URLSearchParams.getSize, null, .{});\n    pub const keys = bridge.function(URLSearchParams.keys, .{});\n    pub const values = bridge.function(URLSearchParams.values, .{});\n    pub const entries = bridge.function(URLSearchParams.entries, .{});\n    pub const symbol_iterator = bridge.iterator(URLSearchParams.entries, .{});\n    pub const forEach = bridge.function(URLSearchParams.forEach, .{});\n    pub const sort = bridge.function(URLSearchParams.sort, .{});\n\n    pub const toString = bridge.function(_toString, .{});\n    fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 {\n        var buf = std.Io.Writer.Allocating.init(page.call_arena);\n        try self.toString(&buf.writer);\n        return buf.written();\n    }\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: URLSearchParams\" {\n    try testing.htmlRunner(\"net/url_search_params.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/XMLHttpRequest.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst log = @import(\"../../../log.zig\");\nconst HttpClient = @import(\"../../HttpClient.zig\");\nconst net_http = @import(\"../../../network/http.zig\");\n\nconst URL = @import(\"../../URL.zig\");\nconst Mime = @import(\"../../Mime.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Blob = @import(\"../Blob.zig\");\nconst Event = @import(\"../Event.zig\");\nconst Headers = @import(\"Headers.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\nconst XMLHttpRequestEventTarget = @import(\"XMLHttpRequestEventTarget.zig\");\n\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst XMLHttpRequest = @This();\n_page: *Page,\n_proto: *XMLHttpRequestEventTarget,\n_arena: Allocator,\n_transfer: ?*HttpClient.Transfer = null,\n\n_url: [:0]const u8 = \"\",\n_method: net_http.Method = .GET,\n_request_headers: *Headers,\n_request_body: ?[]const u8 = null,\n\n_response: ?Response = null,\n_response_data: std.ArrayList(u8) = .empty,\n_response_status: u16 = 0,\n_response_len: ?usize = 0,\n_response_url: [:0]const u8 = \"\",\n_response_mime: ?Mime = null,\n_response_headers: std.ArrayList([]const u8) = .empty,\n_response_type: ResponseType = .text,\n\n_ready_state: ReadyState = .unsent,\n_on_ready_state_change: ?js.Function.Temp = null,\n_with_credentials: bool = false,\n\nconst ReadyState = enum(u8) {\n    unsent = 0,\n    opened = 1,\n    headers_received = 2,\n    loading = 3,\n    done = 4,\n};\n\nconst Response = union(ResponseType) {\n    text: []const u8,\n    json: js.Value.Global,\n    document: *Node.Document,\n    arraybuffer: js.ArrayBuffer,\n};\n\nconst ResponseType = enum {\n    text,\n    json,\n    document,\n    arraybuffer,\n    // TODO: other types to support\n};\n\npub fn init(page: *Page) !*XMLHttpRequest {\n    const arena = try page.getArena(.{ .debug = \"XMLHttpRequest\" });\n    errdefer page.releaseArena(arena);\n    return page._factory.xhrEventTarget(arena, XMLHttpRequest{\n        ._page = page,\n        ._arena = arena,\n        ._proto = undefined,\n        ._request_headers = try Headers.init(null, page),\n    });\n}\n\npub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void {\n    if (self._transfer) |transfer| {\n        if (shutdown) {\n            transfer.terminate();\n        } else {\n            transfer.abort(error.Abort);\n        }\n        self._transfer = null;\n    }\n\n    if (self._on_ready_state_change) |func| {\n        func.release();\n    }\n\n    {\n        const proto = self._proto;\n        if (proto._on_abort) |func| {\n            func.release();\n        }\n        if (proto._on_error) |func| {\n            func.release();\n        }\n        if (proto._on_load) |func| {\n            func.release();\n        }\n        if (proto._on_load_end) |func| {\n            func.release();\n        }\n        if (proto._on_load_start) |func| {\n            func.release();\n        }\n        if (proto._on_progress) |func| {\n            func.release();\n        }\n        if (proto._on_timeout) |func| {\n            func.release();\n        }\n    }\n\n    session.releaseArena(self._arena);\n}\n\nfn asEventTarget(self: *XMLHttpRequest) *EventTarget {\n    return self._proto._proto;\n}\n\npub fn getOnReadyStateChange(self: *const XMLHttpRequest) ?js.Function.Temp {\n    return self._on_ready_state_change;\n}\n\npub fn setOnReadyStateChange(self: *XMLHttpRequest, cb_: ?js.Function) !void {\n    if (cb_) |cb| {\n        self._on_ready_state_change = try cb.tempWithThis(self);\n    } else {\n        self._on_ready_state_change = null;\n    }\n}\n\npub fn getWithCredentials(self: *const XMLHttpRequest) bool {\n    return self._with_credentials;\n}\n\npub fn setWithCredentials(self: *XMLHttpRequest, value: bool) !void {\n    if (self._ready_state != .unsent and self._ready_state != .opened) {\n        return error.InvalidStateError;\n    }\n    self._with_credentials = value;\n}\n\n// TODO: this takes an optional 3 more parameters\n// TODO: url should be a union, as it can be multiple things\npub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void {\n    // Abort any in-progress request\n    if (self._transfer) |transfer| {\n        transfer.abort(error.Abort);\n        self._transfer = null;\n    }\n\n    // Reset internal state\n    self._response = null;\n    self._response_data.clearRetainingCapacity();\n    self._response_status = 0;\n    self._response_len = 0;\n    self._response_url = \"\";\n    self._response_mime = null;\n    self._response_headers.clearRetainingCapacity();\n    self._request_body = null;\n\n    const page = self._page;\n    self._method = try parseMethod(method_);\n    self._url = try URL.resolve(self._arena, page.base(), url, .{ .always_dupe = true, .encode = true });\n    try self.stateChanged(.opened, page);\n}\n\npub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8, page: *Page) !void {\n    if (self._ready_state != .opened) {\n        return error.InvalidStateError;\n    }\n    return self._request_headers.append(name, value, page);\n}\n\npub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"XMLHttpRequest.send\", .{ .url = self._url });\n    }\n    if (self._ready_state != .opened) {\n        return error.InvalidStateError;\n    }\n\n    if (body_) |b| {\n        if (self._method != .GET and self._method != .HEAD) {\n            self._request_body = try self._arena.dupe(u8, b);\n        }\n    }\n\n    const page = self._page;\n\n    if (std.mem.startsWith(u8, self._url, \"blob:\")) {\n        return self.handleBlobUrl(page);\n    }\n\n    const http_client = page._session.browser.http_client;\n    var headers = try http_client.newHeaders();\n\n    // Only add cookies for same-origin or when withCredentials is true\n    const cookie_support = self._with_credentials or try page.isSameOrigin(self._url);\n\n    try self._request_headers.populateHttpHeader(page.call_arena, &headers);\n    if (cookie_support) {\n        try page.headersForRequest(self._arena, self._url, &headers);\n    }\n\n    try http_client.request(.{\n        .ctx = self,\n        .url = self._url,\n        .method = self._method,\n        .headers = headers,\n        .frame_id = page._frame_id,\n        .body = self._request_body,\n        .cookie_jar = if (cookie_support) &page._session.cookie_jar else null,\n        .resource_type = .xhr,\n        .notification = page._session.notification,\n        .start_callback = httpStartCallback,\n        .header_callback = httpHeaderDoneCallback,\n        .data_callback = httpDataCallback,\n        .done_callback = httpDoneCallback,\n        .error_callback = httpErrorCallback,\n        .shutdown_callback = httpShutdownCallback,\n    });\n\n    page.js.strongRef(self);\n}\n\nfn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {\n    const blob = page.lookupBlobUrl(self._url) orelse {\n        self.handleError(error.BlobNotFound);\n        return;\n    };\n\n    self._response_status = 200;\n    self._response_url = self._url;\n\n    try self._response_data.appendSlice(self._arena, blob._slice);\n    self._response_len = blob._slice.len;\n\n    try self.stateChanged(.headers_received, page);\n    try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, page);\n    try self.stateChanged(.loading, page);\n    try self._proto.dispatch(.progress, .{\n        .total = self._response_len orelse 0,\n        .loaded = self._response_data.items.len,\n    }, page);\n    try self.stateChanged(.done, page);\n\n    const loaded = self._response_data.items.len;\n    try self._proto.dispatch(.load, .{\n        .total = loaded,\n        .loaded = loaded,\n    }, page);\n    try self._proto.dispatch(.load_end, .{\n        .total = loaded,\n        .loaded = loaded,\n    }, page);\n}\n\npub fn getReadyState(self: *const XMLHttpRequest) u32 {\n    return @intFromEnum(self._ready_state);\n}\n\npub fn getResponseHeader(self: *const XMLHttpRequest, name: []const u8) ?[]const u8 {\n    for (self._response_headers.items) |entry| {\n        if (entry.len <= name.len) {\n            continue;\n        }\n        if (std.ascii.eqlIgnoreCase(name, entry[0..name.len]) == false) {\n            continue;\n        }\n        if (entry[name.len] != ':') {\n            continue;\n        }\n        return std.mem.trimLeft(u8, entry[name.len + 1 ..], \" \");\n    }\n    return null;\n}\n\npub fn getAllResponseHeaders(self: *const XMLHttpRequest, page: *Page) ![]const u8 {\n    if (self._ready_state != .done) {\n        // MDN says this should return null, but it seems to return an empty string\n        // in every browser. Specs are too hard for a dumbo like me to understand.\n        return \"\";\n    }\n\n    var buf = std.Io.Writer.Allocating.init(page.call_arena);\n    for (self._response_headers.items) |entry| {\n        try buf.writer.writeAll(entry);\n        try buf.writer.writeAll(\"\\r\\n\");\n    }\n    return buf.written();\n}\n\npub fn getResponseType(self: *const XMLHttpRequest) []const u8 {\n    if (self._ready_state != .done) {\n        return \"\";\n    }\n    return @tagName(self._response_type);\n}\n\npub fn setResponseType(self: *XMLHttpRequest, value: []const u8) void {\n    if (std.meta.stringToEnum(ResponseType, value)) |rt| {\n        self._response_type = rt;\n    }\n}\n\npub fn getResponseText(self: *const XMLHttpRequest) []const u8 {\n    return self._response_data.items;\n}\n\npub fn getStatus(self: *const XMLHttpRequest) u16 {\n    return self._response_status;\n}\n\npub fn getStatusText(self: *const XMLHttpRequest) []const u8 {\n    return std.http.Status.phrase(@enumFromInt(self._response_status)) orelse \"\";\n}\n\npub fn getResponseURL(self: *XMLHttpRequest) []const u8 {\n    return self._response_url;\n}\n\npub fn getResponse(self: *XMLHttpRequest, page: *Page) !?Response {\n    if (self._ready_state != .done) {\n        return null;\n    }\n\n    if (self._response) |res| {\n        // was already loaded\n        return res;\n    }\n\n    const data = self._response_data.items;\n    const res: Response = switch (self._response_type) {\n        .text => .{ .text = data },\n        .json => blk: {\n            const value = try page.js.local.?.parseJSON(data);\n            break :blk .{ .json = try value.persist() };\n        },\n        .document => blk: {\n            const document = try page._factory.node(Node.Document{ ._proto = undefined, ._type = .generic });\n            try page.parseHtmlAsChildren(document.asNode(), data);\n            break :blk .{ .document = document };\n        },\n        .arraybuffer => .{ .arraybuffer = .{ .values = data } },\n    };\n\n    self._response = res;\n    return res;\n}\n\npub fn getResponseXML(self: *XMLHttpRequest, page: *Page) !?*Node.Document {\n    const res = (try self.getResponse(page)) orelse return null;\n    return switch (res) {\n        .document => |doc| doc,\n        else => null,\n    };\n}\n\nfn httpStartCallback(transfer: *HttpClient.Transfer) !void {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"request start\", .{ .method = self._method, .url = self._url, .source = \"xhr\" });\n    }\n    self._transfer = transfer;\n}\n\nfn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));\n    const joined = try std.fmt.allocPrint(self._arena, \"{s}: {s}\", .{ header.name, header.value });\n    try self._response_headers.append(self._arena, joined);\n}\n\nfn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));\n\n    const header = &transfer.response_header.?;\n\n    if (comptime IS_DEBUG) {\n        log.debug(.http, \"request header\", .{\n            .source = \"xhr\",\n            .url = self._url,\n            .status = header.status,\n        });\n    }\n\n    if (header.contentType()) |ct| {\n        self._response_mime = Mime.parse(ct) catch |e| {\n            log.info(.http, \"invalid content type\", .{\n                .content_Type = ct,\n                .err = e,\n                .url = self._url,\n            });\n            return false;\n        };\n    }\n\n    var it = transfer.responseHeaderIterator();\n    while (it.next()) |hdr| {\n        const joined = try std.fmt.allocPrint(self._arena, \"{s}: {s}\", .{ hdr.name, hdr.value });\n        try self._response_headers.append(self._arena, joined);\n    }\n\n    self._response_status = header.status;\n    if (transfer.getContentLength()) |cl| {\n        self._response_len = cl;\n        try self._response_data.ensureTotalCapacity(self._arena, cl);\n    }\n    self._response_url = try self._arena.dupeZ(u8, std.mem.span(header.url));\n\n    const page = self._page;\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    try self.stateChanged(.headers_received, page);\n    try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, page);\n    try self.stateChanged(.loading, page);\n\n    return true;\n}\n\nfn httpDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));\n    try self._response_data.appendSlice(self._arena, data);\n\n    const page = self._page;\n\n    try self._proto.dispatch(.progress, .{\n        .total = self._response_len orelse 0,\n        .loaded = self._response_data.items.len,\n    }, page);\n}\n\nfn httpDoneCallback(ctx: *anyopaque) !void {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx));\n\n    log.info(.http, \"request complete\", .{\n        .source = \"xhr\",\n        .url = self._url,\n        .status = self._response_status,\n        .len = self._response_data.items.len,\n    });\n\n    // Not that the request is done, the http/client will free the transfer\n    // object. It isn't safe to keep it around.\n    self._transfer = null;\n\n    const page = self._page;\n\n    try self.stateChanged(.done, page);\n\n    const loaded = self._response_data.items.len;\n    try self._proto.dispatch(.load, .{\n        .total = loaded,\n        .loaded = loaded,\n    }, page);\n    try self._proto.dispatch(.load_end, .{\n        .total = loaded,\n        .loaded = loaded,\n    }, page);\n\n    page.js.weakRef(self);\n}\n\nfn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx));\n    // http client will close it after an error, it isn't safe to keep around\n    self._transfer = null;\n    self.handleError(err);\n    self._page.js.weakRef(self);\n}\n\nfn httpShutdownCallback(ctx: *anyopaque) void {\n    const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx));\n    self._transfer = null;\n}\n\npub fn abort(self: *XMLHttpRequest) void {\n    self.handleError(error.Abort);\n    if (self._transfer) |transfer| {\n        transfer.abort(error.Abort);\n        self._transfer = null;\n    }\n    self._page.js.weakRef(self);\n}\n\nfn handleError(self: *XMLHttpRequest, err: anyerror) void {\n    self._handleError(err) catch |inner| {\n        log.err(.http, \"handle error error\", .{\n            .original = err,\n            .err = inner,\n        });\n    };\n}\nfn _handleError(self: *XMLHttpRequest, err: anyerror) !void {\n    const is_abort = err == error.Abort;\n\n    const new_state: ReadyState = if (is_abort) .unsent else .done;\n    if (new_state != self._ready_state) {\n        const page = self._page;\n\n        try self.stateChanged(new_state, page);\n        if (is_abort) {\n            try self._proto.dispatch(.abort, null, page);\n        }\n        try self._proto.dispatch(.err, null, page);\n        try self._proto.dispatch(.load_end, null, page);\n    }\n\n    const level: log.Level = if (err == error.Abort) .debug else .err;\n    log.log(.http, level, \"error\", .{\n        .url = self._url,\n        .err = err,\n        .source = \"xhr.handleError\",\n    });\n}\n\nfn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {\n    if (state == self._ready_state) {\n        return;\n    }\n\n    self._ready_state = state;\n\n    const target = self.asEventTarget();\n    if (page._event_manager.hasDirectListeners(target, \"readystatechange\", self._on_ready_state_change)) {\n        const event = try Event.initTrusted(.wrap(\"readystatechange\"), .{}, page);\n        try page._event_manager.dispatchDirect(target, event, self._on_ready_state_change, .{ .context = \"XHR state change\" });\n    }\n}\n\nfn parseMethod(method: []const u8) !net_http.Method {\n    if (std.ascii.eqlIgnoreCase(method, \"get\")) {\n        return .GET;\n    }\n    if (std.ascii.eqlIgnoreCase(method, \"post\")) {\n        return .POST;\n    }\n    if (std.ascii.eqlIgnoreCase(method, \"delete\")) {\n        return .DELETE;\n    }\n    if (std.ascii.eqlIgnoreCase(method, \"put\")) {\n        return .PUT;\n    }\n    if (std.ascii.eqlIgnoreCase(method, \"propfind\")) {\n        return .PROPFIND;\n    }\n    return error.InvalidMethod;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(XMLHttpRequest);\n\n    pub const Meta = struct {\n        pub const name = \"XMLHttpRequest\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n        pub const weak = true;\n        pub const finalizer = bridge.finalizer(XMLHttpRequest.deinit);\n    };\n\n    pub const constructor = bridge.constructor(XMLHttpRequest.init, .{});\n    pub const UNSENT = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.unsent), .{ .template = true });\n    pub const OPENED = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.opened), .{ .template = true });\n    pub const HEADERS_RECEIVED = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.headers_received), .{ .template = true });\n    pub const LOADING = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.loading), .{ .template = true });\n    pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true });\n\n    pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});\n    pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true });\n    pub const open = bridge.function(XMLHttpRequest.open, .{});\n    pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true });\n    pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{});\n    pub const status = bridge.accessor(XMLHttpRequest.getStatus, null, .{});\n    pub const statusText = bridge.accessor(XMLHttpRequest.getStatusText, null, .{});\n    pub const readyState = bridge.accessor(XMLHttpRequest.getReadyState, null, .{});\n    pub const response = bridge.accessor(XMLHttpRequest.getResponse, null, .{});\n    pub const responseText = bridge.accessor(XMLHttpRequest.getResponseText, null, .{});\n    pub const responseXML = bridge.accessor(XMLHttpRequest.getResponseXML, null, .{});\n    pub const responseURL = bridge.accessor(XMLHttpRequest.getResponseURL, null, .{});\n    pub const setRequestHeader = bridge.function(XMLHttpRequest.setRequestHeader, .{ .dom_exception = true });\n    pub const getResponseHeader = bridge.function(XMLHttpRequest.getResponseHeader, .{});\n    pub const getAllResponseHeaders = bridge.function(XMLHttpRequest.getAllResponseHeaders, .{});\n    pub const abort = bridge.function(XMLHttpRequest.abort, .{});\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: XHR\" {\n    try testing.htmlRunner(\"net/xhr.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/net/XMLHttpRequestEventTarget.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst EventTarget = @import(\"../EventTarget.zig\");\nconst ProgressEvent = @import(\"../event/ProgressEvent.zig\");\n\nconst XMLHttpRequestEventTarget = @This();\n\n_type: Type,\n_proto: *EventTarget,\n_on_abort: ?js.Function.Temp = null,\n_on_error: ?js.Function.Temp = null,\n_on_load: ?js.Function.Temp = null,\n_on_load_end: ?js.Function.Temp = null,\n_on_load_start: ?js.Function.Temp = null,\n_on_progress: ?js.Function.Temp = null,\n_on_timeout: ?js.Function.Temp = null,\n\npub const Type = union(enum) {\n    request: *@import(\"XMLHttpRequest.zig\"),\n    // TODO: xml_http_request_upload\n};\n\npub fn asEventTarget(self: *XMLHttpRequestEventTarget) *EventTarget {\n    return self._proto;\n}\n\npub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchType, progress_: ?Progress, page: *Page) !void {\n    const field, const typ = comptime blk: {\n        break :blk switch (event_type) {\n            .abort => .{ \"_on_abort\", \"abort\" },\n            .err => .{ \"_on_error\", \"error\" },\n            .load => .{ \"_on_load\", \"load\" },\n            .load_end => .{ \"_on_load_end\", \"loadend\" },\n            .load_start => .{ \"_on_load_start\", \"loadstart\" },\n            .progress => .{ \"_on_progress\", \"progress\" },\n            .timeout => .{ \"_on_timeout\", \"timeout\" },\n        };\n    };\n\n    const progress = progress_ orelse Progress{};\n    const event = (try ProgressEvent.initTrusted(\n        comptime .wrap(typ),\n        .{ .total = progress.total, .loaded = progress.loaded },\n        page,\n    )).asEvent();\n\n    return page._event_manager.dispatchDirect(\n        self.asEventTarget(),\n        event,\n        @field(self, field),\n        .{ .context = \"XHR \" ++ typ },\n    );\n}\n\npub fn getOnAbort(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_abort;\n}\n\npub fn setOnAbort(self: *XMLHttpRequestEventTarget, cb: ?js.Function.Temp) !void {\n    self._on_abort = cb;\n}\n\npub fn getOnError(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_error;\n}\n\npub fn setOnError(self: *XMLHttpRequestEventTarget, cb: ?js.Function.Temp) !void {\n    self._on_error = cb;\n}\n\npub fn getOnLoad(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_load;\n}\n\npub fn setOnLoad(self: *XMLHttpRequestEventTarget, cb: ?js.Function.Temp) !void {\n    self._on_load = cb;\n}\n\npub fn getOnLoadEnd(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_load_end;\n}\n\npub fn setOnLoadEnd(self: *XMLHttpRequestEventTarget, cb: ?js.Function.Temp) !void {\n    self._on_load_end = cb;\n}\n\npub fn getOnLoadStart(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_load_start;\n}\n\npub fn setOnLoadStart(self: *XMLHttpRequestEventTarget, cb: ?js.Function.Temp) !void {\n    self._on_load_start = cb;\n}\n\npub fn getOnProgress(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_progress;\n}\n\npub fn setOnProgress(self: *XMLHttpRequestEventTarget, cb: ?js.Function.Temp) !void {\n    self._on_progress = cb;\n}\n\npub fn getOnTimeout(self: *const XMLHttpRequestEventTarget) ?js.Function.Temp {\n    return self._on_timeout;\n}\n\npub fn setOnTimeout(self: *XMLHttpRequestEventTarget, cb_: ?js.Function) !void {\n    if (cb_) |cb| {\n        self._on_timeout = try cb.tempWithThis(self);\n    } else {\n        self._on_timeout = null;\n    }\n}\n\nconst DispatchType = enum {\n    abort,\n    err,\n    load,\n    load_end,\n    load_start,\n    progress,\n    timeout,\n};\n\nconst Progress = struct {\n    loaded: usize = 0,\n    total: usize = 0,\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(XMLHttpRequestEventTarget);\n\n    pub const Meta = struct {\n        pub const name = \"XMLHttpRequestEventTarget\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const onloadstart = bridge.accessor(XMLHttpRequestEventTarget.getOnLoadStart, XMLHttpRequestEventTarget.setOnLoadStart, .{});\n    pub const onprogress = bridge.accessor(XMLHttpRequestEventTarget.getOnProgress, XMLHttpRequestEventTarget.setOnProgress, .{});\n    pub const onabort = bridge.accessor(XMLHttpRequestEventTarget.getOnAbort, XMLHttpRequestEventTarget.setOnAbort, .{});\n    pub const onerror = bridge.accessor(XMLHttpRequestEventTarget.getOnError, XMLHttpRequestEventTarget.setOnError, .{});\n    pub const onload = bridge.accessor(XMLHttpRequestEventTarget.getOnLoad, XMLHttpRequestEventTarget.setOnLoad, .{});\n    pub const ontimeout = bridge.accessor(XMLHttpRequestEventTarget.getOnTimeout, XMLHttpRequestEventTarget.setOnTimeout, .{});\n    pub const onloadend = bridge.accessor(XMLHttpRequestEventTarget.getOnLoadEnd, XMLHttpRequestEventTarget.setOnLoadEnd, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/selector/List.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Page = @import(\"../../Page.zig\");\nconst Session = @import(\"../../Session.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Part = @import(\"Selector.zig\").Part;\nconst Selector = @import(\"Selector.zig\");\nconst TreeWalker = @import(\"../TreeWalker.zig\").Full;\nconst GenericIterator = @import(\"../collections/iterator.zig\").Entry;\n\nconst Allocator = std.mem.Allocator;\n\nconst List = @This();\n\n_nodes: []const *Node,\n_arena: Allocator,\n// For the [somewhat common] case where we just have an #id selector\n// we can avoid allocating a slice and just use this.\n_single_node: [1]*Node = undefined,\n\npub const EntryIterator = GenericIterator(Iterator, null);\npub const KeyIterator = GenericIterator(Iterator, \"0\");\npub const ValueIterator = GenericIterator(Iterator, \"1\");\n\npub fn deinit(self: *const List, session: *Session) void {\n    session.releaseArena(self._arena);\n}\n\npub fn collect(\n    allocator: std.mem.Allocator,\n    root: *Node,\n    selector: Selector.Selector,\n    nodes: *std.AutoArrayHashMapUnmanaged(*Node, void),\n    page: *Page,\n) !void {\n    if (optimizeSelector(root, &selector, page)) |result| {\n        var tw = TreeWalker.init(result.root, .{});\n        if (result.exclude_root) {\n            _ = tw.next();\n        }\n\n        while (tw.next()) |node| {\n            if (matches(node, result.selector, root, page)) {\n                try nodes.put(allocator, node, {});\n            }\n        }\n    }\n}\n\n// used internally to find the first match\npub fn initOne(root: *Node, selector: Selector.Selector, page: *Page) ?*Node {\n    const result = optimizeSelector(root, &selector, page) orelse return null;\n\n    var tw = TreeWalker.init(result.root, .{});\n    if (result.exclude_root) {\n        _ = tw.next();\n    }\n    while (tw.next()) |node| {\n        if (matches(node, result.selector, root, page)) {\n            return node;\n        }\n    }\n    return null;\n}\n\nconst OptimizeResult = struct {\n    root: *Node,\n    exclude_root: bool,\n    selector: Selector.Selector,\n};\n\nfn optimizeSelector(root: *Node, selector: *const Selector.Selector, page: *Page) ?OptimizeResult {\n    const anchor = findIdSelector(selector) orelse return .{\n        .root = root,\n        .selector = selector.*,\n        // Always exclude root - querySelector only returns descendants\n        .exclude_root = true,\n    };\n\n    // If we have a selector with an #id, we can make a pretty easy and\n    // powerful optimization. We can use the node for that id as the new\n    // root, and only match the selectors after it. However, we'll need to\n    // make sure that node matches the selectors before it (the prefix).\n    const id = anchor.id;\n    const segment_index = anchor.segment_index;\n\n    // Look up the element by ID (O(1) hash map lookup)\n    const id_element = page.getElementByIdFromNode(root, id) orelse return null;\n    const id_node = id_element.asNode();\n\n    if (!root.contains(id_node)) {\n        return null;\n    }\n\n    // If the ID is in the first compound\n    if (segment_index == null) {\n        // Check if there are any segments after the ID\n        if (selector.segments.len == 0) {\n            // Just '#id', return the node itself\n            return .{\n                .root = id_node,\n                .selector = .{\n                    .first = selector.first,\n                    .segments = selector.segments,\n                },\n                .exclude_root = false,\n            };\n        }\n\n        // Check the combinator of the first segment\n        const first_combinator = selector.segments[0].combinator;\n        if (first_combinator == .next_sibling or first_combinator == .subsequent_sibling) {\n            // Cannot optimize: matches are siblings, not descendants of the ID node\n            // Fall back to searching the entire tree\n            return .{\n                .root = root,\n                .selector = selector.*,\n                .exclude_root = true,\n            };\n        }\n\n        // Safe to optimize for descendant/child combinators\n        return .{\n            .root = id_node,\n            .selector = .{\n                .first = selector.first,\n                .segments = selector.segments,\n            },\n            .exclude_root = true,\n        };\n    }\n\n    // ID is in one of the segments\n    const seg_idx = segment_index.?;\n\n    // Check if there are segments after the ID\n    if (seg_idx + 1 < selector.segments.len) {\n        // Check the combinator of the segment after the ID\n        const next_combinator = selector.segments[seg_idx + 1].combinator;\n        if (next_combinator == .next_sibling or next_combinator == .subsequent_sibling) {\n            // Cannot optimize: matches are siblings, not descendants\n            return .{\n                .root = root,\n                .selector = selector.*,\n                .exclude_root = true,\n            };\n        }\n    }\n\n    // If there's a prefix selector, we need to verify that the id_node's\n    // ancestors match it. We construct a selector up to and including the ID segment.\n    const prefix_selector = Selector.Selector{\n        .first = selector.first,\n        .segments = selector.segments[0 .. seg_idx + 1],\n    };\n\n    if (!matches(id_node, prefix_selector, id_node, page)) {\n        return null;\n    }\n\n    // Return a selector starting from the segments after the ID\n    return .{\n        .root = id_node,\n        .selector = .{\n            .first = selector.segments[seg_idx].compound,\n            .segments = selector.segments[seg_idx + 1 ..],\n        },\n        .exclude_root = false,\n    };\n}\n\npub fn getLength(self: *const List) usize {\n    return self._nodes.len;\n}\n\npub fn keys(self: *List, page: *Page) !*KeyIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn values(self: *List, page: *Page) !*ValueIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn entries(self: *List, page: *Page) !*EntryIterator {\n    return .init(.{ .list = self }, page);\n}\n\npub fn getAtIndex(self: *const List, index: usize) !?*Node {\n    if (index >= self._nodes.len) {\n        return null;\n    }\n    return self._nodes[index];\n}\n\nconst NodeList = @import(\"../collections/NodeList.zig\");\npub fn runtimeGenericWrap(self: *List, _: *const Page) !*NodeList {\n    const nl = try self._arena.create(NodeList);\n    nl.* = .{\n        ._data = .{ .selector_list = self },\n    };\n    return nl;\n}\n\nconst IdAnchor = struct {\n    id: []const u8,\n    segment_index: ?usize, // null if ID is in first compound\n};\n\n// Rightmost (last) is best because it minimizes the subtree we need to search\nfn findIdSelector(selector: *const Selector.Selector) ?IdAnchor {\n    // Check segments from right to left\n    var i = selector.segments.len;\n    while (i > 0) {\n        i -= 1;\n        const compound = selector.segments[i].compound.parts;\n        if (compound.len != 1) {\n            continue;\n        }\n        const part = compound[0];\n        if (part == .id) {\n            return .{ .id = part.id, .segment_index = i };\n        }\n    }\n\n    // Check the first compound\n    if (selector.first.parts.len == 1) {\n        const part = selector.first.parts[0];\n        if (part == .id) {\n            return .{ .id = part.id, .segment_index = null };\n        }\n    }\n\n    return null;\n}\n\npub fn matches(node: *Node, selector: Selector.Selector, scope: *Node, page: *Page) bool {\n    const el = node.is(Node.Element) orelse return false;\n\n    if (selector.segments.len == 0) {\n        return matchesCompound(el, selector.first, scope, page);\n    }\n\n    const last_segment = selector.segments[selector.segments.len - 1];\n    if (!matchesCompound(el, last_segment.compound, scope, page)) {\n        return false;\n    }\n\n    return matchSegments(node, selector, selector.segments.len - 1, null, scope, page);\n}\n\n// Match segments backward, with support for backtracking on subsequent_sibling\nfn matchSegments(node: *Node, selector: Selector.Selector, segment_index: usize, root: ?*Node, scope: *Node, page: *Page) bool {\n    const segment = selector.segments[segment_index];\n    const target_compound = if (segment_index == 0)\n        selector.first\n    else\n        selector.segments[segment_index - 1].compound;\n\n    const matched: ?*Node = switch (segment.combinator) {\n        .descendant => matchDescendant(node, target_compound, root, scope, page),\n        .child => matchChild(node, target_compound, root, scope, page),\n        .next_sibling => matchNextSibling(node, target_compound, scope, page),\n        .subsequent_sibling => {\n            // For subsequent_sibling, try all matching siblings with backtracking\n            var sibling = node.previousSibling();\n            while (sibling) |s| {\n                const sibling_el = s.is(Node.Element) orelse {\n                    sibling = s.previousSibling();\n                    continue;\n                };\n\n                if (matchesCompound(sibling_el, target_compound, scope, page)) {\n                    // If we're at the first segment, we found a match\n                    if (segment_index == 0) {\n                        return true;\n                    }\n                    // Try to match remaining segments from this sibling\n                    if (matchSegments(s, selector, segment_index - 1, root, scope, page)) {\n                        return true;\n                    }\n                    // This sibling didn't work, try the next one\n                }\n                sibling = s.previousSibling();\n            }\n            return false;\n        },\n    };\n\n    // For non-subsequent_sibling combinators, matched is either the node or null\n    if (segment.combinator != .subsequent_sibling) {\n        const current = matched orelse return false;\n        if (segment_index == 0) {\n            return true;\n        }\n        return matchSegments(current, selector, segment_index - 1, root, scope, page);\n    }\n\n    // subsequent_sibling already handled its recursion above\n    return false;\n}\n\n// Find an ancestor that matches the compound (any distance up the tree)\nfn matchDescendant(node: *Node, compound: Selector.Compound, root: ?*Node, scope: *Node, page: *Page) ?*Node {\n    var current = node._parent;\n\n    while (current) |ancestor| {\n        if (ancestor.is(Node.Element)) |ancestor_el| {\n            if (matchesCompound(ancestor_el, compound, scope, page)) {\n                return ancestor;\n            }\n        }\n\n        // Stop if we've reached the boundary\n        if (root) |boundary| {\n            if (ancestor == boundary) {\n                return null;\n            }\n        }\n\n        current = ancestor._parent;\n    }\n\n    return null;\n}\n\n// Find the direct parent if it matches the compound\nfn matchChild(node: *Node, compound: Selector.Compound, root: ?*Node, scope: *Node, page: *Page) ?*Node {\n    const parent = node._parent orelse return null;\n\n    // Don't match beyond the root boundary\n    // If there's a boundary, check if parent is outside (an ancestor of) the boundary\n    if (root) |boundary| {\n        if (!boundary.contains(parent)) {\n            return null;\n        }\n    }\n\n    const parent_el = parent.is(Node.Element) orelse return null;\n\n    if (matchesCompound(parent_el, compound, scope, page)) {\n        return parent;\n    }\n\n    return null;\n}\n\n// Find the immediately preceding sibling if it matches the compound\nfn matchNextSibling(node: *Node, compound: Selector.Compound, scope: *Node, page: *Page) ?*Node {\n    var sibling = node.previousSibling();\n\n    // For next_sibling (+), we need the immediately preceding element sibling\n    while (sibling) |s| {\n        const sibling_el = s.is(Node.Element) orelse {\n            // Skip non-element nodes\n            sibling = s.previousSibling();\n            continue;\n        };\n\n        // Found an element - check if it matches\n        if (matchesCompound(sibling_el, compound, scope, page)) {\n            return s;\n        }\n        // we found an element, it wasn't a match, we're done\n        return null;\n    }\n\n    return null;\n}\n\n// Find any preceding sibling that matches the compound\nfn matchSubsequentSibling(node: *Node, compound: Selector.Compound, scope: *Node, page: *Page) ?*Node {\n    var sibling = node.previousSibling();\n\n    // For subsequent_sibling (~), check all preceding element siblings\n    while (sibling) |s| {\n        const sibling_el = s.is(Node.Element) orelse {\n            // Skip non-element nodes\n            sibling = s.previousSibling();\n            continue;\n        };\n\n        if (matchesCompound(sibling_el, compound, scope, page)) {\n            return s;\n        }\n\n        sibling = s.previousSibling();\n    }\n\n    return null;\n}\n\nfn matchesCompound(el: *Node.Element, compound: Selector.Compound, scope: *Node, page: *Page) bool {\n    // For compound selectors, ALL parts must match\n    for (compound.parts) |part| {\n        if (!matchesPart(el, part, scope, page)) {\n            return false;\n        }\n    }\n    return true;\n}\n\nfn matchesPart(el: *Node.Element, part: Part, scope: *Node, page: *Page) bool {\n    switch (part) {\n        .id => |id| {\n            const element_id = el.getAttributeSafe(comptime .wrap(\"id\")) orelse return false;\n            return std.mem.eql(u8, element_id, id);\n        },\n        .class => |cls| {\n            const class_attr = el.getAttributeSafe(comptime .wrap(\"class\")) orelse return false;\n            return Selector.classAttributeContains(class_attr, cls);\n        },\n        .tag => |tag| {\n            // Optimized: compare enum directly\n            return el.getTag() == tag;\n        },\n        .tag_name => |tag_name| {\n            // Fallback for custom/unknown tags\n            // Both are lowercase, so we can use fast string comparison\n            const element_tag = el.getTagNameLower();\n            return std.mem.eql(u8, element_tag, tag_name);\n        },\n        .universal => return true,\n        .pseudo_class => |pseudo| return matchesPseudoClass(el, pseudo, scope, page),\n        .attribute => |attr| return matchesAttribute(el, attr),\n    }\n}\n\nfn matchesAttribute(el: *Node.Element, attr: Selector.Attribute) bool {\n    const value = el.getAttributeSafe(attr.name) orelse {\n        return false;\n    };\n\n    switch (attr.matcher) {\n        .presence => return true,\n        .exact => |expected| {\n            return if (attr.case_insensitive)\n                std.ascii.eqlIgnoreCase(value, expected)\n            else\n                std.mem.eql(u8, value, expected);\n        },\n        .substring => |expected| {\n            return if (attr.case_insensitive)\n                std.ascii.indexOfIgnoreCase(value, expected) != null\n            else\n                std.mem.indexOf(u8, value, expected) != null;\n        },\n        .starts_with => |expected| {\n            return if (attr.case_insensitive)\n                std.ascii.startsWithIgnoreCase(value, expected)\n            else\n                std.mem.startsWith(u8, value, expected);\n        },\n        .ends_with => |expected| {\n            return if (attr.case_insensitive)\n                std.ascii.endsWithIgnoreCase(value, expected)\n            else\n                std.mem.endsWith(u8, value, expected);\n        },\n        .word => |expected| {\n            // Space-separated word match (like class names)\n            var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace);\n            while (it.next()) |word| {\n                const same = if (attr.case_insensitive)\n                    std.ascii.eqlIgnoreCase(word, expected)\n                else\n                    std.mem.eql(u8, word, expected);\n\n                if (same) return true;\n            }\n            return false;\n        },\n        .prefix_dash => |expected| {\n            // Matches value or value- prefix (for language codes like en, en-US)\n            if (attr.case_insensitive) {\n                if (std.ascii.eqlIgnoreCase(value, expected)) return true;\n                if (value.len > expected.len and value[expected.len] == '-') {\n                    return std.ascii.eqlIgnoreCase(value[0..expected.len], expected);\n                }\n            } else {\n                if (std.mem.eql(u8, value, expected)) return true;\n                if (value.len > expected.len and value[expected.len] == '-') {\n                    return std.mem.eql(u8, value[0..expected.len], expected);\n                }\n            }\n            return false;\n        },\n    }\n}\n\nfn attributeContainsWord(value: []const u8, word: []const u8) bool {\n    var remaining = value;\n    while (remaining.len > 0) {\n        const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace);\n        if (trimmed.len == 0) return false;\n\n        const end = std.mem.indexOfAny(u8, trimmed, &std.ascii.whitespace) orelse trimmed.len;\n        const current_word = trimmed[0..end];\n\n        if (std.mem.eql(u8, current_word, word)) {\n            return true;\n        }\n\n        if (end >= trimmed.len) break;\n        remaining = trimmed[end..];\n    }\n    return false;\n}\n\nfn matchesPseudoClass(el: *Node.Element, pseudo: Selector.PseudoClass, scope: *Node, page: *Page) bool {\n    const node = el.asNode();\n    switch (pseudo) {\n        // State pseudo-classes\n        .modal => return false,\n        .checked => {\n            const input = el.is(Node.Element.Html.Input) orelse return false;\n            return input.getChecked();\n        },\n        .disabled => {\n            return el.getAttributeSafe(comptime .wrap(\"disabled\")) != null;\n        },\n        .enabled => {\n            return el.getAttributeSafe(comptime .wrap(\"disabled\")) == null;\n        },\n        .indeterminate => {\n            const input = el.is(Node.Element.Html.Input) orelse return false;\n            return switch (input._input_type) {\n                .checkbox => input.getIndeterminate(),\n                else => false,\n            };\n        },\n\n        // Form validation\n        .valid => {\n            if (el.is(Node.Element.Html.Input)) |input| {\n                return switch (input._input_type) {\n                    .hidden, .submit, .reset, .button => false,\n                    else => !input.getRequired() or input.getValue().len > 0,\n                };\n            }\n            if (el.is(Node.Element.Html.Select)) |select| {\n                return !select.getRequired() or select.getValue(page).len > 0;\n            }\n            if (el.is(Node.Element.Html.Form) != null or el.is(Node.Element.Html.FieldSet) != null) {\n                return !hasInvalidDescendant(node, page);\n            }\n            return false;\n        },\n        .invalid => {\n            if (el.is(Node.Element.Html.Input)) |input| {\n                return switch (input._input_type) {\n                    .hidden, .submit, .reset, .button => false,\n                    else => input.getRequired() and input.getValue().len == 0,\n                };\n            }\n            if (el.is(Node.Element.Html.Select)) |select| {\n                return select.getRequired() and select.getValue(page).len == 0;\n            }\n            if (el.is(Node.Element.Html.Form) != null or el.is(Node.Element.Html.FieldSet) != null) {\n                return hasInvalidDescendant(node, page);\n            }\n            return false;\n        },\n        .required => {\n            return el.getAttributeSafe(comptime .wrap(\"required\")) != null;\n        },\n        .optional => {\n            return el.getAttributeSafe(comptime .wrap(\"required\")) == null;\n        },\n        .in_range => return false,\n        .out_of_range => return false,\n        .placeholder_shown => return false,\n        .read_only => {\n            return el.getAttributeSafe(comptime .wrap(\"readonly\")) != null;\n        },\n        .read_write => {\n            return el.getAttributeSafe(comptime .wrap(\"readonly\")) == null;\n        },\n        .default => return false,\n\n        // User interaction\n        .hover => return false,\n        .active => return false,\n        .focus => {\n            const active = page.document._active_element orelse return false;\n            return active == el;\n        },\n        .focus_within => {\n            const active = page.document._active_element orelse return false;\n            return node.contains(active.asNode());\n        },\n        .focus_visible => return false,\n\n        // Link states\n        .link => return false,\n        .visited => return false,\n        .any_link => {\n            if (el.getTag() != .anchor) return false;\n            return el.getAttributeSafe(comptime .wrap(\"href\")) != null;\n        },\n        .target => {\n            const element_id = el.getAttributeSafe(comptime .wrap(\"id\")) orelse return false;\n            const location = page.document._location orelse return false;\n            const hash = location.getHash();\n            if (hash.len <= 1) return false;\n            return std.mem.eql(u8, element_id, hash[1..]);\n        },\n\n        // Tree structural\n        .root => {\n            const parent = node.parentNode() orelse return false;\n            return parent._type == .document;\n        },\n        .scope => {\n            // :scope matches the reference element (querySelector root)\n            return node == scope;\n        },\n        .empty => {\n            return node.firstChild() == null;\n        },\n        .first_child => return isFirstChild(el),\n        .last_child => return isLastChild(el),\n        .only_child => return isFirstChild(el) and isLastChild(el),\n        .first_of_type => return isFirstOfType(el),\n        .last_of_type => return isLastOfType(el),\n        .only_of_type => return isFirstOfType(el) and isLastOfType(el),\n        .nth_child => |pattern| return matchesNthChild(el, pattern),\n        .nth_last_child => |pattern| return matchesNthLastChild(el, pattern),\n        .nth_of_type => |pattern| return matchesNthOfType(el, pattern),\n        .nth_last_of_type => |pattern| return matchesNthLastOfType(el, pattern),\n\n        // Custom elements\n        .defined => {\n            const tag_name = el.getTagNameLower();\n            if (std.mem.indexOfScalar(u8, tag_name, '-') == null) return true;\n            const registry = &page.window._custom_elements;\n            return registry.get(tag_name) != null;\n        },\n\n        // Functional\n        .lang => return false,\n        .not => |selectors| {\n            for (selectors) |selector| {\n                if (matches(node, selector, scope, page)) {\n                    return false;\n                }\n            }\n            return true;\n        },\n        .is => |selectors| {\n            for (selectors) |selector| {\n                if (matches(node, selector, scope, page)) {\n                    return true;\n                }\n            }\n            return false;\n        },\n        .where => |selectors| {\n            for (selectors) |selector| {\n                if (matches(node, selector, scope, page)) {\n                    return true;\n                }\n            }\n            return false;\n        },\n        .has => |selectors| {\n            for (selectors) |selector| {\n                var child = node.firstChild();\n                while (child) |c| {\n                    const child_el = c.is(Node.Element) orelse {\n                        child = c.nextSibling();\n                        continue;\n                    };\n\n                    if (matches(child_el.asNode(), selector, scope, page)) {\n                        return true;\n                    }\n\n                    if (matchesHasDescendant(child_el, selector, scope, page)) {\n                        return true;\n                    }\n\n                    child = c.nextSibling();\n                }\n            }\n            return false;\n        },\n    }\n}\n\nfn matchesHasDescendant(el: *Node.Element, selector: Selector.Selector, scope: *Node, page: *Page) bool {\n    var child = el.asNode().firstChild();\n    while (child) |c| {\n        const child_el = c.is(Node.Element) orelse {\n            child = c.nextSibling();\n            continue;\n        };\n\n        if (matches(child_el.asNode(), selector, scope, page)) {\n            return true;\n        }\n\n        if (matchesHasDescendant(child_el, selector, scope, page)) {\n            return true;\n        }\n\n        child = c.nextSibling();\n    }\n    return false;\n}\n\nfn hasInvalidDescendant(parent: *Node, page: *Page) bool {\n    var child = parent.firstChild();\n    while (child) |c| {\n        if (c.is(Node.Element)) |child_el| {\n            if (child_el.is(Node.Element.Html.Input)) |input| {\n                const invalid = switch (input._input_type) {\n                    .hidden, .submit, .reset, .button => false,\n                    else => input.getRequired() and input.getValue().len == 0,\n                };\n                if (invalid) return true;\n            } else if (child_el.is(Node.Element.Html.Select)) |select| {\n                if (select.getRequired() and select.getValue(page).len == 0) return true;\n            }\n        }\n        if (hasInvalidDescendant(c, page)) return true;\n        child = c.nextSibling();\n    }\n    return false;\n}\n\nfn isFirstChild(el: *Node.Element) bool {\n    const node = el.asNode();\n    var sibling = node.previousSibling();\n\n    // Check if there are any element siblings before this one\n    while (sibling) |s| {\n        if (s.is(Node.Element)) |_| {\n            return false;\n        }\n        sibling = s.previousSibling();\n    }\n\n    return true;\n}\n\nfn isLastChild(el: *Node.Element) bool {\n    const node = el.asNode();\n    var sibling = node.nextSibling();\n\n    // Check if there are any element siblings after this one\n    while (sibling) |s| {\n        if (s.is(Node.Element)) |_| {\n            return false;\n        }\n        sibling = s.nextSibling();\n    }\n\n    return true;\n}\n\nfn isFirstOfType(el: *Node.Element) bool {\n    const tag = el.getTag();\n    const node = el.asNode();\n    var sibling = node.previousSibling();\n\n    // Check if there are any element siblings of the same type before this one\n    while (sibling) |s| {\n        const sibling_el = s.is(Node.Element) orelse {\n            sibling = s.previousSibling();\n            continue;\n        };\n\n        if (sibling_el.getTag() == tag) {\n            return false;\n        }\n\n        sibling = s.previousSibling();\n    }\n\n    return true;\n}\n\nfn isLastOfType(el: *Node.Element) bool {\n    const tag = el.getTag();\n    const node = el.asNode();\n    var sibling = node.nextSibling();\n\n    // Check if there are any element siblings of the same type after this one\n    while (sibling) |s| {\n        const sibling_el = s.is(Node.Element) orelse {\n            sibling = s.nextSibling();\n            continue;\n        };\n\n        if (sibling_el.getTag() == tag) {\n            return false;\n        }\n\n        sibling = s.nextSibling();\n    }\n\n    return true;\n}\n\nfn matchesNthChild(el: *Node.Element, pattern: Selector.NthPattern) bool {\n    const index = getChildIndex(el) orelse return false;\n    return matchesNthPattern(index, pattern);\n}\n\nfn matchesNthLastChild(el: *Node.Element, pattern: Selector.NthPattern) bool {\n    const index = getChildIndexFromEnd(el) orelse return false;\n    return matchesNthPattern(index, pattern);\n}\n\nfn matchesNthOfType(el: *Node.Element, pattern: Selector.NthPattern) bool {\n    const index = getTypeIndex(el) orelse return false;\n    return matchesNthPattern(index, pattern);\n}\n\nfn matchesNthLastOfType(el: *Node.Element, pattern: Selector.NthPattern) bool {\n    const index = getTypeIndexFromEnd(el) orelse return false;\n    return matchesNthPattern(index, pattern);\n}\n\nfn getChildIndex(el: *Node.Element) ?usize {\n    const node = el.asNode();\n    var index: usize = 1;\n    var sibling = node.previousSibling();\n\n    while (sibling) |s| {\n        if (s.is(Node.Element)) |_| {\n            index += 1;\n        }\n        sibling = s.previousSibling();\n    }\n\n    return index;\n}\n\nfn getChildIndexFromEnd(el: *Node.Element) ?usize {\n    const node = el.asNode();\n    var index: usize = 1;\n    var sibling = node.nextSibling();\n\n    while (sibling) |s| {\n        if (s.is(Node.Element)) |_| {\n            index += 1;\n        }\n        sibling = s.nextSibling();\n    }\n\n    return index;\n}\n\nfn getTypeIndex(el: *Node.Element) ?usize {\n    const tag = el.getTag();\n    const node = el.asNode();\n\n    var index: usize = 1;\n    var sibling = node.previousSibling();\n\n    while (sibling) |s| {\n        const sibling_el = s.is(Node.Element) orelse {\n            sibling = s.previousSibling();\n            continue;\n        };\n\n        if (sibling_el.getTag() == tag) {\n            index += 1;\n        }\n\n        sibling = s.previousSibling();\n    }\n\n    return index;\n}\n\nfn getTypeIndexFromEnd(el: *Node.Element) ?usize {\n    const tag = el.getTag();\n    const node = el.asNode();\n\n    var index: usize = 1;\n    var sibling = node.nextSibling();\n\n    while (sibling) |s| {\n        const sibling_el = s.is(Node.Element) orelse {\n            sibling = s.nextSibling();\n            continue;\n        };\n\n        if (sibling_el.getTag() == tag) {\n            index += 1;\n        }\n\n        sibling = s.nextSibling();\n    }\n\n    return index;\n}\n\nfn matchesNthPattern(index: usize, pattern: Selector.NthPattern) bool {\n    const a = pattern.a;\n    const b = pattern.b;\n\n    // Special case: a=0 means we're matching a specific index\n    if (a == 0) {\n        return @as(i32, @intCast(index)) == b;\n    }\n\n    // For an+b pattern, we need to find if there's an integer n >= 0\n    // such that an + b = index\n    // Rearranging: n = (index - b) / a\n    const index_i = @as(i32, @intCast(index));\n    const diff = index_i - b;\n\n    // Check if (index - b) is divisible by a\n    if (@rem(diff, a) != 0) {\n        return false;\n    }\n\n    const n = @divTrunc(diff, a);\n\n    // n must be non-negative\n    return n >= 0;\n}\n\nconst Iterator = struct {\n    index: u32 = 0,\n    list: *List,\n\n    const Entry = struct { u32, *Node };\n\n    pub fn next(self: *Iterator, _: *const Page) ?Entry {\n        const index = self.index;\n        if (index >= self.list._nodes.len) {\n            return null;\n        }\n        self.index = index + 1;\n        return .{ index, self.list._nodes[index] };\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/selector/Parser.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst Page = @import(\"../../Page.zig\");\n\nconst Node = @import(\"../Node.zig\");\nconst Attribute = @import(\"../element/Attribute.zig\");\n\nconst Selector = @import(\"Selector.zig\");\n\nconst Part = Selector.Part;\nconst Segment = Selector.Segment;\nconst Combinator = Selector.Combinator;\nconst Allocator = std.mem.Allocator;\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst Parser = @This();\n\ninput: []const u8,\n\n// need an explicit error set because the function is recursive\nconst ParseError = error{\n    OutOfMemory,\n    InvalidIDSelector,\n    InvalidClassSelector,\n    InvalidAttributeSelector,\n    InvalidPseudoClass,\n    InvalidNthPattern,\n    UnknownPseudoClass,\n    InvalidTagSelector,\n    InvalidSelector,\n    StringTooLarge,\n};\n\n// CSS Syntax preprocessing: normalize line endings (CRLF → LF, CR → LF)\n// https://drafts.csswg.org/css-syntax/#input-preprocessing\nfn preprocessInput(arena: Allocator, input: []const u8) ![]const u8 {\n    var i = std.mem.indexOfScalar(u8, input, '\\r') orelse return input;\n\n    var result = try std.ArrayList(u8).initCapacity(arena, input.len);\n    result.appendSliceAssumeCapacity(input[0..i]);\n\n    while (i < input.len) {\n        const c = input[i];\n        if (c == '\\r') {\n            result.appendAssumeCapacity('\\n');\n            i += 1;\n            if (i < input.len and input[i] == '\\n') {\n                i += 1;\n            }\n        } else {\n            result.appendAssumeCapacity(c);\n            i += 1;\n        }\n    }\n\n    return result.items;\n}\n\npub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]const Selector.Selector {\n    // Preprocess input to normalize line endings\n    const preprocessed = try preprocessInput(arena, input);\n\n    var selectors: std.ArrayList(Selector.Selector) = .empty;\n\n    var remaining = preprocessed;\n    while (true) {\n        const trimmed = std.mem.trimLeft(u8, remaining, &std.ascii.whitespace);\n        if (trimmed.len == 0) break;\n\n        var comma_pos: usize = trimmed.len;\n        var depth: usize = 0;\n        var in_quote: u8 = 0; // 0 = not in quotes, '\"' or '\\'' = in that quote type\n        var i: usize = 0;\n        while (i < trimmed.len) {\n            const c = trimmed[i];\n            if (in_quote != 0) {\n                // Inside a quoted string\n                if (c == '\\\\') {\n                    // Skip escape sequence inside quotes\n                    i += 1;\n                    if (i < trimmed.len) i += 1;\n                } else if (c == in_quote) {\n                    // Closing quote\n                    in_quote = 0;\n                    i += 1;\n                } else {\n                    i += 1;\n                }\n                continue;\n            }\n            switch (c) {\n                '\\\\' => {\n                    // Skip escape sequence (backslash + next character)\n                    i += 1;\n                    if (i < trimmed.len) i += 1;\n                },\n                '\"', '\\'' => {\n                    in_quote = c;\n                    i += 1;\n                },\n                '(' => {\n                    depth += 1;\n                    i += 1;\n                },\n                ')' => {\n                    if (depth > 0) depth -= 1;\n                    i += 1;\n                },\n                ',' => {\n                    if (depth == 0) {\n                        comma_pos = i;\n                        break;\n                    }\n                    i += 1;\n                },\n                else => {\n                    i += 1;\n                },\n            }\n        }\n\n        const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace);\n\n        if (selector_input.len > 0) {\n            const selector = try parse(arena, selector_input, page);\n            try selectors.append(arena, selector);\n        }\n\n        if (comma_pos >= trimmed.len) break;\n        remaining = trimmed[comma_pos + 1 ..];\n    }\n\n    if (selectors.items.len == 0) {\n        return error.InvalidSelector;\n    }\n\n    return selectors.items;\n}\n\npub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Selector.Selector {\n    var parser = Parser{ .input = input };\n    var segments: std.ArrayList(Segment) = .empty;\n    var current_compound: std.ArrayList(Part) = .empty;\n\n    // Parse the first compound (no combinator before it)\n    while (parser.skipSpaces()) {\n        if (parser.peek() == 0) break;\n\n        const part = try parser.parsePart(arena, page);\n        try current_compound.append(arena, part);\n\n        // Check what comes after this part\n        const start_pos = parser.input;\n        const has_whitespace = parser.skipSpacesConsumed();\n        const next = parser.peek();\n\n        if (next == 0) {\n            // End of input\n            break;\n        }\n\n        if (next == '>' or next == '+' or next == '~') {\n            // Explicit combinator\n            break;\n        }\n\n        if (has_whitespace and isStartOfPart(next)) {\n            // Whitespace followed by another selector part = descendant combinator\n            // Restore position before the whitespace so the segment loop can handle it\n            parser.input = start_pos;\n            break;\n        }\n\n        // If we have a non-whitespace character that could start a part,\n        // it's part of this compound (like \"div.class\" or \"div#id\")\n        if (!has_whitespace and isStartOfPart(next)) {\n            // Continue parsing this compound\n            continue;\n        }\n\n        // Otherwise, end of compound\n        break;\n    }\n\n    if (current_compound.items.len == 0) {\n        return error.InvalidSelector;\n    }\n\n    const first_compound = current_compound.items;\n    current_compound = .empty;\n\n    // Parse remaining segments with combinators\n    while (parser.skipSpaces()) {\n        const next = parser.peek();\n        if (next == 0) break;\n\n        // Parse combinator\n        const combinator: Combinator = switch (next) {\n            '>' => blk: {\n                parser.input = parser.input[1..];\n                break :blk .child;\n            },\n            '+' => blk: {\n                parser.input = parser.input[1..];\n                break :blk .next_sibling;\n            },\n            '~' => blk: {\n                parser.input = parser.input[1..];\n                break :blk .subsequent_sibling;\n            },\n            else => .descendant, // whitespace = descendant combinator\n        };\n\n        // Parse the compound that follows the combinator\n        _ = parser.skipSpaces();\n        if (parser.peek() == 0) {\n            return error.InvalidSelector; // Combinator with nothing after it\n        }\n\n        while (parser.skipSpaces()) {\n            if (parser.peek() == 0) break;\n\n            const part = try parser.parsePart(arena, page);\n            try current_compound.append(arena, part);\n\n            // Check what comes after this part\n            const seg_start_pos = parser.input;\n            const seg_has_whitespace = parser.skipSpacesConsumed();\n            const peek_next = parser.peek();\n\n            if (peek_next == 0) {\n                // End of input\n                break;\n            }\n\n            if (peek_next == '>' or peek_next == '+' or peek_next == '~') {\n                // Next combinator found\n                break;\n            }\n\n            if (seg_has_whitespace and isStartOfPart(peek_next)) {\n                // Whitespace followed by another part = new segment\n                // Restore position before whitespace\n                parser.input = seg_start_pos;\n                break;\n            }\n\n            // If no whitespace and it's a start of part, continue compound\n            if (!seg_has_whitespace and isStartOfPart(peek_next)) {\n                continue;\n            }\n\n            // Otherwise, end of compound\n            break;\n        }\n\n        if (current_compound.items.len == 0) {\n            return error.InvalidSelector;\n        }\n\n        try segments.append(arena, .{\n            .combinator = combinator,\n            .compound = .{ .parts = current_compound.items },\n        });\n        current_compound = .empty;\n    }\n\n    return .{\n        .first = .{ .parts = first_compound },\n        .segments = segments.items,\n    };\n}\n\nfn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part {\n    return switch (self.peek()) {\n        '#' => .{ .id = try self.id(arena) },\n        '.' => .{ .class = try self.class(arena) },\n        '*' => blk: {\n            self.input = self.input[1..];\n            break :blk .universal;\n        },\n        '[' => .{ .attribute = try self.attribute(arena, page) },\n        ':' => .{ .pseudo_class = try self.pseudoClass(arena, page) },\n        'a'...'z', 'A'...'Z', '_', '\\\\', 0x80...0xFF => blk: {\n            // Use parseIdentifier for full escape support\n            const tag_name = try self.parseIdentifier(arena, error.InvalidTagSelector);\n            if (tag_name.len > 256) {\n                return error.InvalidTagSelector;\n            }\n            // Try to match as a known tag enum for optimization\n            const lower = std.ascii.lowerString(&page.buf, tag_name);\n            if (Node.Element.Tag.parseForMatch(lower)) |known_tag| {\n                break :blk .{ .tag = known_tag };\n            }\n            // Store lowercased for fast comparison\n            const lower_tag = try arena.dupe(u8, lower);\n            break :blk .{ .tag_name = lower_tag };\n        },\n        else => error.InvalidSelector,\n    };\n}\n\nfn isStartOfPart(c: u8) bool {\n    return switch (c) {\n        '#', '.', '*', '[', ':', 'a'...'z', 'A'...'Z', '_' => true,\n        else => false,\n    };\n}\n\n// Returns true if there's more input after trimming whitespace\nfn skipSpaces(self: *Parser) bool {\n    const trimmed = std.mem.trimLeft(u8, self.input, &std.ascii.whitespace);\n    self.input = trimmed;\n    return trimmed.len > 0;\n}\n\n// Returns true if whitespace was actually removed\nfn skipSpacesConsumed(self: *Parser) bool {\n    const original_len = self.input.len;\n    const trimmed = std.mem.trimLeft(u8, self.input, &std.ascii.whitespace);\n    self.input = trimmed;\n    return trimmed.len < original_len;\n}\n\nfn peek(self: *const Parser) u8 {\n    const input = self.input;\n    if (input.len == 0) {\n        return 0;\n    }\n    return input[0];\n}\n\nfn consumeUntilCommaOrParen(self: *Parser) []const u8 {\n    const input = self.input;\n    var depth: usize = 0;\n    var i: usize = 0;\n\n    while (i < input.len) : (i += 1) {\n        const c = input[i];\n        switch (c) {\n            '(' => depth += 1,\n            ')' => {\n                if (depth == 0) break;\n                depth -= 1;\n            },\n            ',' => {\n                if (depth == 0) break;\n            },\n            else => {},\n        }\n    }\n\n    const result = input[0..i];\n    self.input = input[i..];\n    return result;\n}\n\nfn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoClass {\n    if (comptime IS_DEBUG) {\n        // Should have been verified by caller\n        std.debug.assert(self.peek() == ':');\n    }\n\n    self.input = self.input[1..];\n\n    // Parse the pseudo-class name\n    const start = self.input;\n    var i: usize = 0;\n    while (i < start.len) : (i += 1) {\n        const c = start[i];\n        if (!std.ascii.isAlphanumeric(c) and c != '-') {\n            break;\n        }\n    }\n\n    if (i == 0) {\n        return error.InvalidPseudoClass;\n    }\n\n    const name = start[0..i];\n    self.input = start[i..];\n\n    const next = self.peek();\n\n    // Check for functional pseudo-classes like :nth-child(2n+1) or :not(...)\n    if (next == '(') {\n        self.input = self.input[1..]; // Skip '('\n\n        if (std.mem.eql(u8, name, \"nth-child\")) {\n            const pattern = try self.parseNthPattern();\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n            return .{ .nth_child = pattern };\n        }\n\n        if (std.mem.eql(u8, name, \"nth-last-child\")) {\n            const pattern = try self.parseNthPattern();\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n            return .{ .nth_last_child = pattern };\n        }\n\n        if (std.mem.eql(u8, name, \"nth-of-type\")) {\n            const pattern = try self.parseNthPattern();\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n            return .{ .nth_of_type = pattern };\n        }\n\n        if (std.mem.eql(u8, name, \"nth-last-of-type\")) {\n            const pattern = try self.parseNthPattern();\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n            return .{ .nth_last_of_type = pattern };\n        }\n\n        if (std.mem.eql(u8, name, \"not\")) {\n            // CSS Level 4: :not() can contain a full selector list (comma-separated selectors)\n            // e.g., :not(div, .class, #id > span)\n            var selectors: std.ArrayList(Selector.Selector) = .empty;\n\n            _ = self.skipSpaces();\n\n            // Parse comma-separated selectors\n            while (true) {\n                if (self.peek() == ')') break;\n                if (self.peek() == 0) return error.InvalidPseudoClass;\n\n                // Parse a full selector (with potential combinators and compounds)\n                const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);\n                try selectors.append(arena, selector);\n\n                _ = self.skipSpaces();\n                if (self.peek() == ',') {\n                    self.input = self.input[1..]; // Skip comma\n                    _ = self.skipSpaces();\n                    continue;\n                }\n                break;\n            }\n\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..]; // Skip ')'\n\n            if (selectors.items.len == 0) return error.InvalidPseudoClass;\n            return .{ .not = selectors.items };\n        }\n\n        if (std.mem.eql(u8, name, \"is\")) {\n            var selectors: std.ArrayList(Selector.Selector) = .empty;\n\n            _ = self.skipSpaces();\n            while (true) {\n                if (self.peek() == ')') break;\n                if (self.peek() == 0) return error.InvalidPseudoClass;\n\n                const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);\n                try selectors.append(arena, selector);\n\n                _ = self.skipSpaces();\n                if (self.peek() == ',') {\n                    self.input = self.input[1..];\n                    _ = self.skipSpaces();\n                    continue;\n                }\n                break;\n            }\n\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n\n            // Empty :is() is valid per spec - matches nothing\n            return .{ .is = selectors.items };\n        }\n\n        if (std.mem.eql(u8, name, \"where\")) {\n            var selectors: std.ArrayList(Selector.Selector) = .empty;\n\n            _ = self.skipSpaces();\n            while (true) {\n                if (self.peek() == ')') break;\n                if (self.peek() == 0) return error.InvalidPseudoClass;\n\n                const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);\n                try selectors.append(arena, selector);\n\n                _ = self.skipSpaces();\n                if (self.peek() == ',') {\n                    self.input = self.input[1..];\n                    _ = self.skipSpaces();\n                    continue;\n                }\n                break;\n            }\n\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n\n            // Empty :where() is valid per spec - matches nothing\n            return .{ .where = selectors.items };\n        }\n\n        if (std.mem.eql(u8, name, \"has\")) {\n            var selectors: std.ArrayList(Selector.Selector) = .empty;\n\n            _ = self.skipSpaces();\n            while (true) {\n                if (self.peek() == ')') break;\n                if (self.peek() == 0) return error.InvalidPseudoClass;\n\n                const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);\n                try selectors.append(arena, selector);\n\n                _ = self.skipSpaces();\n                if (self.peek() == ',') {\n                    self.input = self.input[1..];\n                    _ = self.skipSpaces();\n                    continue;\n                }\n                break;\n            }\n\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n\n            if (selectors.items.len == 0) return error.InvalidPseudoClass;\n            return .{ .has = selectors.items };\n        }\n\n        if (std.mem.eql(u8, name, \"lang\")) {\n            _ = self.skipSpaces();\n            const lang_start = self.input;\n            var lang_i: usize = 0;\n            while (lang_i < lang_start.len and lang_start[lang_i] != ')') : (lang_i += 1) {}\n            if (lang_i == 0 or self.peek() == 0) return error.InvalidPseudoClass;\n\n            const lang = try arena.dupe(u8, std.mem.trim(u8, lang_start[0..lang_i], &std.ascii.whitespace));\n            self.input = lang_start[lang_i..];\n\n            if (self.peek() != ')') return error.InvalidPseudoClass;\n            self.input = self.input[1..];\n\n            return .{ .lang = lang };\n        }\n\n        return error.UnknownPseudoClass;\n    }\n\n    switch (name.len) {\n        4 => {\n            if (fastEql(name, \"root\")) return .root;\n            if (fastEql(name, \"link\")) return .link;\n        },\n        5 => {\n            if (fastEql(name, \"modal\")) return .modal;\n            if (fastEql(name, \"hover\")) return .hover;\n            if (fastEql(name, \"focus\")) return .focus;\n            if (fastEql(name, \"scope\")) return .scope;\n            if (fastEql(name, \"empty\")) return .empty;\n            if (fastEql(name, \"valid\")) return .valid;\n        },\n        6 => {\n            if (fastEql(name, \"active\")) return .active;\n            if (fastEql(name, \"target\")) return .target;\n        },\n        7 => {\n            if (fastEql(name, \"checked\")) return .checked;\n            if (fastEql(name, \"visited\")) return .visited;\n            if (fastEql(name, \"enabled\")) return .enabled;\n            if (fastEql(name, \"invalid\")) return .invalid;\n            if (fastEql(name, \"default\")) return .default;\n            if (fastEql(name, \"defined\")) return .defined;\n        },\n        8 => {\n            if (fastEql(name, \"disabled\")) return .disabled;\n            if (fastEql(name, \"required\")) return .required;\n            if (fastEql(name, \"optional\")) return .optional;\n            if (fastEql(name, \"any-link\")) return .any_link;\n            if (fastEql(name, \"in-range\")) return .in_range;\n        },\n        9 => {\n            if (fastEql(name, \"read-only\")) return .read_only;\n        },\n        10 => {\n            if (fastEql(name, \"only-child\")) return .only_child;\n            if (fastEql(name, \"last-child\")) return .last_child;\n            if (fastEql(name, \"read-write\")) return .read_write;\n        },\n        11 => {\n            if (fastEql(name, \"first-child\")) return .first_child;\n        },\n        12 => {\n            if (fastEql(name, \"only-of-type\")) return .only_of_type;\n            if (fastEql(name, \"last-of-type\")) return .last_of_type;\n            if (fastEql(name, \"focus-within\")) return .focus_within;\n            if (fastEql(name, \"out-of-range\")) return .out_of_range;\n        },\n        13 => {\n            if (fastEql(name, \"first-of-type\")) return .first_of_type;\n            if (fastEql(name, \"focus-visible\")) return .focus_visible;\n            if (fastEql(name, \"indeterminate\")) return .indeterminate;\n        },\n        17 => {\n            if (fastEql(name, \"placeholder-shown\")) return .placeholder_shown;\n        },\n        else => {},\n    }\n\n    return error.UnknownPseudoClass;\n}\n\nfn parseNthPattern(self: *Parser) !Selector.NthPattern {\n    _ = self.skipSpaces();\n\n    const start = self.input;\n\n    // Check for special keywords\n    if (std.mem.startsWith(u8, start, \"odd\")) {\n        self.input = start[3..];\n        return .{ .a = 2, .b = 1 };\n    }\n\n    if (std.mem.startsWith(u8, start, \"even\")) {\n        self.input = start[4..];\n        return .{ .a = 2, .b = 0 };\n    }\n\n    // Parse An+B notation\n    var a: i32 = 0;\n    var b: i32 = 0;\n    var has_n = false;\n\n    // Try to parse coefficient 'a'\n    var p = self.peek();\n    const sign_a: i32 = if (p == '-') blk: {\n        self.input = self.input[1..];\n        break :blk -1;\n    } else if (p == '+') blk: {\n        self.input = self.input[1..];\n        break :blk 1;\n    } else 1;\n\n    p = self.peek();\n    if (p == 'n' or p == 'N') {\n        // Just 'n' means a=1\n        a = sign_a;\n        has_n = true;\n        self.input = self.input[1..];\n    } else {\n        // Parse numeric coefficient\n        var num: i32 = 0;\n        var digit_count: usize = 0;\n        p = self.peek();\n        while (std.ascii.isDigit(p)) {\n            num = num * 10 + @as(i32, p - '0');\n            self.input = self.input[1..];\n            digit_count += 1;\n            p = self.peek();\n        }\n\n        if (digit_count > 0) {\n            p = self.peek();\n            if (p == 'n' or p == 'N') {\n                a = sign_a * num;\n                has_n = true;\n                self.input = self.input[1..];\n            } else {\n                // Just a number, no 'n', so this is 'b'\n                b = sign_a * num;\n                return .{ .a = 0, .b = b };\n            }\n        } else if (sign_a != 1) {\n            // We had a sign but no number and no 'n'\n            return error.InvalidNthPattern;\n        }\n    }\n\n    if (!has_n) {\n        return error.InvalidNthPattern;\n    }\n\n    // Parse offset 'b'\n    _ = self.skipSpaces();\n    p = self.peek();\n    if (p == '+' or p == '-') {\n        const sign_b: i32 = if (p == '-') -1 else 1;\n        self.input = self.input[1..];\n        _ = self.skipSpaces();\n\n        var num: i32 = 0;\n        var digit_count: usize = 0;\n        p = self.peek();\n        while (std.ascii.isDigit(p)) {\n            num = num * 10 + @as(i32, p - '0');\n            self.input = self.input[1..];\n            digit_count += 1;\n            p = self.peek();\n        }\n\n        if (digit_count == 0) {\n            return error.InvalidNthPattern;\n        }\n\n        b = sign_b * num;\n    }\n\n    return .{ .a = a, .b = b };\n}\n\npub fn id(self: *Parser, arena: Allocator) ![]const u8 {\n    if (comptime IS_DEBUG) {\n        // should have been verified by caller\n        std.debug.assert(self.peek() == '#');\n    }\n\n    self.input = self.input[1..]; // Skip '#'\n    return self.parseIdentifier(arena, error.InvalidIDSelector);\n}\n\nfn class(self: *Parser, arena: Allocator) ![]const u8 {\n    if (comptime IS_DEBUG) {\n        // should have been verified by caller\n        std.debug.assert(self.peek() == '.');\n    }\n\n    self.input = self.input[1..]; // Skip '.'\n    return self.parseIdentifier(arena, error.InvalidClassSelector);\n}\n\n// Parse a CSS identifier (used by id and class selectors)\nfn parseIdentifier(self: *Parser, arena: Allocator, err: ParseError) ParseError![]const u8 {\n    const input = self.input;\n\n    if (input.len == 0) {\n        @branchHint(.cold);\n        return err;\n    }\n\n    var i: usize = 0;\n    const first = input[0];\n\n    if (first == '\\\\' or first == 0) {\n        // First char needs special processing - go straight to slow path\n    } else if (first >= 0x80 or std.ascii.isAlphabetic(first) or first == '_') {\n        // Valid first char\n        i = 1;\n    } else if (first == '-') {\n        // Dash must be followed by dash, letter, underscore, escape, or non-ASCII\n        if (input.len < 2) {\n            @branchHint(.cold);\n            return err;\n        }\n        const second = input[1];\n        if (second == '-' or second == '\\\\' or std.ascii.isAlphabetic(second) or second == '_' or second >= 0x80) {\n            i = 1; // First char validated, start scanning from position 1\n        } else {\n            @branchHint(.cold);\n            return err;\n        }\n    } else {\n        @branchHint(.cold);\n        return err;\n    }\n\n    // Fast scan remaining characters (no escapes/nulls)\n    while (i < input.len) {\n        const b = input[i];\n\n        if (b == '\\\\' or b == 0) {\n            // Stop at escape or null - need slow path\n            break;\n        }\n\n        // Check if valid identifier character\n        switch (b) {\n            'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},\n            0x80...0xFF => {},\n            ' ', '\\t', '\\n', '\\r', '.', '#', '>', '+', '~', '[', ':', ')', ']' => break,\n            else => {\n                @branchHint(.cold);\n                return err;\n            },\n        }\n        i += 1;\n    }\n\n    // Fast path: no escapes/nulls found\n    if (i == input.len or (i > 0 and input[i] != '\\\\' and input[i] != 0)) {\n        if (i == 0) {\n            @branchHint(.cold);\n            return err;\n        }\n        self.input = input[i..];\n        return input[0..i];\n    }\n\n    // Slow path: has escapes or nulls\n    var result = try std.ArrayList(u8).initCapacity(arena, input.len);\n\n    try result.appendSlice(arena, input[0..i]);\n\n    var j = i;\n    while (j < input.len) {\n        const b = input[j];\n\n        if (b == '\\\\') {\n            j += 1;\n            const escape_result = try parseEscape(input[j..], arena);\n            try result.appendSlice(arena, escape_result.bytes);\n            j += escape_result.consumed;\n            continue;\n        }\n\n        if (b == 0) {\n            try result.appendSlice(arena, \"\\u{FFFD}\");\n            j += 1;\n            continue;\n        }\n\n        const is_ident_char = switch (b) {\n            'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => true,\n            0x80...0xFF => true,\n            else => false,\n        };\n\n        if (!is_ident_char) {\n            break;\n        }\n        try result.append(arena, b);\n        j += 1;\n    }\n\n    if (result.items.len == 0) {\n        @branchHint(.cold);\n        return err;\n    }\n\n    self.input = input[j..];\n    return result.items;\n}\n\nfn tag(self: *Parser) ![]const u8 {\n    var input = self.input;\n\n    // First character: must be letter, underscore, or non-ASCII (>= 0x80)\n    // Can also be hyphen if not followed by digit or another hyphen\n    const first = input[0];\n    if (first == '-') {\n        if (input.len < 2) {\n            @branchHint(.cold);\n            return error.InvalidTagSelector;\n        }\n        const second = input[1];\n        if (second == '-' or std.ascii.isDigit(second)) {\n            @branchHint(.cold);\n            return error.InvalidTagSelector;\n        }\n    } else if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {\n        @branchHint(.cold);\n        return error.InvalidTagSelector;\n    }\n\n    var i: usize = 1;\n    for (input[1..]) |b| {\n        switch (b) {\n            'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},\n            0x80...0xFF => {}, // non-ASCII characters\n            ' ', '\\t', '\\n', '\\r' => break,\n            // Stop at selector delimiters\n            '.', '#', '>', '+', '~', '[', ':', ')', ']' => break,\n            else => {\n                @branchHint(.cold);\n                return error.InvalidTagSelector;\n            },\n        }\n        i += 1;\n    }\n\n    self.input = input[i..];\n    return input[0..i];\n}\n\nfn attribute(self: *Parser, arena: Allocator, page: *Page) !Selector.Attribute {\n    if (comptime IS_DEBUG) {\n        // should have been verified by caller\n        std.debug.assert(self.peek() == '[');\n    }\n\n    self.input = self.input[1..];\n    _ = self.skipSpaces();\n\n    const attr_name = try self.attributeName();\n\n    // Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup)\n    const normalized = try Attribute.normalizeNameForLookup(.wrap(attr_name), page);\n    const name = try normalized.dupe(arena);\n    var case_insensitive = false;\n    _ = self.skipSpaces();\n\n    if (self.peek() == ']') {\n        self.input = self.input[1..];\n        return .{ .name = name, .matcher = .presence, .case_insensitive = case_insensitive };\n    }\n\n    const matcher_type = try self.attributeMatcher();\n    _ = self.skipSpaces();\n\n    const value_raw = try self.attributeValue();\n    const value = try arena.dupe(u8, value_raw);\n    _ = self.skipSpaces();\n\n    // Parse optional case-sensitivity flag\n    if (std.ascii.toLower(self.peek()) == 'i') {\n        self.input = self.input[1..];\n        case_insensitive = true;\n        _ = self.skipSpaces();\n    } else if (std.ascii.toLower(self.peek()) == 's') {\n        // 's' flag means case-sensitive (explicit)\n        self.input = self.input[1..];\n        case_insensitive = false;\n        _ = self.skipSpaces();\n    }\n\n    if (self.peek() != ']') {\n        return error.InvalidAttributeSelector;\n    }\n    self.input = self.input[1..];\n\n    const matcher: Selector.AttributeMatcher = switch (matcher_type) {\n        .exact => .{ .exact = value },\n        .word => .{ .word = value },\n        .prefix_dash => .{ .prefix_dash = value },\n        .starts_with => .{ .starts_with = value },\n        .ends_with => .{ .ends_with = value },\n        .substring => .{ .substring = value },\n        .presence => unreachable,\n    };\n\n    return .{ .name = name, .matcher = matcher, .case_insensitive = case_insensitive };\n}\n\nfn attributeName(self: *Parser) ![]const u8 {\n    const input = self.input;\n    if (input.len == 0) {\n        return error.InvalidAttributeSelector;\n    }\n\n    const first = input[0];\n    if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {\n        return error.InvalidAttributeSelector;\n    }\n\n    var i: usize = 1;\n    for (input[1..]) |b| {\n        switch (b) {\n            'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},\n            0x80...0xFF => {},\n            else => break,\n        }\n        i += 1;\n    }\n\n    self.input = input[i..];\n    return input[0..i];\n}\n\nfn attributeMatcher(self: *Parser) !std.meta.FieldEnum(Selector.AttributeMatcher) {\n    const input = self.input;\n    if (input.len < 2) {\n        return error.InvalidAttributeSelector;\n    }\n\n    if (input[0] == '=') {\n        self.input = input[1..];\n        return .exact;\n    }\n\n    self.input = input[2..];\n    return switch (@as(u16, @bitCast(input[0..2].*))) {\n        asUint(\"~=\") => .word,\n        asUint(\"|=\") => .prefix_dash,\n        asUint(\"^=\") => .starts_with,\n        asUint(\"$=\") => .ends_with,\n        asUint(\"*=\") => .substring,\n        else => return error.InvalidAttributeSelector,\n    };\n}\n\nfn attributeValue(self: *Parser) ![]const u8 {\n    const input = self.input;\n    if (input.len == 0) {\n        return error.InvalidAttributeSelector;\n    }\n\n    const quote = input[0];\n    if (quote == '\"' or quote == '\\'') {\n        const end = std.mem.indexOfScalarPos(u8, input, 1, quote) orelse return error.InvalidAttributeSelector;\n        const value = input[1..end];\n        self.input = input[end + 1 ..];\n        return value;\n    }\n\n    var i: usize = 0;\n    for (input) |b| {\n        switch (b) {\n            'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},\n            0x80...0xFF => {},\n            else => break,\n        }\n        i += 1;\n    }\n\n    if (i == 0) {\n        return error.InvalidAttributeSelector;\n    }\n\n    const value = input[0..i];\n    self.input = input[i..];\n    return value;\n}\n\nfn asUint(comptime string: anytype) std.meta.Int(\n    .unsigned,\n    @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0\n) {\n    const byteLength = @sizeOf(@TypeOf(string.*)) - 1;\n    const expectedType = *const [byteLength:0]u8;\n    if (@TypeOf(string) != expectedType) {\n        @compileError(\"expected : \" ++ @typeName(expectedType) ++ \", got: \" ++ @typeName(@TypeOf(string)));\n    }\n\n    return @bitCast(@as(*const [byteLength]u8, string).*);\n}\n\nfn fastEql(a: []const u8, comptime b: []const u8) bool {\n    for (a, b) |a_byte, b_byte| {\n        if (a_byte != b_byte) return false;\n    }\n    return true;\n}\n\nconst EscapeResult = struct {\n    bytes: []const u8,\n    consumed: usize, // how many bytes from input were consumed\n};\n\n// Parse CSS escape sequence starting after the backslash\n// Input should point to the character after '\\'\n// Returns the UTF-8 bytes for the escaped character and how many input bytes were consumed\nfn parseEscape(input: []const u8, arena: Allocator) !EscapeResult {\n    if (input.len == 0) {\n        // EOF after backslash -> replacement character\n        return .{ .bytes = \"\\u{FFFD}\", .consumed = 0 };\n    }\n\n    const first = input[0];\n\n    // Check if it's a hex escape (1-6 hex digits)\n    if (std.ascii.isHex(first)) {\n        var hex_value: u32 = 0;\n        var i: usize = 0;\n\n        // Parse up to 6 hex digits\n        while (i < 6 and i < input.len) : (i += 1) {\n            const c = input[i];\n            if (!std.ascii.isHex(c)) break;\n\n            const digit = if (c >= '0' and c <= '9')\n                c - '0'\n            else if (c >= 'a' and c <= 'f')\n                c - 'a' + 10\n            else if (c >= 'A' and c <= 'F')\n                c - 'A' + 10\n            else\n                unreachable;\n\n            hex_value = hex_value * 16 + digit;\n        }\n\n        var consumed = i;\n\n        // Consume one optional whitespace character (space, tab, CR, LF, FF)\n        if (i < input.len) {\n            const next = input[i];\n            if (next == ' ' or next == '\\t' or next == '\\r' or next == '\\n' or next == '\\x0C') {\n                consumed += 1;\n            }\n        }\n\n        // Validate the code point and convert to UTF-8\n        // Invalid: 0, > 0x10FFFF, or surrogate range 0xD800-0xDFFF\n        if (hex_value == 0 or hex_value > 0x10FFFF or (hex_value >= 0xD800 and hex_value <= 0xDFFF)) {\n            return .{ .bytes = \"\\u{FFFD}\", .consumed = consumed };\n        }\n\n        // Encode as UTF-8\n        var buf = try arena.alloc(u8, 4);\n        const len = std.unicode.utf8Encode(@intCast(hex_value), buf) catch {\n            return .{ .bytes = \"\\u{FFFD}\", .consumed = consumed };\n        };\n        return .{ .bytes = buf[0..len], .consumed = consumed };\n    }\n\n    // Simple escape - just the character itself\n    var buf = try arena.alloc(u8, 1);\n    buf[0] = first;\n    return .{ .bytes = buf, .consumed = 1 };\n}\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"Selector: Parser.ID\" {\n    const arena = testing.allocator;\n\n    {\n        var parser = Parser{ .input = \"#\" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"# \" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"#1\" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"#9abc\" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"#-1\" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"#-5abc\" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"#--\" };\n        try testing.expectEqual(\"--\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#--test\" };\n        try testing.expectEqual(\"--test\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#-\" };\n        try testing.expectError(error.InvalidIDSelector, parser.id(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \"#over\" };\n        try testing.expectEqual(\"over\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#myID123\" };\n        try testing.expectEqual(\"myID123\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#_test\" };\n        try testing.expectEqual(\"_test\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#test_123\" };\n        try testing.expectEqual(\"test_123\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#-test\" };\n        try testing.expectEqual(\"-test\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#my-id\" };\n        try testing.expectEqual(\"my-id\", try parser.id(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#test other\" };\n        try testing.expectEqual(\"test\", try parser.id(arena));\n        try testing.expectEqual(\" other\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#id.class\" };\n        try testing.expectEqual(\"id\", try parser.id(arena));\n        try testing.expectEqual(\".class\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#id:hover\" };\n        try testing.expectEqual(\"id\", try parser.id(arena));\n        try testing.expectEqual(\":hover\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#id>child\" };\n        try testing.expectEqual(\"id\", try parser.id(arena));\n        try testing.expectEqual(\">child\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"#id[attr]\" };\n        try testing.expectEqual(\"id\", try parser.id(arena));\n        try testing.expectEqual(\"[attr]\", parser.input);\n    }\n}\n\ntest \"Selector: Parser.class\" {\n    const arena = testing.allocator;\n\n    {\n        var parser = Parser{ .input = \".\" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \". \" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \".1\" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \".9abc\" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \".-1\" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \".-5abc\" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \".--\" };\n        try testing.expectEqual(\"--\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".--test\" };\n        try testing.expectEqual(\"--test\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".-\" };\n        try testing.expectError(error.InvalidClassSelector, parser.class(arena));\n    }\n\n    {\n        var parser = Parser{ .input = \".active\" };\n        try testing.expectEqual(\"active\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".myClass123\" };\n        try testing.expectEqual(\"myClass123\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"._test\" };\n        try testing.expectEqual(\"_test\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".test_123\" };\n        try testing.expectEqual(\"test_123\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".-test\" };\n        try testing.expectEqual(\"-test\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".my-class\" };\n        try testing.expectEqual(\"my-class\", try parser.class(arena));\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".test other\" };\n        try testing.expectEqual(\"test\", try parser.class(arena));\n        try testing.expectEqual(\" other\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".class1.class2\" };\n        try testing.expectEqual(\"class1\", try parser.class(arena));\n        try testing.expectEqual(\".class2\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".class:hover\" };\n        try testing.expectEqual(\"class\", try parser.class(arena));\n        try testing.expectEqual(\":hover\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".class>child\" };\n        try testing.expectEqual(\"class\", try parser.class(arena));\n        try testing.expectEqual(\">child\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \".class[attr]\" };\n        try testing.expectEqual(\"class\", try parser.class(arena));\n        try testing.expectEqual(\"[attr]\", parser.input);\n    }\n}\n\ntest \"Selector: Parser.tag\" {\n    {\n        var parser = Parser{ .input = \"1\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"9abc\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"-1\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"-5abc\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"--\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"--test\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"-\" };\n        try testing.expectError(error.InvalidTagSelector, parser.tag());\n    }\n\n    {\n        var parser = Parser{ .input = \"div\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"p\" };\n        try testing.expectEqual(\"p\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"MyCustomElement\" };\n        try testing.expectEqual(\"MyCustomElement\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"_test\" };\n        try testing.expectEqual(\"_test\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"test_123\" };\n        try testing.expectEqual(\"test_123\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"-test\" };\n        try testing.expectEqual(\"-test\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"my-element\" };\n        try testing.expectEqual(\"my-element\", try parser.tag());\n        try testing.expectEqual(\"\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"div other\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\" other\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"div.class\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\".class\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"div#id\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\"#id\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"div:hover\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\":hover\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"div>child\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\">child\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"div[attr]\" };\n        try testing.expectEqual(\"div\", try parser.tag());\n        try testing.expectEqual(\"[attr]\", parser.input);\n    }\n}\n\ntest \"Selector: Parser.parseNthPattern\" {\n    {\n        var parser = Parser{ .input = \"odd)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(2, pattern.a);\n        try testing.expectEqual(1, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"even)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(2, pattern.a);\n        try testing.expectEqual(0, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"3)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(0, pattern.a);\n        try testing.expectEqual(3, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"2n)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(2, pattern.a);\n        try testing.expectEqual(0, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"2n+1)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(2, pattern.a);\n        try testing.expectEqual(1, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"3n-2)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(3, pattern.a);\n        try testing.expectEqual(-2, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"n)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(1, pattern.a);\n        try testing.expectEqual(0, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"-n)\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(-1, pattern.a);\n        try testing.expectEqual(0, pattern.b);\n        try testing.expectEqual(\")\", parser.input);\n    }\n\n    {\n        var parser = Parser{ .input = \"  2n + 1  )\" };\n        const pattern = try parser.parseNthPattern();\n        try testing.expectEqual(2, pattern.a);\n        try testing.expectEqual(1, pattern.b);\n        try testing.expectEqual(\"  )\", parser.input);\n    }\n}\n"
  },
  {
    "path": "src/browser/webapi/selector/Selector.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst String = @import(\"../../../string.zig\").String;\n\nconst Parser = @import(\"Parser.zig\");\nconst Node = @import(\"../Node.zig\");\nconst Page = @import(\"../../Page.zig\");\npub const List = @import(\"List.zig\");\n\npub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Element {\n    if (input.len == 0) {\n        return error.SyntaxError;\n    }\n\n    const arena = page.call_arena;\n    const selectors = try Parser.parseList(arena, input, page);\n\n    for (selectors) |selector| {\n        // Fast path: single compound with only an ID selector\n        if (selector.segments.len == 0 and selector.first.parts.len == 1) {\n            const first = selector.first.parts[0];\n            if (first == .id) {\n                const el = page.getElementByIdFromNode(root, first.id) orelse continue;\n                // Check if the element is within the root subtree\n                const node = el.asNode();\n                if (node != root and root.contains(node)) {\n                    return el;\n                }\n                continue;\n            }\n        }\n\n        if (List.initOne(root, selector, page)) |node| {\n            if (node.is(Node.Element)) |el| {\n                return el;\n            }\n        }\n    }\n    return null;\n}\n\npub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List {\n    if (input.len == 0) {\n        return error.SyntaxError;\n    }\n\n    const arena = try page.getArena(.{ .debug = \"querySelectorAll\" });\n    errdefer page.releaseArena(arena);\n\n    var nodes: std.AutoArrayHashMapUnmanaged(*Node, void) = .empty;\n\n    const selectors = try Parser.parseList(arena, input, page);\n    for (selectors) |selector| {\n        try List.collect(arena, root, selector, &nodes, page);\n    }\n\n    const list = try arena.create(List);\n    list.* = .{\n        ._arena = arena,\n        ._nodes = nodes.keys(),\n    };\n    return list;\n}\n\npub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {\n    if (input.len == 0) {\n        return error.SyntaxError;\n    }\n\n    const arena = page.call_arena;\n    const selectors = try Parser.parseList(arena, input, page);\n\n    for (selectors) |selector| {\n        if (List.matches(el.asNode(), selector, el.asNode(), page)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n// Like matches, but allows the caller to specify a scope node distinct from el.\n// Used by closest() so that :scope always refers to the original context element.\npub fn matchesWithScope(el: *Node.Element, input: []const u8, scope: *Node.Element, page: *Page) !bool {\n    if (input.len == 0) {\n        return error.SyntaxError;\n    }\n\n    const arena = page.call_arena;\n    const selectors = try Parser.parseList(arena, input, page);\n\n    for (selectors) |selector| {\n        if (List.matches(el.asNode(), selector, scope.asNode(), page)) {\n            return true;\n        }\n    }\n    return false;\n}\n\npub fn classAttributeContains(class_attr: []const u8, class_name: []const u8) bool {\n    if (class_name.len == 0 or class_name.len > class_attr.len) return false;\n\n    var search = class_attr;\n    while (std.mem.indexOf(u8, search, class_name)) |pos| {\n        const is_start = pos == 0 or search[pos - 1] == ' ';\n        const end = pos + class_name.len;\n        const is_end = end == search.len or search[end] == ' ';\n\n        if (is_start and is_end) return true;\n\n        search = search[pos + 1 ..];\n    }\n    return false;\n}\n\npub const Part = union(enum) {\n    id: []const u8,\n    class: []const u8,\n    tag: Node.Element.Tag, // optimized, for known tags\n    tag_name: []const u8, // fallback for custom/unknown tags\n    universal, // '*' any element\n    pseudo_class: PseudoClass,\n    attribute: Attribute,\n};\n\npub const Attribute = struct {\n    name: String,\n    matcher: AttributeMatcher,\n    case_insensitive: bool,\n};\n\npub const AttributeMatcher = union(enum) {\n    presence,\n    exact: []const u8,\n    word: []const u8,\n    prefix_dash: []const u8,\n    starts_with: []const u8,\n    ends_with: []const u8,\n    substring: []const u8,\n};\n\npub const PseudoClass = union(enum) {\n    // State pseudo-classes\n    modal,\n    checked,\n    disabled,\n    enabled,\n    indeterminate,\n\n    // Form validation\n    valid,\n    invalid,\n    required,\n    optional,\n    in_range,\n    out_of_range,\n    placeholder_shown,\n    read_only,\n    read_write,\n    default,\n\n    // User interaction\n    hover,\n    active,\n    focus,\n    focus_within,\n    focus_visible,\n\n    // Link states\n    link,\n    visited,\n    any_link,\n    target,\n\n    // Tree structural\n    root,\n    scope,\n    empty,\n    first_child,\n    last_child,\n    only_child,\n    first_of_type,\n    last_of_type,\n    only_of_type,\n    nth_child: NthPattern,\n    nth_last_child: NthPattern,\n    nth_of_type: NthPattern,\n    nth_last_of_type: NthPattern,\n\n    // Custom elements\n    defined,\n\n    // Functional\n    lang: []const u8,\n    not: []const Selector, // :not() - CSS Level 4: supports full selectors and comma-separated lists\n    is: []const Selector, // :is() - matches any of the selectors\n    where: []const Selector, // :where() - like :is() but with zero specificity\n    has: []const Selector, // :has() - element containing descendants matching selector\n};\n\npub const NthPattern = struct {\n    a: i32, // coefficient (e.g., 2 in \"2n+1\")\n    b: i32, // offset (e.g., 1 in \"2n+1\")\n\n    // Common patterns:\n    // odd: a=2, b=1\n    // even: a=2, b=0\n    // 3n+1: a=3, b=1\n    // 5: a=0, b=5\n};\n\n// Combinator represents the relationship between two compound selectors\npub const Combinator = enum {\n    descendant, // ' ' - any descendant\n    child, // '>' - direct child\n    next_sibling, // '+' - immediately following sibling\n    subsequent_sibling, // '~' - any following sibling\n};\n\n// A compound selector is multiple parts that all match the same element\n//   \"div.class#id\" -> [tag(div), class(\"class\"), id(\"id\")]\npub const Compound = struct {\n    parts: []const Part,\n\n    pub fn format(self: Compound, writer: *std.Io.Writer) !void {\n        for (self.parts) |part| switch (part) {\n            .id => |val| {\n                try writer.writeByte('#');\n                try writer.writeAll(val);\n            },\n            .class => |val| {\n                try writer.writeByte('.');\n                try writer.writeAll(val);\n            },\n            .tag => |val| try writer.writeAll(@tagName(val)),\n            .tag_name => |val| try writer.writeAll(val),\n            .universal => try writer.writeByte('*'),\n            .pseudo_class => |val| {\n                try writer.writeByte(':');\n                try writer.writeAll(@tagName(val));\n            },\n            .attribute => {\n                try writer.writeAll(\"TODO\");\n            },\n        };\n    }\n};\n\n// A segment represents a compound selector with the combinator that precedes it\npub const Segment = struct {\n    compound: Compound,\n    combinator: Combinator,\n\n    pub fn format(self: Segment, writer: *std.Io.Writer) !void {\n        switch (self.combinator) {\n            .descendant => try writer.writeByte(' '),\n            .child => try writer.writeAll(\" > \"),\n            .next_sibling => try writer.writeAll(\" + \"),\n            .subsequent_sibling => try writer.writeAll(\" ~ \"),\n        }\n        return self.compound.format(writer);\n    }\n};\n\n// A full selector is the first compound plus subsequent segments\n//   \"div > p + span\" -> { first: [tag(div)], segments: [{child, [tag(p)]}, {next_sibling, [tag(span)]}] }\npub const Selector = struct {\n    first: Compound,\n    segments: []const Segment,\n\n    pub fn format(self: Selector, writer: *std.Io.Writer) !void {\n        try self.first.format(writer);\n        for (self.segments) |segment| {\n            try segment.format(writer);\n        }\n    }\n};\n"
  },
  {
    "path": "src/browser/webapi/storage/Cookie.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst URL = @import(\"../../URL.zig\");\nconst log = @import(\"../../../log.zig\");\nconst DateTime = @import(\"../../../datetime.zig\").DateTime;\nconst public_suffix_list = @import(\"../../../data/public_suffix_list.zig\").lookup;\n\nconst Cookie = @This();\n\nconst max_cookie_size = 4 * 1024;\nconst max_cookie_header_size = 8 * 1024;\nconst max_jar_size = 1024;\n\narena: ArenaAllocator,\nname: []const u8,\nvalue: []const u8,\ndomain: []const u8,\npath: []const u8,\nexpires: ?f64,\nsecure: bool = false,\nhttp_only: bool = false,\nsame_site: SameSite = .none,\n\nconst SameSite = enum {\n    strict,\n    lax,\n    none,\n};\n\npub fn deinit(self: *const Cookie) void {\n    self.arena.deinit();\n}\n\n// There's https://datatracker.ietf.org/doc/html/rfc6265 but browsers are\n// far less strict. I only found 2 cases where browsers will reject a cookie:\n//   - a byte 0...31 and 127...255 anywhere in the cookie (the HTTP header\n//     parser might take care of this already)\n//   - any shenanigans with the domain attribute - it has to be the current\n//     domain or one of higher order, excluding TLD.\n// Anything else, will turn into a cookie.\n// Single value? That's a cookie with an emtpy name and a value\n// Key or Values with characters the RFC says aren't allowed? Allowed! (\n//   (as long as the characters are 32...126)\n// Invalid attributes? Ignored.\n// Invalid attribute values? Ignore.\n// Duplicate attributes - use the last valid\n// Value-less attributes with a value? Ignore the value\npub fn parse(allocator: Allocator, url: [:0]const u8, str: []const u8) !Cookie {\n    if (str.len > max_cookie_header_size) {\n        return error.CookieHeaderSizeExceeded;\n    }\n\n    try validateCookieString(str);\n\n    const cookie_name, const cookie_value, const rest = parseNameValue(str) catch {\n        return error.InvalidNameValue;\n    };\n\n    var scrap: [8]u8 = undefined;\n\n    var path: ?[]const u8 = null;\n    var domain: ?[]const u8 = null;\n    var secure: ?bool = null;\n    var max_age: ?i64 = null;\n    var http_only: ?bool = null;\n    var expires: ?[]const u8 = null;\n    var same_site: ?Cookie.SameSite = null;\n\n    var it = std.mem.splitScalar(u8, rest, ';');\n    while (it.next()) |attribute| {\n        const sep = std.mem.indexOfScalarPos(u8, attribute, 0, '=') orelse attribute.len;\n        const key_string = trim(attribute[0..sep]);\n\n        if (key_string.len > scrap.len) {\n            // not valid, ignore\n            continue;\n        }\n\n        const key = std.meta.stringToEnum(enum {\n            path,\n            domain,\n            secure,\n            @\"max-age\",\n            expires,\n            httponly,\n            samesite,\n        }, std.ascii.lowerString(&scrap, key_string)) orelse continue;\n\n        const value = if (sep == attribute.len) \"\" else trim(attribute[sep + 1 ..]);\n        switch (key) {\n            .path => path = value,\n            .domain => domain = value,\n            .secure => secure = true,\n            .@\"max-age\" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,\n            .expires => expires = value,\n            .httponly => http_only = true,\n            .samesite => {\n                if (value.len > scrap.len) {\n                    continue;\n                }\n                same_site = std.meta.stringToEnum(Cookie.SameSite, std.ascii.lowerString(&scrap, value)) orelse continue;\n            },\n        }\n    }\n\n    if (same_site == .none and secure == null) {\n        return error.InsecureSameSite;\n    }\n\n    if (cookie_value.len > max_cookie_size) {\n        return error.CookieSizeExceeded;\n    }\n\n    var arena = ArenaAllocator.init(allocator);\n    errdefer arena.deinit();\n    const aa = arena.allocator();\n    const owned_name = try aa.dupe(u8, cookie_name);\n    const owned_value = try aa.dupe(u8, cookie_value);\n    const owned_path = try parsePath(aa, url, path);\n    const owned_domain = try parseDomain(aa, url, domain);\n\n    var normalized_expires: ?f64 = null;\n    if (max_age) |ma| {\n        normalized_expires = @floatFromInt(std.time.timestamp() + ma);\n    } else {\n        // max age takes priority over expires\n        if (expires) |expires_| {\n            var exp_dt = DateTime.parse(expires_, .rfc822) catch null;\n            if (exp_dt == null) {\n                if ((expires_.len > 11 and expires_[7] == '-' and expires_[11] == '-')) {\n                    // Replace dashes and try again\n                    const output = try aa.dupe(u8, expires_);\n                    output[7] = ' ';\n                    output[11] = ' ';\n                    exp_dt = DateTime.parse(output, .rfc822) catch null;\n                }\n            }\n            if (exp_dt) |dt| {\n                normalized_expires = @floatFromInt(dt.unix(.seconds));\n            } else {\n                // Algolia, for example, will call document.setCookie with\n                // an expired value which is literally 'Invalid Date'\n                // (it's trying to do something like: `new Date() + undefined`).\n                log.debug(.page, \"cookie expires date\", .{ .date = expires_ });\n            }\n        }\n    }\n\n    return .{\n        .arena = arena,\n        .name = owned_name,\n        .value = owned_value,\n        .path = owned_path,\n        .same_site = same_site orelse .lax,\n        .secure = secure orelse false,\n        .http_only = http_only orelse false,\n        .domain = owned_domain,\n        .expires = normalized_expires,\n    };\n}\n\nconst ValidateCookieError = error{ Empty, InvalidByteSequence };\n\n/// Returns an error if cookie str length is 0\n/// or contains characters outside of the ascii range 32...126.\nfn validateCookieString(str: []const u8) ValidateCookieError!void {\n    if (str.len == 0) {\n        return error.Empty;\n    }\n\n    const vec_size_suggestion = std.simd.suggestVectorLength(u8);\n    var offset: usize = 0;\n\n    // Fast path if possible.\n    if (comptime vec_size_suggestion) |size| {\n        while (str.len - offset >= size) : (offset += size) {\n            const Vec = @Vector(size, u8);\n            const space: Vec = @splat(32);\n            const tilde: Vec = @splat(126);\n            const chunk: Vec = str[offset..][0..size].*;\n\n            // This creates a mask where invalid characters represented\n            // as ones and valid characters as zeros. We then bitCast this\n            // into an unsigned integer. If the integer is not equal to 0,\n            // we know that we've invalid characters in this chunk.\n            // @popCount can also be used but using integers are simpler.\n            const mask = (@intFromBool(chunk < space) | @intFromBool(chunk > tilde));\n            const reduced: std.meta.Int(.unsigned, size) = @bitCast(mask);\n\n            // Got match.\n            if (reduced != 0) {\n                return error.InvalidByteSequence;\n            }\n        }\n\n        // Means str.len % size == 0; we also know str.len != 0.\n        // Cookie is valid.\n        if (offset == str.len) {\n            return;\n        }\n    }\n\n    // Either remaining slice or the original if fast path not taken.\n    const slice = str[offset..];\n    // Slow path.\n    const min, const max = std.mem.minMax(u8, slice);\n    if (min < 32 or max > 126) {\n        return error.InvalidByteSequence;\n    }\n}\n\npub fn parsePath(arena: Allocator, url_: ?[:0]const u8, explicit_path: ?[]const u8) ![]const u8 {\n    // path attribute value either begins with a '/' or we\n    // ignore it and use the \"default-path\" algorithm\n    if (explicit_path) |path| {\n        if (path.len > 0 and path[0] == '/') {\n            return try arena.dupe(u8, path);\n        }\n    }\n\n    // default-path\n    const url = url_ orelse return \"/\";\n    const url_path = URL.getPathname(url);\n    if (url_path.len == 0 or (url_path.len == 1 and url_path[0] == '/')) {\n        return \"/\";\n    }\n\n    var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);\n    const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {\n        return \"/\";\n    };\n    return try arena.dupe(u8, owned_path[0 .. last + 1]);\n}\n\npub fn parseDomain(arena: Allocator, url_: ?[:0]const u8, explicit_domain: ?[]const u8) ![]const u8 {\n    var encoded_host: ?[]const u8 = null;\n    if (url_) |url| {\n        const host = try percentEncode(arena, URL.getHostname(url), isHostChar);\n        _ = toLower(host);\n        encoded_host = host;\n    }\n\n    if (explicit_domain) |domain| {\n        if (domain.len > 0) {\n            const no_leading_dot = if (domain[0] == '.') domain[1..] else domain;\n\n            var aw = try std.Io.Writer.Allocating.initCapacity(arena, no_leading_dot.len + 1);\n            try aw.writer.writeByte('.');\n            try std.Uri.Component.percentEncode(&aw.writer, no_leading_dot, isHostChar);\n            const owned_domain = toLower(aw.written());\n\n            if (std.mem.indexOfScalarPos(u8, owned_domain, 1, '.') == null and std.mem.eql(u8, \"localhost\", owned_domain[1..]) == false) {\n                // can't set a cookie for a TLD\n                return error.InvalidDomain;\n            }\n            if (encoded_host) |host| {\n                if (std.mem.endsWith(u8, host, owned_domain[1..]) == false) {\n                    return error.InvalidDomain;\n                }\n            }\n\n            return owned_domain;\n        }\n    }\n\n    return encoded_host orelse return error.InvalidDomain; // default-domain\n}\n\npub fn percentEncode(arena: Allocator, part: []const u8, comptime isValidChar: fn (u8) bool) ![]u8 {\n    var aw = try std.Io.Writer.Allocating.initCapacity(arena, part.len);\n    try std.Uri.Component.percentEncode(&aw.writer, part, isValidChar);\n    return aw.written(); // @memory retains memory used before growing\n}\n\npub fn isHostChar(c: u8) bool {\n    return switch (c) {\n        'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,\n        '!', '$', '&', '\\'', '(', ')', '*', '+', ',', ';', '=' => true,\n        ':' => true,\n        '[', ']' => true,\n        else => false,\n    };\n}\n\npub fn isPathChar(c: u8) bool {\n    return switch (c) {\n        'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,\n        '!', '$', '&', '\\'', '(', ')', '*', '+', ',', ';', '=' => true,\n        '/', ':', '@' => true,\n        else => false,\n    };\n}\n\nfn parseNameValue(str: []const u8) !struct { []const u8, []const u8, []const u8 } {\n    const key_value_end = std.mem.indexOfScalarPos(u8, str, 0, ';') orelse str.len;\n    const rest = if (key_value_end == str.len) \"\" else str[key_value_end + 1 ..];\n\n    const sep = std.mem.indexOfScalarPos(u8, str[0..key_value_end], 0, '=') orelse {\n        const value = trim(str[0..key_value_end]);\n        if (value.len == 0) {\n            return error.Empty;\n        }\n        return .{ \"\", value, rest };\n    };\n\n    const name = trim(str[0..sep]);\n    const value = trim(str[sep + 1 .. key_value_end]);\n    return .{ name, value, rest };\n}\n\npub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, is_navigation: bool, is_http: bool) bool {\n    if (self.http_only and is_http == false) {\n        // http only cookies cannot be accessed from Javascript\n        return false;\n    }\n\n    if (url.secure == false and self.secure) {\n        // secure cookie can only be sent over HTTPs\n        return false;\n    }\n\n    if (same_site == false) {\n        // If we aren't on the \"same site\" (matching 2nd level domain\n        // taking into account public suffix list), then the cookie\n        // can only be sent if cookie.same_site == .none, or if\n        // we're navigating to (as opposed to, say, loading an image)\n        // and cookie.same_site == .lax\n        switch (self.same_site) {\n            .strict => return false,\n            .lax => if (is_navigation == false) return false,\n            .none => {},\n        }\n    }\n\n    {\n        if (self.domain[0] == '.') {\n            // When a Set-Cookie header has a Domain attribute\n            // Then we will _always_ prefix it with a dot, extending its\n            // availability to all subdomains (yes, setting the Domain\n            // attributes EXPANDS the domains which the cookie will be\n            // sent to, to always include all subdomains).\n            if (std.mem.eql(u8, url.host, self.domain[1..]) == false and std.mem.endsWith(u8, url.host, self.domain) == false) {\n                return false;\n            }\n        } else if (std.mem.eql(u8, url.host, self.domain) == false) {\n            // When the Domain attribute isn't specific, then the cookie\n            // is only sent on an exact match.\n            return false;\n        }\n    }\n\n    {\n        if (self.path[self.path.len - 1] == '/') {\n            // If our cookie has a trailing slash, we can only match is\n            // the target path is a perfix. I.e., if our path is\n            // /doc/  we can only match /doc/*\n            if (std.mem.startsWith(u8, url.path, self.path) == false) {\n                return false;\n            }\n        } else {\n            // Our cookie path is something like /hello\n            if (std.mem.startsWith(u8, url.path, self.path) == false) {\n                // The target path has to either be /hello (it isn't)\n                return false;\n            } else if (url.path.len < self.path.len or (url.path.len > self.path.len and url.path[self.path.len] != '/')) {\n                // Or it has to be something like /hello/* (it isn't)\n                // it isn't!\n                return false;\n            }\n        }\n    }\n    return true;\n}\n\npub const Jar = struct {\n    allocator: Allocator,\n    cookies: std.ArrayList(Cookie),\n\n    pub fn init(allocator: Allocator) Jar {\n        return .{\n            .cookies = .{},\n            .allocator = allocator,\n        };\n    }\n\n    pub fn deinit(self: *Jar) void {\n        for (self.cookies.items) |c| {\n            c.deinit();\n        }\n        self.cookies.deinit(self.allocator);\n    }\n\n    pub fn clearRetainingCapacity(self: *Jar) void {\n        for (self.cookies.items) |c| {\n            c.deinit();\n        }\n        self.cookies.clearRetainingCapacity();\n    }\n\n    pub fn add(\n        self: *Jar,\n        cookie: Cookie,\n        request_time: i64,\n    ) !void {\n        const is_expired = isCookieExpired(&cookie, request_time);\n        defer if (is_expired) {\n            cookie.deinit();\n        };\n\n        if (self.cookies.items.len >= max_jar_size) {\n            return error.CookieJarQuotaExceeded;\n        }\n        if (cookie.value.len > max_cookie_size) {\n            return error.CookieSizeExceeded;\n        }\n\n        for (self.cookies.items, 0..) |*c, i| {\n            if (areCookiesEqual(&cookie, c)) {\n                c.deinit();\n                if (is_expired) {\n                    _ = self.cookies.swapRemove(i);\n                } else {\n                    self.cookies.items[i] = cookie;\n                }\n                return;\n            }\n        }\n\n        if (!is_expired) {\n            try self.cookies.append(self.allocator, cookie);\n        }\n    }\n\n    pub fn removeExpired(self: *Jar, request_time: ?i64) void {\n        if (self.cookies.items.len == 0) return;\n        const time = request_time orelse std.time.timestamp();\n        var i: usize = self.cookies.items.len;\n        while (i > 0) {\n            i -= 1;\n            const cookie = &self.cookies.items[i];\n            if (isCookieExpired(cookie, time)) {\n                self.cookies.swapRemove(i).deinit();\n            }\n        }\n    }\n\n    pub const LookupOpts = struct {\n        is_http: bool,\n        request_time: ?i64 = null,\n        is_navigation: bool = true,\n        prefix: ?[]const u8 = null,\n        origin_url: ?[:0]const u8 = null,\n    };\n    pub fn forRequest(self: *Jar, target_url: [:0]const u8, writer: anytype, opts: LookupOpts) !void {\n        const target = PreparedUri{\n            .host = URL.getHostname(target_url),\n            .path = URL.getPathname(target_url),\n            .secure = URL.isHTTPS(target_url),\n        };\n        const same_site = try areSameSite(opts.origin_url, target.host);\n\n        removeExpired(self, opts.request_time);\n\n        var first = true;\n        for (self.cookies.items) |*cookie| {\n            if (!cookie.appliesTo(&target, same_site, opts.is_navigation, opts.is_http)) {\n                continue;\n            }\n\n            // we have a match!\n            if (first) {\n                if (opts.prefix) |prefix| {\n                    try writer.writeAll(prefix);\n                }\n                first = false;\n            } else {\n                try writer.writeAll(\"; \");\n            }\n            try writeCookie(cookie, writer);\n        }\n    }\n\n    pub fn populateFromResponse(self: *Jar, url: [:0]const u8, set_cookie: []const u8) !void {\n        const c = Cookie.parse(self.allocator, url, set_cookie) catch |err| {\n            log.warn(.page, \"cookie parse failed\", .{ .raw = set_cookie, .err = err });\n            return;\n        };\n\n        const now = std.time.timestamp();\n        try self.add(c, now);\n    }\n\n    fn writeCookie(cookie: *const Cookie, writer: anytype) !void {\n        if (cookie.name.len > 0) {\n            try writer.writeAll(cookie.name);\n            try writer.writeByte('=');\n        }\n        if (cookie.value.len > 0) {\n            try writer.writeAll(cookie.value);\n        }\n    }\n};\n\nfn isCookieExpired(cookie: *const Cookie, now: i64) bool {\n    const ce = cookie.expires orelse return false;\n    return ce <= @as(f64, @floatFromInt(now));\n}\n\nfn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {\n    if (std.mem.eql(u8, a.name, b.name) == false) {\n        return false;\n    }\n    if (std.mem.eql(u8, a.domain, b.domain) == false) {\n        return false;\n    }\n    if (std.mem.eql(u8, a.path, b.path) == false) {\n        return false;\n    }\n    return true;\n}\n\nfn areSameSite(origin_url_: ?[:0]const u8, target_host: []const u8) !bool {\n    const origin_url = origin_url_ orelse return true;\n    const origin_host = URL.getHostname(origin_url);\n\n    // common case\n    if (std.mem.eql(u8, target_host, origin_host)) {\n        return true;\n    }\n\n    return std.mem.eql(u8, findSecondLevelDomain(target_host), findSecondLevelDomain(origin_host));\n}\n\nfn findSecondLevelDomain(host: []const u8) []const u8 {\n    var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host;\n    while (true) {\n        i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host;\n        const strip = i + 1;\n        if (public_suffix_list(host[strip..]) == false) {\n            return host[strip..];\n        }\n    }\n}\n\npub const PreparedUri = struct {\n    host: []const u8, // Percent encoded, lower case\n    path: []const u8, // Percent encoded\n    secure: bool, // True if scheme is https\n};\n\nfn trim(str: []const u8) []const u8 {\n    return std.mem.trim(u8, str, &std.ascii.whitespace);\n}\n\nfn trimLeft(str: []const u8) []const u8 {\n    return std.mem.trimLeft(u8, str, &std.ascii.whitespace);\n}\n\nfn trimRight(str: []const u8) []const u8 {\n    return std.mem.trimRight(u8, str, &std.ascii.whitespace);\n}\n\nfn toLower(str: []u8) []u8 {\n    for (str, 0..) |c, i| {\n        str[i] = std.ascii.toLower(c);\n    }\n    return str;\n}\n\nconst testing = @import(\"../../../testing.zig\");\nconst test_url = \"http://lightpanda.io/\";\ntest \"cookie: findSecondLevelDomain\" {\n    const cases = [_]struct { []const u8, []const u8 }{\n        .{ \"\", \"\" },\n        .{ \"com\", \"com\" },\n        .{ \"lightpanda.io\", \"lightpanda.io\" },\n        .{ \"lightpanda.io\", \"test.lightpanda.io\" },\n        .{ \"lightpanda.io\", \"first.test.lightpanda.io\" },\n        .{ \"www.gov.uk\", \"www.gov.uk\" },\n        .{ \"stats.gov.uk\", \"www.stats.gov.uk\" },\n        .{ \"api.gov.uk\", \"api.gov.uk\" },\n        .{ \"dev.api.gov.uk\", \"dev.api.gov.uk\" },\n        .{ \"dev.api.gov.uk\", \"1.dev.api.gov.uk\" },\n    };\n    for (cases) |c| {\n        try testing.expectEqual(c.@\"0\", findSecondLevelDomain(c.@\"1\"));\n    }\n}\n\ntest \"Jar: add\" {\n    const expectCookies = struct {\n        fn expect(expected: []const struct { []const u8, []const u8 }, jar: Jar) !void {\n            try testing.expectEqual(expected.len, jar.cookies.items.len);\n            LOOP: for (expected) |e| {\n                for (jar.cookies.items) |c| {\n                    if (std.mem.eql(u8, e.@\"0\", c.name) and std.mem.eql(u8, e.@\"1\", c.value)) {\n                        continue :LOOP;\n                    }\n                }\n                std.debug.print(\"Cookie ({s}={s}) not found\", .{ e.@\"0\", e.@\"1\" });\n                return error.CookieNotFound;\n            }\n        }\n    }.expect;\n\n    const now = std.time.timestamp();\n\n    var jar = Jar.init(testing.allocator);\n    defer jar.deinit();\n    try expectCookies(&.{}, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"over=9000;Max-Age=0\"), now);\n    try expectCookies(&.{}, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"over=9000\"), now);\n    try expectCookies(&.{.{ \"over\", \"9000\" }}, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"over=9000!!\"), now);\n    try expectCookies(&.{.{ \"over\", \"9000!!\" }}, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"spice=flow\"), now);\n    try expectCookies(&.{ .{ \"over\", \"9000!!\" }, .{ \"spice\", \"flow\" } }, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"spice=flows;Path=/\"), now);\n    try expectCookies(&.{ .{ \"over\", \"9000!!\" }, .{ \"spice\", \"flows\" } }, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"over=9001;Path=/other\"), now);\n    try expectCookies(&.{ .{ \"over\", \"9000!!\" }, .{ \"spice\", \"flows\" }, .{ \"over\", \"9001\" } }, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"over=9002;Path=/;Domain=lightpanda.io\"), now);\n    try expectCookies(&.{ .{ \"over\", \"9000!!\" }, .{ \"spice\", \"flows\" }, .{ \"over\", \"9001\" }, .{ \"over\", \"9002\" } }, jar);\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"over=x;Path=/other;Max-Age=-200\"), now);\n    try expectCookies(&.{ .{ \"over\", \"9000!!\" }, .{ \"spice\", \"flows\" }, .{ \"over\", \"9002\" } }, jar);\n}\n\ntest \"Jar: add limit\" {\n    var jar = Jar.init(testing.allocator);\n    defer jar.deinit();\n\n    const now = std.time.timestamp();\n\n    // add a too big cookie value.\n    try testing.expectError(error.CookieSizeExceeded, jar.add(.{\n        .arena = std.heap.ArenaAllocator.init(testing.allocator),\n        .name = \"v\",\n        .domain = \"lightpanda.io\",\n        .path = \"/\",\n        .expires = null,\n        .value = \"v\" ** 4096 ++ \"v\",\n    }, now));\n\n    // generate unique names.\n    const names = comptime blk: {\n        @setEvalBranchQuota(max_jar_size);\n        var result: [max_jar_size][]const u8 = undefined;\n        for (0..max_jar_size) |i| {\n            result[i] = \"v\" ** i;\n        }\n        break :blk result;\n    };\n\n    // test the max number limit\n    var i: usize = 0;\n    while (i < max_jar_size) : (i += 1) {\n        const c = Cookie{\n            .arena = std.heap.ArenaAllocator.init(testing.allocator),\n            .name = names[i],\n            .domain = \"lightpanda.io\",\n            .path = \"/\",\n            .expires = null,\n            .value = \"v\",\n        };\n\n        try jar.add(c, now);\n    }\n\n    try testing.expectError(error.CookieJarQuotaExceeded, jar.add(.{\n        .arena = std.heap.ArenaAllocator.init(testing.allocator),\n        .name = \"last\",\n        .domain = \"lightpanda.io\",\n        .path = \"/\",\n        .expires = null,\n        .value = \"v\",\n    }, now));\n}\n\ntest \"Jar: forRequest\" {\n    const expectCookies = struct {\n        fn expect(expected: []const u8, jar: *Jar, target_url: [:0]const u8, opts: Jar.LookupOpts) !void {\n            var arr: std.ArrayList(u8) = .empty;\n            defer arr.deinit(testing.allocator);\n            try jar.forRequest(target_url, arr.writer(testing.allocator), opts);\n            try testing.expectEqual(expected, arr.items);\n        }\n    }.expect;\n\n    const now = std.time.timestamp();\n\n    var jar = Jar.init(testing.allocator);\n    defer jar.deinit();\n\n    const url2 = \"http://test.lightpanda.io/\";\n\n    {\n        // test with no cookies\n        try expectCookies(\"\", &jar, test_url, .{ .is_http = true });\n    }\n\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"global1=1\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"global2=2;Max-Age=30;domain=lightpanda.io\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"path1=3;Path=/about\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"path2=4;Path=/docs/\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"secure=5;Secure\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"sitenone=6;SameSite=None;Path=/x/;Secure\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"sitelax=7;SameSite=Lax;Path=/x/\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, test_url, \"sitestrict=8;SameSite=Strict;Path=/x/\"), now);\n    try jar.add(try Cookie.parse(testing.allocator, url2, \"domain1=9;domain=test.lightpanda.io\"), now);\n\n    // nothing fancy here\n    try expectCookies(\"global1=1; global2=2\", &jar, test_url, .{ .is_http = true });\n    try expectCookies(\"global1=1; global2=2\", &jar, test_url, .{ .origin_url = test_url, .is_navigation = false, .is_http = true });\n\n    // We have a cookie where Domain=lightpanda.io\n    // This should _not_ match xyxlightpanda.io\n    try expectCookies(\"\", &jar, \"http://anothersitelightpanda.io/\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // matching path without trailing /\n    try expectCookies(\"global1=1; global2=2; path1=3\", &jar, \"http://lightpanda.io/about\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // incomplete prefix path\n    try expectCookies(\"global1=1; global2=2\", &jar, \"http://lightpanda.io/abou\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // path doesn't match\n    try expectCookies(\"global1=1; global2=2\", &jar, \"http://lightpanda.io/aboutus\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // path doesn't match cookie directory\n    try expectCookies(\"global1=1; global2=2\", &jar, \"http://lightpanda.io/docs\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // exact directory match\n    try expectCookies(\"global1=1; global2=2; path2=4\", &jar, \"http://lightpanda.io/docs/\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // sub directory match\n    try expectCookies(\"global1=1; global2=2; path2=4\", &jar, \"http://lightpanda.io/docs/more\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // secure\n    try expectCookies(\"global1=1; global2=2; secure=5\", &jar, \"https://lightpanda.io/\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // navigational cross domain, secure\n    try expectCookies(\"global1=1; global2=2; secure=5; sitenone=6; sitelax=7\", &jar, \"https://lightpanda.io/x/\", .{\n        .origin_url = \"https://example.com/\",\n        .is_http = true,\n    });\n\n    // navigational cross domain, insecure\n    try expectCookies(\"global1=1; global2=2; sitelax=7\", &jar, \"http://lightpanda.io/x/\", .{\n        .origin_url = \"https://example.com/\",\n        .is_http = true,\n    });\n\n    // non-navigational cross domain, insecure\n    try expectCookies(\"\", &jar, \"http://lightpanda.io/x/\", .{\n        .origin_url = \"https://example.com/\",\n        .is_http = true,\n        .is_navigation = false,\n    });\n\n    // non-navigational cross domain, secure\n    try expectCookies(\"sitenone=6\", &jar, \"https://lightpanda.io/x/\", .{\n        .origin_url = \"https://example.com/\",\n        .is_http = true,\n        .is_navigation = false,\n    });\n\n    // non-navigational same origin\n    try expectCookies(\"global1=1; global2=2; sitelax=7; sitestrict=8\", &jar, \"http://lightpanda.io/x/\", .{\n        .origin_url = \"https://lightpanda.io/\",\n        .is_http = true,\n        .is_navigation = false,\n    });\n\n    // exact domain match + suffix\n    try expectCookies(\"global2=2; domain1=9\", &jar, \"http://test.lightpanda.io/\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // domain suffix match + suffix\n    try expectCookies(\"global2=2; domain1=9\", &jar, \"http://1.test.lightpanda.io/\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    // non-matching domain\n    try expectCookies(\"global2=2\", &jar, \"http://other.lightpanda.io/\", .{\n        .origin_url = test_url,\n        .is_http = true,\n    });\n\n    const l = jar.cookies.items.len;\n    try expectCookies(\"global1=1\", &jar, test_url, .{\n        .request_time = now + 100,\n        .origin_url = test_url,\n        .is_http = true,\n    });\n    try testing.expectEqual(l - 1, jar.cookies.items.len);\n\n    // If you add more cases after this point, note that the above test removes\n    // the 'global2' cookie\n}\n\ntest \"Cookie: parse key=value\" {\n    try expectError(error.Empty, null, \"\");\n    try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });\n    try expectError(error.InvalidByteSequence, null, &.{ 'a', 127, '=', 'b' });\n    try expectError(error.InvalidByteSequence, null, &.{ 'a', '=', 'b', 20 });\n    try expectError(error.InvalidByteSequence, null, &.{ 'a', '=', 'b', 128 });\n\n    try expectAttribute(.{ .name = \"\", .value = \"a\" }, null, \"a\");\n    try expectAttribute(.{ .name = \"\", .value = \"a\" }, null, \"a;\");\n    try expectAttribute(.{ .name = \"\", .value = \"a b\" }, null, \"a b\");\n    try expectAttribute(.{ .name = \"a b\", .value = \"b\" }, null, \"a b=b\");\n    try expectAttribute(.{ .name = \"a,\", .value = \"b\" }, null, \"a,=b\");\n    try expectAttribute(.{ .name = \":a>\", .value = \"b>><\" }, null, \":a>=b>><\");\n\n    try expectAttribute(.{ .name = \"abc\", .value = \"\" }, null, \"abc=\");\n    try expectAttribute(.{ .name = \"abc\", .value = \"\" }, null, \"abc=;\");\n\n    try expectAttribute(.{ .name = \"a\", .value = \"b\" }, null, \"a=b\");\n    try expectAttribute(.{ .name = \"a\", .value = \"b\" }, null, \"a=b;\");\n\n    try expectAttribute(.{ .name = \"abc\", .value = \"fe f\" }, null, \"abc=  fe f\");\n    try expectAttribute(.{ .name = \"abc\", .value = \"fe f\" }, null, \"abc=  fe f  \");\n    try expectAttribute(.{ .name = \"abc\", .value = \"fe f\" }, null, \"abc=  fe f;\");\n    try expectAttribute(.{ .name = \"abc\", .value = \"fe f\" }, null, \"abc=  fe f   ;\");\n    try expectAttribute(.{ .name = \"abc\", .value = \"\\\"  fe f\\\"\" }, null, \"abc=\\\"  fe f\\\"\");\n    try expectAttribute(.{ .name = \"abc\", .value = \"\\\"  fe f   \\\"\" }, null, \"abc=\\\"  fe f   \\\"\");\n    try expectAttribute(.{ .name = \"ab4344c\", .value = \"1ads23\" }, null, \"  ab4344c=1ads23  \");\n\n    try expectAttribute(.{ .name = \"ab4344c\", .value = \"1ads23\" }, null, \"  ab4344c  =  1ads23  ;\");\n}\n\ntest \"Cookie: parse path\" {\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/\", \"b\");\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/\", \"b;path\");\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/\", \"b;Path=\");\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/\", \"b;Path=;\");\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/\", \"b; Path=other\");\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/23\", \"b; path=other \");\n\n    try expectAttribute(.{ .path = \"/\" }, \"http://a/abc\", \"b\");\n    try expectAttribute(.{ .path = \"/abc\" }, \"http://a/abc/\", \"b\");\n    try expectAttribute(.{ .path = \"/abc\" }, \"http://a/abc/123\", \"b\");\n    try expectAttribute(.{ .path = \"/abc/123\" }, \"http://a/abc/123/\", \"b\");\n\n    try expectAttribute(.{ .path = \"/a\" }, \"http://a/\", \"b;Path=/a\");\n    try expectAttribute(.{ .path = \"/aa\" }, \"http://a/\", \"b;path=/aa;\");\n    try expectAttribute(.{ .path = \"/aabc/\" }, \"http://a/\", \"b;  path=  /aabc/ ;\");\n\n    try expectAttribute(.{ .path = \"/bbb/\" }, \"http://a/\", \"b;  path=/a/; path=/bbb/\");\n    try expectAttribute(.{ .path = \"/cc\" }, \"http://a/\", \"b;  path=/a/; path=/bbb/; path = /cc\");\n}\n\ntest \"Cookie: parse secure\" {\n    try expectAttribute(.{ .secure = false }, null, \"b\");\n    try expectAttribute(.{ .secure = false }, null, \"b;secured\");\n    try expectAttribute(.{ .secure = false }, null, \"b;security\");\n    try expectAttribute(.{ .secure = false }, null, \"b;SecureX\");\n    try expectAttribute(.{ .secure = true }, null, \"b; Secure\");\n    try expectAttribute(.{ .secure = true }, null, \"b; Secure  \");\n    try expectAttribute(.{ .secure = true }, null, \"b; Secure=on  \");\n    try expectAttribute(.{ .secure = true }, null, \"b; Secure=Off  \");\n    try expectAttribute(.{ .secure = true }, null, \"b; secure=Off  \");\n    try expectAttribute(.{ .secure = true }, null, \"b; seCUre=Off  \");\n}\n\ntest \"Cookie: parse HttpOnly\" {\n    try expectAttribute(.{ .http_only = false }, null, \"b\");\n    try expectAttribute(.{ .http_only = false }, null, \"b;HttpOnly0\");\n    try expectAttribute(.{ .http_only = false }, null, \"b;H ttpOnly\");\n    try expectAttribute(.{ .http_only = true }, null, \"b; HttpOnly\");\n    try expectAttribute(.{ .http_only = true }, null, \"b; Httponly  \");\n    try expectAttribute(.{ .http_only = true }, null, \"b; Httponly=on  \");\n    try expectAttribute(.{ .http_only = true }, null, \"b; httpOnly=Off  \");\n    try expectAttribute(.{ .http_only = true }, null, \"b; httpOnly=Off  \");\n    try expectAttribute(.{ .http_only = true }, null, \"b;    HttpOnly=Off  \");\n}\n\ntest \"Cookie: parse SameSite\" {\n    try expectAttribute(.{ .same_site = .lax }, null, \"b;samesite\");\n    try expectAttribute(.{ .same_site = .lax }, null, \"b;samesite=lax\");\n    try expectAttribute(.{ .same_site = .lax }, null, \"b;  SameSite=Lax  \");\n    try expectAttribute(.{ .same_site = .lax }, null, \"b;  SameSite=Other  \");\n    try expectAttribute(.{ .same_site = .lax }, null, \"b;  SameSite=Nope  \");\n\n    // SameSite=none is only valid when Secure is set. The whole cookie is\n    // rejected otherwise\n    try expectError(error.InsecureSameSite, null, \"b;samesite=none\");\n    try expectError(error.InsecureSameSite, null, \"b;SameSite=None\");\n    try expectAttribute(.{ .same_site = .none }, null, \"b;  samesite=none; secure  \");\n    try expectAttribute(.{ .same_site = .none }, null, \"b;  SameSite=None  ; SECURE\");\n    try expectAttribute(.{ .same_site = .none }, null, \"b;Secure;  SameSite=None\");\n    try expectAttribute(.{ .same_site = .none }, null, \"b; SameSite=None; Secure\");\n\n    try expectAttribute(.{ .same_site = .strict }, null, \"b;  samesite=Strict  \");\n    try expectAttribute(.{ .same_site = .strict }, null, \"b;  SameSite=  STRICT  \");\n    try expectAttribute(.{ .same_site = .strict }, null, \"b;  SameSITE=strict;\");\n    try expectAttribute(.{ .same_site = .strict }, null, \"b; SameSite=Strict\");\n\n    try expectAttribute(.{ .same_site = .strict }, null, \"b; SameSite=None; SameSite=lax; SameSite=Strict\");\n}\n\ntest \"Cookie: parse max-age\" {\n    try expectAttribute(.{ .expires = null }, null, \"b;max-age\");\n    try expectAttribute(.{ .expires = null }, null, \"b;max-age=abc\");\n    try expectAttribute(.{ .expires = null }, null, \"b;max-age=13.22\");\n    try expectAttribute(.{ .expires = null }, null, \"b;max-age=13abc\");\n\n    try expectAttribute(.{ .expires = std.time.timestamp() + 13 }, null, \"b;max-age=13\");\n    try expectAttribute(.{ .expires = std.time.timestamp() + -22 }, null, \"b;max-age=-22\");\n    try expectAttribute(.{ .expires = std.time.timestamp() + 4294967296 }, null, \"b;max-age=4294967296\");\n    try expectAttribute(.{ .expires = std.time.timestamp() + -4294967296 }, null, \"b;Max-Age= -4294967296\");\n    try expectAttribute(.{ .expires = std.time.timestamp() + 0 }, null, \"b; Max-Age=0\");\n    try expectAttribute(.{ .expires = std.time.timestamp() + 500 }, null, \"b; Max-Age = 500  ; Max-Age=invalid\");\n    try expectAttribute(.{ .expires = std.time.timestamp() + 1000 }, null, \"b;max-age=600;max-age=0;max-age = 1000\");\n}\n\ntest \"Cookie: parse expires\" {\n    try expectAttribute(.{ .expires = null }, null, \"b;expires=\");\n    try expectAttribute(.{ .expires = null }, null, \"b;expires=abc\");\n    try expectAttribute(.{ .expires = null }, null, \"b;expires=13.22\");\n    try expectAttribute(.{ .expires = null }, null, \"b;expires=33\");\n\n    try expectAttribute(.{ .expires = 1918798080 }, null, \"b;expires=Wed, 21 Oct 2030 07:28:00 GMT\");\n    try expectAttribute(.{ .expires = 1784275395 }, null, \"b;expires=Fri, 17-Jul-2026 08:03:15 GMT\");\n    // max-age has priority over expires\n    try expectAttribute(.{ .expires = std.time.timestamp() + 10 }, null, \"b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT\");\n}\n\ntest \"Cookie: parse all\" {\n    try expectCookie(.{\n        .name = \"user-id\",\n        .value = \"9000\",\n        .path = \"/cms\",\n        .domain = \"lightpanda.io\",\n    }, \"https://lightpanda.io/cms/users\", \"user-id=9000\");\n\n    try expectCookie(.{\n        .name = \"user-id\",\n        .value = \"9000\",\n        .path = \"/\",\n        .http_only = true,\n        .secure = true,\n        .domain = \".lightpanda.io\",\n        .expires = @floatFromInt(std.time.timestamp() + 30),\n    }, \"https://lightpanda.io/cms/users\", \"user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io\");\n\n    try expectCookie(.{\n        .name = \"app_session\",\n        .value = \"123\",\n        .path = \"/\",\n        .http_only = true,\n        .secure = false,\n        .domain = \".localhost\",\n        .same_site = .lax,\n        .expires = @floatFromInt(std.time.timestamp() + 7200),\n    }, \"http://localhost:8000/login\", \"app_session=123; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax\");\n}\n\ntest \"Cookie: parse domain\" {\n    try expectAttribute(.{ .domain = \"lightpanda.io\" }, \"http://lightpanda.io/\", \"b\");\n    try expectAttribute(.{ .domain = \"dev.lightpanda.io\" }, \"http://dev.lightpanda.io/\", \"b\");\n    try expectAttribute(.{ .domain = \".lightpanda.io\" }, \"http://lightpanda.io/\", \"b;domain=lightpanda.io\");\n    try expectAttribute(.{ .domain = \".lightpanda.io\" }, \"http://lightpanda.io/\", \"b;domain=.lightpanda.io\");\n    try expectAttribute(.{ .domain = \".dev.lightpanda.io\" }, \"http://dev.lightpanda.io/\", \"b;domain=dev.lightpanda.io\");\n    try expectAttribute(.{ .domain = \".lightpanda.io\" }, \"http://dev.lightpanda.io/\", \"b;domain=lightpanda.io\");\n    try expectAttribute(.{ .domain = \".lightpanda.io\" }, \"http://dev.lightpanda.io/\", \"b;domain=.lightpanda.io\");\n    try expectAttribute(.{ .domain = \".localhost\" }, \"http://localhost/\", \"b;domain=localhost\");\n    try expectAttribute(.{ .domain = \".localhost\" }, \"http://localhost/\", \"b;domain=.localhost\");\n\n    try expectError(error.InvalidDomain, \"http://lightpanda.io/\", \"b;domain=io\");\n    try expectError(error.InvalidDomain, \"http://lightpanda.io/\", \"b;domain=.io\");\n    try expectError(error.InvalidDomain, \"http://lightpanda.io/\", \"b;domain=other.lightpanda.io\");\n    try expectError(error.InvalidDomain, \"http://lightpanda.io/\", \"b;domain=other.lightpanda.com\");\n    try expectError(error.InvalidDomain, \"http://lightpanda.io/\", \"b;domain=other.example.com\");\n}\n\ntest \"Cookie: parse limit\" {\n    try expectError(error.CookieHeaderSizeExceeded, \"http://lightpanda.io/\", \"v\" ** 8192 ++ \";domain=lightpanda.io\");\n    try expectError(error.CookieSizeExceeded, \"http://lightpanda.io/\", \"v\" ** 4096 ++ \"v;domain=lightpanda.io\");\n}\n\nconst ExpectedCookie = struct {\n    name: []const u8,\n    value: []const u8,\n    path: []const u8,\n    domain: []const u8,\n    expires: ?f64 = null,\n    secure: bool = false,\n    http_only: bool = false,\n    same_site: Cookie.SameSite = .lax,\n};\n\nfn expectCookie(expected: ExpectedCookie, url: [:0]const u8, set_cookie: []const u8) !void {\n    var cookie = try Cookie.parse(testing.allocator, url, set_cookie);\n    defer cookie.deinit();\n\n    try testing.expectEqual(expected.name, cookie.name);\n    try testing.expectEqual(expected.value, cookie.value);\n    try testing.expectEqual(expected.secure, cookie.secure);\n    try testing.expectEqual(expected.http_only, cookie.http_only);\n    try testing.expectEqual(expected.same_site, cookie.same_site);\n    try testing.expectEqual(expected.path, cookie.path);\n    try testing.expectEqual(expected.domain, cookie.domain);\n\n    try testing.expectDelta(expected.expires, cookie.expires, 2.0);\n}\n\nfn expectAttribute(expected: anytype, url_: ?[:0]const u8, set_cookie: []const u8) !void {\n    var cookie = try Cookie.parse(testing.allocator, url_ orelse test_url, set_cookie);\n    defer cookie.deinit();\n\n    inline for (@typeInfo(@TypeOf(expected)).@\"struct\".fields) |f| {\n        if (comptime std.mem.eql(u8, f.name, \"expires\")) {\n            switch (@typeInfo(@TypeOf(expected.expires))) {\n                .int, .comptime_int => try testing.expectDelta(@as(f64, @floatFromInt(expected.expires)), cookie.expires, 1.0),\n                else => try testing.expectDelta(expected.expires, cookie.expires, 1.0),\n            }\n        } else {\n            try testing.expectEqual(@field(expected, f.name), @field(cookie, f.name));\n        }\n    }\n}\n\nfn expectError(expected: anyerror, url: ?[:0]const u8, set_cookie: []const u8) !void {\n    try testing.expectError(expected, Cookie.parse(testing.allocator, url orelse test_url, set_cookie));\n}\n"
  },
  {
    "path": "src/browser/webapi/storage/storage.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub fn registerTypes() []const type {\n    return &.{Lookup};\n}\n\npub const Cookie = @import(\"Cookie.zig\");\n\npub const Shed = struct {\n    _origins: std.StringHashMapUnmanaged(*Bucket) = .empty,\n\n    pub fn deinit(self: *Shed, allocator: Allocator) void {\n        var it = self._origins.iterator();\n        while (it.next()) |kv| {\n            allocator.free(kv.key_ptr.*);\n            allocator.destroy(kv.value_ptr.*);\n        }\n        self._origins.deinit(allocator);\n    }\n\n    pub fn getOrPut(self: *Shed, allocator: Allocator, origin: []const u8) !*Bucket {\n        const gop = try self._origins.getOrPut(allocator, origin);\n        if (gop.found_existing) {\n            return gop.value_ptr.*;\n        }\n\n        const bucket = try allocator.create(Bucket);\n        errdefer allocator.free(bucket);\n        bucket.* = .{};\n\n        gop.key_ptr.* = try allocator.dupe(u8, origin);\n        gop.value_ptr.* = bucket;\n        return bucket;\n    }\n};\n\npub const Bucket = struct { local: Lookup = .{}, session: Lookup = .{} };\n\npub const Lookup = struct {\n    _data: std.StringHashMapUnmanaged([]const u8) = .empty,\n    _size: usize = 0,\n\n    const max_size = 5 * 1024 * 1024;\n\n    pub fn getItem(self: *const Lookup, key_: ?[]const u8) ?[]const u8 {\n        const k = key_ orelse return null;\n        return self._data.get(k);\n    }\n\n    pub fn setItem(self: *Lookup, key_: ?[]const u8, value: []const u8, page: *Page) !void {\n        const k = key_ orelse return;\n\n        if (self._size + value.len > max_size) {\n            return error.QuotaExceeded;\n        }\n        defer self._size += value.len;\n\n        const key_owned = try page.dupeString(k);\n        const value_owned = try page.dupeString(value);\n\n        const gop = try self._data.getOrPut(page.arena, key_owned);\n        gop.value_ptr.* = value_owned;\n    }\n\n    pub fn removeItem(self: *Lookup, key_: ?[]const u8) void {\n        const k = key_ orelse return;\n        if (self._data.get(k)) |value| {\n            self._size -= value.len;\n            _ = self._data.remove(k);\n        }\n    }\n\n    pub fn clear(self: *Lookup) void {\n        self._data.clearRetainingCapacity();\n        self._size = 0;\n    }\n\n    pub fn key(self: *const Lookup, index: u32) ?[]const u8 {\n        var it = self._data.keyIterator();\n        var i: u32 = 0;\n        while (it.next()) |k| {\n            if (i == index) {\n                return k.*;\n            }\n            i += 1;\n        }\n        return null;\n    }\n\n    pub fn getLength(self: *const Lookup) u32 {\n        return @intCast(self._data.count());\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(Lookup);\n\n        pub const Meta = struct {\n            pub const name = \"Storage\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n\n        pub const length = bridge.accessor(Lookup.getLength, null, .{});\n        pub const getItem = bridge.function(Lookup.getItem, .{});\n        pub const setItem = bridge.function(Lookup.setItem, .{ .dom_exception = true });\n        pub const removeItem = bridge.function(Lookup.removeItem, .{});\n        pub const clear = bridge.function(Lookup.clear, .{});\n        pub const key = bridge.function(Lookup.key, .{});\n        pub const @\"[str]\" = bridge.namedIndexed(Lookup.getItem, Lookup.setItem, null, .{ .null_as_undefined = true });\n    };\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: Storage\" {\n    try testing.htmlRunner(\"storage.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/streams/ReadableStream.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../../log.zig\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ReadableStreamDefaultReader = @import(\"ReadableStreamDefaultReader.zig\");\nconst ReadableStreamDefaultController = @import(\"ReadableStreamDefaultController.zig\");\nconst WritableStream = @import(\"WritableStream.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub fn registerTypes() []const type {\n    return &.{\n        ReadableStream,\n        AsyncIterator,\n    };\n}\n\nconst ReadableStream = @This();\n\npub const State = enum {\n    readable,\n    closed,\n    errored,\n};\n\n_page: *Page,\n_state: State,\n_reader: ?*ReadableStreamDefaultReader,\n_controller: *ReadableStreamDefaultController,\n_stored_error: ?[]const u8,\n_pull_fn: ?js.Function.Global = null,\n_pulling: bool = false,\n_pull_again: bool = false,\n_cancel: ?Cancel = null,\n\nconst UnderlyingSource = struct {\n    start: ?js.Function = null,\n    pull: ?js.Function.Global = null,\n    cancel: ?js.Function.Global = null,\n    type: ?[]const u8 = null,\n};\n\nconst QueueingStrategy = struct {\n    size: ?js.Function = null,\n    highWaterMark: u32 = 1,\n};\n\npub fn init(src_: ?UnderlyingSource, strategy_: ?QueueingStrategy, page: *Page) !*ReadableStream {\n    const strategy: QueueingStrategy = strategy_ orelse .{};\n\n    const self = try page._factory.create(ReadableStream{\n        ._page = page,\n        ._state = .readable,\n        ._reader = null,\n        ._controller = undefined,\n        ._stored_error = null,\n    });\n\n    self._controller = try ReadableStreamDefaultController.init(self, strategy.highWaterMark, page);\n\n    if (src_) |src| {\n        if (src.start) |start| {\n            try start.call(void, .{self._controller});\n        }\n\n        if (src.cancel) |callback| {\n            self._cancel = .{\n                .callback = callback,\n            };\n        }\n\n        if (src.pull) |pull| {\n            self._pull_fn = pull;\n            try self.callPullIfNeeded();\n        }\n    }\n\n    return self;\n}\n\npub fn initWithData(data: []const u8, page: *Page) !*ReadableStream {\n    const stream = try init(null, null, page);\n\n    // For Phase 1: immediately enqueue all data and close\n    try stream._controller.enqueue(.{ .uint8array = .{ .values = data } });\n    try stream._controller.close();\n\n    return stream;\n}\n\npub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader {\n    if (self.getLocked()) {\n        return error.ReaderLocked;\n    }\n\n    const reader = try ReadableStreamDefaultReader.init(self, page);\n    self._reader = reader;\n    return reader;\n}\n\npub fn releaseReader(self: *ReadableStream) void {\n    self._reader = null;\n}\n\npub fn getAsyncIterator(self: *ReadableStream, page: *Page) !*AsyncIterator {\n    return AsyncIterator.init(self, page);\n}\n\npub fn getLocked(self: *const ReadableStream) bool {\n    return self._reader != null;\n}\n\npub fn callPullIfNeeded(self: *ReadableStream) !void {\n    if (!self.shouldCallPull()) {\n        return;\n    }\n\n    if (self._pulling) {\n        self._pull_again = true;\n        return;\n    }\n\n    self._pulling = true;\n\n    if (comptime IS_DEBUG) {\n        if (self._page.js.local == null) {\n            log.fatal(.bug, \"null context scope\", .{ .src = \"ReadableStream.callPullIfNeeded\", .url = self._page.url });\n            std.debug.assert(self._page.js.local != null);\n        }\n    }\n\n    {\n        const func = self._pull_fn orelse return;\n\n        var ls: js.Local.Scope = undefined;\n        self._page.js.localScope(&ls);\n        defer ls.deinit();\n\n        // Call the pull function\n        // Note: In a complete implementation, we'd handle the promise returned by pull\n        // and set _pulling = false when it resolves\n        try ls.toLocal(func).call(void, .{self._controller});\n    }\n\n    self._pulling = false;\n\n    // If pull was requested again while we were pulling, pull again\n    if (self._pull_again) {\n        self._pull_again = false;\n        try self.callPullIfNeeded();\n    }\n}\n\nfn shouldCallPull(self: *const ReadableStream) bool {\n    if (self._state != .readable) {\n        return false;\n    }\n\n    if (self._pull_fn == null) {\n        return false;\n    }\n\n    const desired_size = self._controller.getDesiredSize() orelse return false;\n    return desired_size > 0;\n}\n\npub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promise {\n    const local = page.js.local.?;\n\n    if (self._state != .readable) {\n        if (self._cancel) |c| {\n            if (c.resolver) |r| {\n                return local.toLocal(r).promise();\n            }\n        }\n        return local.resolvePromise(.{});\n    }\n\n    if (self._cancel == null) {\n        self._cancel = Cancel{};\n    }\n\n    var c = &self._cancel.?;\n    var resolver = blk: {\n        if (c.resolver) |r| {\n            break :blk local.toLocal(r);\n        }\n        var temp = local.createPromiseResolver();\n        c.resolver = try temp.persist();\n        break :blk temp;\n    };\n\n    // Execute the cancel callback if provided\n    if (c.callback) |cb| {\n        if (reason) |r| {\n            try local.toLocal(cb).call(void, .{r});\n        } else {\n            try local.toLocal(cb).call(void, .{});\n        }\n    }\n\n    self._state = .closed;\n    self._controller._queue.clearRetainingCapacity();\n\n    const result = ReadableStreamDefaultReader.ReadResult{\n        .done = true,\n        .value = .empty,\n    };\n    for (self._controller._pending_reads.items) |r| {\n        local.toLocal(r).resolve(\"stream cancelled\", result);\n    }\n    self._controller._pending_reads.clearRetainingCapacity();\n    resolver.resolve(\"ReadableStream.cancel\", {});\n    return resolver.promise();\n}\n\n/// pipeThrough(transform) — pipes this readable stream through a transform stream,\n/// returning the readable side. `transform` is a JS object with `readable` and `writable` properties.\nconst PipeTransform = struct {\n    writable: *WritableStream,\n    readable: *ReadableStream,\n};\npub fn pipeThrough(self: *ReadableStream, transform: PipeTransform, page: *Page) !*ReadableStream {\n    if (self.getLocked()) {\n        return error.ReaderLocked;\n    }\n\n    // Start async piping from this stream to the writable side\n    try PipeState.startPipe(self, transform.writable, null, page);\n\n    return transform.readable;\n}\n\n/// pipeTo(writable) — pipes this readable stream to a writable stream.\n/// Returns a promise that resolves when piping is complete.\npub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {\n    if (self.getLocked()) {\n        return page.js.local.?.rejectPromise(\"ReadableStream is locked\");\n    }\n\n    const local = page.js.local.?;\n    var pipe_resolver = local.createPromiseResolver();\n    const promise = pipe_resolver.promise();\n    const persisted_resolver = try pipe_resolver.persist();\n\n    try PipeState.startPipe(self, destination, persisted_resolver, page);\n\n    return promise;\n}\n\n/// State for an async pipe operation.\nconst PipeState = struct {\n    reader: *ReadableStreamDefaultReader,\n    writable: *WritableStream,\n    context_id: usize,\n    resolver: ?js.PromiseResolver.Global,\n\n    fn startPipe(\n        stream: *ReadableStream,\n        writable: *WritableStream,\n        resolver: ?js.PromiseResolver.Global,\n        page: *Page,\n    ) !void {\n        const reader = try stream.getReader(page);\n        const state = try page.arena.create(PipeState);\n        state.* = .{\n            .reader = reader,\n            .writable = writable,\n            .context_id = page.js.id,\n            .resolver = resolver,\n        };\n        try state.pumpRead(page);\n    }\n\n    fn pumpRead(state: *PipeState, page: *Page) !void {\n        const local = page.js.local.?;\n\n        // Call reader.read() which returns a Promise\n        const read_promise = try state.reader.read(page);\n\n        // Create JS callback functions for .then() and .catch()\n        const then_fn = local.newCallback(onReadFulfilled, state);\n        const catch_fn = local.newCallback(onReadRejected, state);\n\n        _ = read_promise.thenAndCatch(then_fn, catch_fn) catch {\n            state.finish(local);\n        };\n    }\n\n    const ReadData = struct {\n        done: bool,\n        value: js.Value,\n    };\n    fn onReadFulfilled(self: *PipeState, data_: ?ReadData, page: *Page) void {\n        const local = page.js.local.?;\n        const data = data_ orelse {\n            return self.finish(local);\n        };\n\n        if (data.done) {\n            // Stream is finished, close the writable side\n            self.writable.closeStream(page) catch {};\n            self.reader.releaseLock();\n            if (self.resolver) |r| {\n                local.toLocal(r).resolve(\"pipeTo complete\", {});\n            }\n            return;\n        }\n\n        const value = data.value;\n        if (value.isUndefined()) {\n            return self.finish(local);\n        }\n\n        self.writable.writeChunk(value, page) catch {\n            return self.finish(local);\n        };\n\n        // Continue reading the next chunk\n        self.pumpRead(page) catch {\n            self.finish(local);\n        };\n    }\n\n    fn onReadRejected(self: *PipeState, page: *Page) void {\n        self.finish(page.js.local.?);\n    }\n\n    fn finish(self: *PipeState, local: *const js.Local) void {\n        self.reader.releaseLock();\n        if (self.resolver) |r| {\n            local.toLocal(r).resolve(\"pipe finished\", {});\n        }\n    }\n};\n\nconst Cancel = struct {\n    callback: ?js.Function.Global = null,\n    reason: ?[]const u8 = null,\n    resolver: ?js.PromiseResolver.Global = null,\n};\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ReadableStream);\n\n    pub const Meta = struct {\n        pub const name = \"ReadableStream\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(ReadableStream.init, .{});\n    pub const cancel = bridge.function(ReadableStream.cancel, .{});\n    pub const getReader = bridge.function(ReadableStream.getReader, .{});\n    pub const pipeThrough = bridge.function(ReadableStream.pipeThrough, .{});\n    pub const pipeTo = bridge.function(ReadableStream.pipeTo, .{});\n    pub const locked = bridge.accessor(ReadableStream.getLocked, null, .{});\n    pub const symbol_async_iterator = bridge.iterator(ReadableStream.getAsyncIterator, .{ .async = true });\n};\n\npub const AsyncIterator = struct {\n    _stream: *ReadableStream,\n    _reader: *ReadableStreamDefaultReader,\n\n    pub fn init(stream: *ReadableStream, page: *Page) !*AsyncIterator {\n        const reader = try stream.getReader(page);\n        return page._factory.create(AsyncIterator{\n            ._reader = reader,\n            ._stream = stream,\n        });\n    }\n\n    pub fn next(self: *AsyncIterator, page: *Page) !js.Promise {\n        return self._reader.read(page);\n    }\n\n    pub fn @\"return\"(self: *AsyncIterator, page: *Page) !js.Promise {\n        self._reader.releaseLock();\n        return page.js.local.?.resolvePromise(.{ .done = true, .value = null });\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(ReadableStream.AsyncIterator);\n\n        pub const Meta = struct {\n            pub const name = \"ReadableStreamAsyncIterator\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n\n        pub const next = bridge.function(ReadableStream.AsyncIterator.next, .{});\n        pub const @\"return\" = bridge.function(ReadableStream.AsyncIterator.@\"return\", .{});\n    };\n};\n\nconst testing = @import(\"../../../testing.zig\");\ntest \"WebApi: ReadableStream\" {\n    try testing.htmlRunner(\"streams/readable_stream.html\", .{});\n}\n"
  },
  {
    "path": "src/browser/webapi/streams/ReadableStreamDefaultController.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../../log.zig\");\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ReadableStream = @import(\"ReadableStream.zig\");\nconst ReadableStreamDefaultReader = @import(\"ReadableStreamDefaultReader.zig\");\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\nconst ReadableStreamDefaultController = @This();\n\npub const Chunk = union(enum) {\n    // the order matters, sorry.\n    uint8array: js.TypedArray(u8),\n    string: []const u8,\n    js_value: js.Value.Global,\n\n    pub fn dupe(self: Chunk, allocator: std.mem.Allocator) !Chunk {\n        return switch (self) {\n            .string => |str| .{ .string = try allocator.dupe(u8, str) },\n            .uint8array => |arr| .{ .uint8array = try arr.dupe(allocator) },\n            .js_value => |val| .{ .js_value = val },\n        };\n    }\n};\n\n_page: *Page,\n_stream: *ReadableStream,\n_arena: std.mem.Allocator,\n_queue: std.ArrayList(Chunk),\n_pending_reads: std.ArrayList(js.PromiseResolver.Global),\n_high_water_mark: u32,\n\npub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*ReadableStreamDefaultController {\n    return page._factory.create(ReadableStreamDefaultController{\n        ._page = page,\n        ._queue = .empty,\n        ._stream = stream,\n        ._arena = page.arena,\n        ._pending_reads = .empty,\n        ._high_water_mark = high_water_mark,\n    });\n}\n\npub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise {\n    const resolver = page.js.local.?.createPromiseResolver();\n    try self._pending_reads.append(self._arena, try resolver.persist());\n    return resolver.promise();\n}\n\npub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void {\n    if (self._stream._state != .readable) {\n        return error.StreamNotReadable;\n    }\n\n    if (self._pending_reads.items.len == 0) {\n        const chunk_copy = try chunk.dupe(self._page.arena);\n        return self._queue.append(self._arena, chunk_copy);\n    }\n\n    // I know, this is ouch! But we expect to have very few (if any)\n    // pending reads.\n    const resolver = self._pending_reads.orderedRemove(0);\n    const result = ReadableStreamDefaultReader.ReadResult{\n        .done = false,\n        .value = .fromChunk(chunk),\n    };\n\n    if (comptime IS_DEBUG) {\n        if (self._page.js.local == null) {\n            log.fatal(.bug, \"null context scope\", .{ .src = \"ReadableStreamDefaultController.enqueue\", .url = self._page.url });\n            std.debug.assert(self._page.js.local != null);\n        }\n    }\n\n    var ls: js.Local.Scope = undefined;\n    self._page.js.localScope(&ls);\n    defer ls.deinit();\n\n    ls.toLocal(resolver).resolve(\"stream enqueue\", result);\n}\n\n/// Enqueue a raw JS value, preserving its type (number, bool, object, etc.).\n/// Used by the JS-facing API; internal Zig callers should use enqueue(Chunk).\npub fn enqueueValue(self: *ReadableStreamDefaultController, value: js.Value) !void {\n    if (self._stream._state != .readable) {\n        return error.StreamNotReadable;\n    }\n\n    if (self._pending_reads.items.len == 0) {\n        const persisted = try value.persist();\n        try self._queue.append(self._arena, .{ .js_value = persisted });\n        return;\n    }\n\n    const resolver = self._pending_reads.orderedRemove(0);\n    const persisted = try value.persist();\n    const result = ReadableStreamDefaultReader.ReadResult{\n        .done = false,\n        .value = .{ .js_value = persisted },\n    };\n\n    if (comptime IS_DEBUG) {\n        if (self._page.js.local == null) {\n            log.fatal(.bug, \"null context scope\", .{ .src = \"ReadableStreamDefaultController.enqueueValue\", .url = self._page.url });\n            std.debug.assert(self._page.js.local != null);\n        }\n    }\n\n    var ls: js.Local.Scope = undefined;\n    self._page.js.localScope(&ls);\n    defer ls.deinit();\n\n    ls.toLocal(resolver).resolve(\"stream enqueue value\", result);\n}\n\npub fn close(self: *ReadableStreamDefaultController) !void {\n    if (self._stream._state != .readable) {\n        return error.StreamNotReadable;\n    }\n\n    self._stream._state = .closed;\n\n    // Resolve all pending reads with done=true\n    const result = ReadableStreamDefaultReader.ReadResult{\n        .done = true,\n        .value = .empty,\n    };\n\n    if (comptime IS_DEBUG) {\n        if (self._page.js.local == null) {\n            log.fatal(.bug, \"null context scope\", .{ .src = \"ReadableStreamDefaultController.close\", .url = self._page.url });\n            std.debug.assert(self._page.js.local != null);\n        }\n    }\n\n    for (self._pending_reads.items) |resolver| {\n        var ls: js.Local.Scope = undefined;\n        self._page.js.localScope(&ls);\n        defer ls.deinit();\n        ls.toLocal(resolver).resolve(\"stream close\", result);\n    }\n\n    self._pending_reads.clearRetainingCapacity();\n}\n\npub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void {\n    if (self._stream._state != .readable) {\n        return;\n    }\n\n    self._stream._state = .errored;\n    self._stream._stored_error = try self._page.arena.dupe(u8, err);\n\n    // Reject all pending reads\n    for (self._pending_reads.items) |resolver| {\n        self._page.js.toLocal(resolver).reject(\"stream errror\", err);\n    }\n    self._pending_reads.clearRetainingCapacity();\n}\n\npub fn dequeue(self: *ReadableStreamDefaultController) ?Chunk {\n    if (self._queue.items.len == 0) {\n        return null;\n    }\n    const chunk = self._queue.orderedRemove(0);\n\n    // After dequeueing, we may need to pull more data\n    self._stream.callPullIfNeeded() catch {};\n\n    return chunk;\n}\n\npub fn getDesiredSize(self: *const ReadableStreamDefaultController) ?i32 {\n    switch (self._stream._state) {\n        .errored => return null,\n        .closed => return 0,\n        .readable => {\n            const queue_size: i32 = @intCast(self._queue.items.len);\n            const hwm: i32 = @intCast(self._high_water_mark);\n            return hwm - queue_size;\n        },\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ReadableStreamDefaultController);\n\n    pub const Meta = struct {\n        pub const name = \"ReadableStreamDefaultController\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueueValue, .{});\n    pub const close = bridge.function(ReadableStreamDefaultController.close, .{});\n    pub const @\"error\" = bridge.function(ReadableStreamDefaultController.doError, .{});\n    pub const desiredSize = bridge.accessor(ReadableStreamDefaultController.getDesiredSize, null, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/streams/ReadableStreamDefaultReader.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst js = @import(\"../../js/js.zig\");\n\nconst Page = @import(\"../../Page.zig\");\nconst ReadableStream = @import(\"ReadableStream.zig\");\nconst ReadableStreamDefaultController = @import(\"ReadableStreamDefaultController.zig\");\n\nconst ReadableStreamDefaultReader = @This();\n\n_page: *Page,\n_stream: ?*ReadableStream,\n\npub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader {\n    return page._factory.create(ReadableStreamDefaultReader{\n        ._stream = stream,\n        ._page = page,\n    });\n}\n\npub const ReadResult = struct {\n    done: bool,\n    value: Chunk,\n\n    // Done like this so that we can properly return undefined in some cases\n    const Chunk = union(enum) {\n        empty,\n        string: []const u8,\n        uint8array: js.TypedArray(u8),\n        js_value: js.Value.Global,\n\n        pub fn fromChunk(chunk: ReadableStreamDefaultController.Chunk) Chunk {\n            return switch (chunk) {\n                .string => |s| .{ .string = s },\n                .uint8array => |arr| .{ .uint8array = arr },\n                .js_value => |val| .{ .js_value = val },\n            };\n        }\n    };\n};\n\npub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {\n    const stream = self._stream orelse {\n        return page.js.local.?.rejectPromise(\"Reader has been released\");\n    };\n\n    if (stream._state == .errored) {\n        const err = stream._stored_error orelse \"Stream errored\";\n        return page.js.local.?.rejectPromise(err);\n    }\n\n    if (stream._controller.dequeue()) |chunk| {\n        const result = ReadResult{\n            .done = false,\n            .value = .fromChunk(chunk),\n        };\n        return page.js.local.?.resolvePromise(result);\n    }\n\n    if (stream._state == .closed) {\n        const result = ReadResult{\n            .done = true,\n            .value = .empty,\n        };\n        return page.js.local.?.resolvePromise(result);\n    }\n\n    // No data, but not closed. We need to queue the read for any future data\n    return stream._controller.addPendingRead(page);\n}\n\npub fn releaseLock(self: *ReadableStreamDefaultReader) void {\n    if (self._stream) |stream| {\n        stream.releaseReader();\n        self._stream = null;\n    }\n}\n\npub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {\n    const stream = self._stream orelse {\n        return page.js.local.?.rejectPromise(\"Reader has been released\");\n    };\n\n    self.releaseLock();\n\n    return stream.cancel(reason_, page);\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(ReadableStreamDefaultReader);\n\n    pub const Meta = struct {\n        pub const name = \"ReadableStreamDefaultReader\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const read = bridge.function(ReadableStreamDefaultReader.read, .{});\n    pub const cancel = bridge.function(ReadableStreamDefaultReader.cancel, .{});\n    pub const releaseLock = bridge.function(ReadableStreamDefaultReader.releaseLock, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/streams/TransformStream.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst ReadableStream = @import(\"ReadableStream.zig\");\nconst ReadableStreamDefaultController = @import(\"ReadableStreamDefaultController.zig\");\nconst WritableStream = @import(\"WritableStream.zig\");\n\nconst TransformStream = @This();\n\npub const DefaultController = TransformStreamDefaultController;\n\npub const ZigTransformFn = *const fn (*TransformStreamDefaultController, js.Value) anyerror!void;\n\n_readable: *ReadableStream,\n_writable: *WritableStream,\n_controller: *TransformStreamDefaultController,\n\nconst Transformer = struct {\n    start: ?js.Function = null,\n    transform: ?js.Function.Global = null,\n    flush: ?js.Function.Global = null,\n};\n\npub fn init(transformer_: ?Transformer, page: *Page) !*TransformStream {\n    const readable = try ReadableStream.init(null, null, page);\n\n    const self = try page._factory.create(TransformStream{\n        ._readable = readable,\n        ._writable = undefined,\n        ._controller = undefined,\n    });\n\n    const transform_controller = try TransformStreamDefaultController.init(\n        self,\n        if (transformer_) |t| t.transform else null,\n        if (transformer_) |t| t.flush else null,\n        null,\n        page,\n    );\n    self._controller = transform_controller;\n\n    self._writable = try WritableStream.initForTransform(self, page);\n\n    if (transformer_) |transformer| {\n        if (transformer.start) |start| {\n            try start.call(void, .{transform_controller});\n        }\n    }\n\n    return self;\n}\n\npub fn initWithZigTransform(zig_transform: ZigTransformFn, page: *Page) !*TransformStream {\n    const readable = try ReadableStream.init(null, null, page);\n\n    const self = try page._factory.create(TransformStream{\n        ._readable = readable,\n        ._writable = undefined,\n        ._controller = undefined,\n    });\n\n    const transform_controller = try TransformStreamDefaultController.init(self, null, null, zig_transform, page);\n    self._controller = transform_controller;\n\n    self._writable = try WritableStream.initForTransform(self, page);\n\n    return self;\n}\n\npub fn transformWrite(self: *TransformStream, chunk: js.Value, page: *Page) !void {\n    if (self._controller._zig_transform_fn) |zig_fn| {\n        // Zig-level transform (used by TextEncoderStream etc.)\n        try zig_fn(self._controller, chunk);\n        return;\n    }\n\n    if (self._controller._transform_fn) |transform_fn| {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        try ls.toLocal(transform_fn).call(void, .{ chunk, self._controller });\n    } else {\n        try self._readable._controller.enqueue(.{ .string = try chunk.toStringSlice() });\n    }\n}\n\npub fn transformClose(self: *TransformStream, page: *Page) !void {\n    if (self._controller._flush_fn) |flush_fn| {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        try ls.toLocal(flush_fn).call(void, .{self._controller});\n    }\n\n    try self._readable._controller.close();\n}\n\npub fn getReadable(self: *const TransformStream) *ReadableStream {\n    return self._readable;\n}\n\npub fn getWritable(self: *const TransformStream) *WritableStream {\n    return self._writable;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(TransformStream);\n\n    pub const Meta = struct {\n        pub const name = \"TransformStream\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(TransformStream.init, .{});\n    pub const readable = bridge.accessor(TransformStream.getReadable, null, .{});\n    pub const writable = bridge.accessor(TransformStream.getWritable, null, .{});\n};\n\npub fn registerTypes() []const type {\n    return &.{\n        TransformStream,\n        TransformStreamDefaultController,\n    };\n}\n\npub const TransformStreamDefaultController = struct {\n    _stream: *TransformStream,\n    _transform_fn: ?js.Function.Global,\n    _flush_fn: ?js.Function.Global,\n    _zig_transform_fn: ?ZigTransformFn,\n\n    pub fn init(\n        stream: *TransformStream,\n        transform_fn: ?js.Function.Global,\n        flush_fn: ?js.Function.Global,\n        zig_transform_fn: ?ZigTransformFn,\n        page: *Page,\n    ) !*TransformStreamDefaultController {\n        return page._factory.create(TransformStreamDefaultController{\n            ._stream = stream,\n            ._transform_fn = transform_fn,\n            ._flush_fn = flush_fn,\n            ._zig_transform_fn = zig_transform_fn,\n        });\n    }\n\n    pub fn enqueue(self: *TransformStreamDefaultController, chunk: ReadableStreamDefaultController.Chunk) !void {\n        try self._stream._readable._controller.enqueue(chunk);\n    }\n\n    /// Enqueue a raw JS value, preserving its type. Used by the JS-facing API.\n    pub fn enqueueValue(self: *TransformStreamDefaultController, value: js.Value) !void {\n        try self._stream._readable._controller.enqueueValue(value);\n    }\n\n    pub fn doError(self: *TransformStreamDefaultController, reason: []const u8) !void {\n        try self._stream._readable._controller.doError(reason);\n    }\n\n    pub fn terminate(self: *TransformStreamDefaultController) !void {\n        try self._stream._readable._controller.close();\n    }\n\n    pub const JsApi = struct {\n        pub const bridge = js.Bridge(TransformStreamDefaultController);\n\n        pub const Meta = struct {\n            pub const name = \"TransformStreamDefaultController\";\n            pub const prototype_chain = bridge.prototypeChain();\n            pub var class_id: bridge.ClassId = undefined;\n        };\n\n        pub const enqueue = bridge.function(TransformStreamDefaultController.enqueueValue, .{});\n        pub const @\"error\" = bridge.function(TransformStreamDefaultController.doError, .{});\n        pub const terminate = bridge.function(TransformStreamDefaultController.terminate, .{});\n    };\n};\n"
  },
  {
    "path": "src/browser/webapi/streams/WritableStream.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\n\nconst WritableStreamDefaultWriter = @import(\"WritableStreamDefaultWriter.zig\");\nconst WritableStreamDefaultController = @import(\"WritableStreamDefaultController.zig\");\nconst TransformStream = @import(\"TransformStream.zig\");\n\nconst WritableStream = @This();\n\npub const State = enum {\n    writable,\n    closed,\n    errored,\n};\n\n_state: State,\n_writer: ?*WritableStreamDefaultWriter,\n_controller: *WritableStreamDefaultController,\n_stored_error: ?[]const u8,\n_write_fn: ?js.Function.Global,\n_close_fn: ?js.Function.Global,\n_transform_stream: ?*TransformStream,\n\nconst UnderlyingSink = struct {\n    start: ?js.Function = null,\n    write: ?js.Function.Global = null,\n    close: ?js.Function.Global = null,\n    abort: ?js.Function.Global = null,\n    type: ?[]const u8 = null,\n};\n\npub fn init(sink_: ?UnderlyingSink, page: *Page) !*WritableStream {\n    const self = try page._factory.create(WritableStream{\n        ._state = .writable,\n        ._writer = null,\n        ._controller = undefined,\n        ._stored_error = null,\n        ._write_fn = null,\n        ._close_fn = null,\n        ._transform_stream = null,\n    });\n\n    self._controller = try WritableStreamDefaultController.init(self, page);\n\n    if (sink_) |sink| {\n        if (sink.start) |start| {\n            try start.call(void, .{self._controller});\n        }\n        self._write_fn = sink.write;\n        self._close_fn = sink.close;\n    }\n\n    return self;\n}\n\npub fn initForTransform(transform_stream: *TransformStream, page: *Page) !*WritableStream {\n    const self = try page._factory.create(WritableStream{\n        ._state = .writable,\n        ._writer = null,\n        ._controller = undefined,\n        ._stored_error = null,\n        ._write_fn = null,\n        ._close_fn = null,\n        ._transform_stream = transform_stream,\n    });\n\n    self._controller = try WritableStreamDefaultController.init(self, page);\n    return self;\n}\n\npub fn getWriter(self: *WritableStream, page: *Page) !*WritableStreamDefaultWriter {\n    if (self.getLocked()) {\n        return error.WriterLocked;\n    }\n\n    const writer = try WritableStreamDefaultWriter.init(self, page);\n    self._writer = writer;\n    return writer;\n}\n\npub fn getLocked(self: *const WritableStream) bool {\n    return self._writer != null;\n}\n\npub fn writeChunk(self: *WritableStream, chunk: js.Value, page: *Page) !void {\n    if (self._state != .writable) return;\n\n    if (self._transform_stream) |ts| {\n        try ts.transformWrite(chunk, page);\n        return;\n    }\n\n    if (self._write_fn) |write_fn| {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        try ls.toLocal(write_fn).call(void, .{ chunk, self._controller });\n    }\n}\n\npub fn closeStream(self: *WritableStream, page: *Page) !void {\n    if (self._state != .writable) return;\n    self._state = .closed;\n\n    if (self._transform_stream) |ts| {\n        try ts.transformClose(page);\n        return;\n    }\n\n    if (self._close_fn) |close_fn| {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        try ls.toLocal(close_fn).call(void, .{self._controller});\n    }\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(WritableStream);\n\n    pub const Meta = struct {\n        pub const name = \"WritableStream\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const constructor = bridge.constructor(WritableStream.init, .{});\n    pub const getWriter = bridge.function(WritableStream.getWriter, .{});\n    pub const locked = bridge.accessor(WritableStream.getLocked, null, .{});\n};\n\npub fn registerTypes() []const type {\n    return &.{\n        WritableStream,\n    };\n}\n"
  },
  {
    "path": "src/browser/webapi/streams/WritableStreamDefaultController.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst WritableStream = @import(\"WritableStream.zig\");\n\nconst WritableStreamDefaultController = @This();\n\n_stream: *WritableStream,\n\npub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultController {\n    return page._factory.create(WritableStreamDefaultController{\n        ._stream = stream,\n    });\n}\n\npub fn doError(self: *WritableStreamDefaultController, reason: []const u8) void {\n    if (self._stream._state != .writable) return;\n    self._stream._state = .errored;\n    self._stream._stored_error = reason;\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(WritableStreamDefaultController);\n\n    pub const Meta = struct {\n        pub const name = \"WritableStreamDefaultController\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const @\"error\" = bridge.function(WritableStreamDefaultController.doError, .{});\n};\n"
  },
  {
    "path": "src/browser/webapi/streams/WritableStreamDefaultWriter.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst js = @import(\"../../js/js.zig\");\nconst Page = @import(\"../../Page.zig\");\nconst WritableStream = @import(\"WritableStream.zig\");\n\nconst WritableStreamDefaultWriter = @This();\n\n_stream: ?*WritableStream,\n\npub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultWriter {\n    return page._factory.create(WritableStreamDefaultWriter{\n        ._stream = stream,\n    });\n}\n\npub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {\n    const stream = self._stream orelse {\n        return page.js.local.?.rejectPromise(\"Writer has been released\");\n    };\n\n    if (stream._state != .writable) {\n        return page.js.local.?.rejectPromise(\"Stream is not writable\");\n    }\n\n    try stream.writeChunk(chunk, page);\n\n    return page.js.local.?.resolvePromise(.{});\n}\n\npub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {\n    const stream = self._stream orelse {\n        return page.js.local.?.rejectPromise(\"Writer has been released\");\n    };\n\n    if (stream._state != .writable) {\n        return page.js.local.?.rejectPromise(\"Stream is not writable\");\n    }\n\n    try stream.closeStream(page);\n\n    return page.js.local.?.resolvePromise(.{});\n}\n\npub fn releaseLock(self: *WritableStreamDefaultWriter) void {\n    if (self._stream) |stream| {\n        stream._writer = null;\n        self._stream = null;\n    }\n}\n\npub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {\n    const stream = self._stream orelse {\n        return page.js.local.?.rejectPromise(\"Writer has been released\");\n    };\n\n    if (stream._state == .closed) {\n        return page.js.local.?.resolvePromise(.{});\n    }\n\n    return page.js.local.?.resolvePromise(.{});\n}\n\npub fn getDesiredSize(self: *const WritableStreamDefaultWriter) ?i32 {\n    const stream = self._stream orelse return null;\n    return switch (stream._state) {\n        .writable => 1,\n        .closed => 0,\n        .errored => null,\n    };\n}\n\npub fn getReady(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {\n    _ = self;\n    return page.js.local.?.resolvePromise(.{});\n}\n\npub const JsApi = struct {\n    pub const bridge = js.Bridge(WritableStreamDefaultWriter);\n\n    pub const Meta = struct {\n        pub const name = \"WritableStreamDefaultWriter\";\n        pub const prototype_chain = bridge.prototypeChain();\n        pub var class_id: bridge.ClassId = undefined;\n    };\n\n    pub const write = bridge.function(WritableStreamDefaultWriter.write, .{});\n    pub const close = bridge.function(WritableStreamDefaultWriter.close, .{});\n    pub const releaseLock = bridge.function(WritableStreamDefaultWriter.releaseLock, .{});\n    pub const closed = bridge.accessor(WritableStreamDefaultWriter.getClosed, null, .{});\n    pub const ready = bridge.accessor(WritableStreamDefaultWriter.getReady, null, .{});\n    pub const desiredSize = bridge.accessor(WritableStreamDefaultWriter.getDesiredSize, null, .{});\n};\n"
  },
  {
    "path": "src/cdp/AXNode.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst jsonStringify = std.json.Stringify;\n\nconst log = @import(\"../log.zig\");\nconst Page = @import(\"../browser/Page.zig\");\nconst DOMNode = @import(\"../browser/webapi/Node.zig\");\n\nconst Node = @import(\"Node.zig\");\n\nconst AXNode = @This();\n\n// Need a custom writer, because we can't just serialize the node as-is.\n// Sometimes we want to serializ the node without chidren, sometimes with just\n// its direct children, and sometimes the entire tree.\n// (For now, we only support direct children)\npub const Writer = struct {\n    root: *const Node,\n    registry: *Node.Registry,\n    page: *Page,\n\n    pub const Opts = struct {};\n\n    pub fn jsonStringify(self: *const Writer, w: anytype) error{WriteFailed}!void {\n        self.toJSON(self.root, w) catch |err| {\n            // The only error our jsonStringify method can return is\n            // @TypeOf(w).Error. In other words, our code can't return its own\n            // error, we can only return a writer error. Kinda sucks.\n            log.err(.cdp, \"node toJSON stringify\", .{ .err = err });\n            return error.WriteFailed;\n        };\n    }\n\n    fn toJSON(self: *const Writer, node: *const Node, w: anytype) !void {\n        try w.beginArray();\n        const root = AXNode.fromNode(node.dom);\n        if (try self.writeNode(node.id, root, w)) {\n            try self.writeNodeChildren(root, w);\n        }\n        return w.endArray();\n    }\n\n    fn writeNodeChildren(self: *const Writer, parent: AXNode, w: anytype) !void {\n        // Add ListMarker for listitem elements\n        if (parent.dom.is(DOMNode.Element)) |parent_el| {\n            if (parent_el.getTag() == .li) {\n                try self.writeListMarker(parent.dom, w);\n            }\n        }\n\n        var it = parent.dom.childrenIterator();\n        const ignore_text = ignoreText(parent.dom);\n        while (it.next()) |dom_node| {\n            switch (dom_node._type) {\n                .cdata => {\n                    if (dom_node.is(DOMNode.CData.Text) == null) {\n                        continue;\n                    }\n                    if (ignore_text) {\n                        continue;\n                    }\n                },\n                .element => {},\n                else => continue,\n            }\n\n            const node = try self.registry.register(dom_node);\n            const axn = AXNode.fromNode(node.dom);\n            if (try self.writeNode(node.id, axn, w)) {\n                try self.writeNodeChildren(axn, w);\n            }\n        }\n    }\n\n    fn writeListMarker(self: *const Writer, li_node: *DOMNode, w: anytype) !void {\n        // Find the parent list element\n        const parent = li_node._parent orelse return;\n        const parent_el = parent.is(DOMNode.Element) orelse return;\n        const list_type = parent_el.getTag();\n\n        // Only create markers for actual list elements\n        switch (list_type) {\n            .ul, .ol, .menu => {},\n            else => return,\n        }\n\n        // Write the ListMarker node\n        try w.beginObject();\n\n        // Use the next available ID for the marker\n        try w.objectField(\"nodeId\");\n        const marker_id = self.registry.node_id;\n        self.registry.node_id += 1;\n        try w.write(marker_id);\n\n        try w.objectField(\"backendDOMNodeId\");\n        try w.write(marker_id);\n\n        try w.objectField(\"role\");\n        try self.writeAXValue(.{ .role = \"ListMarker\" }, w);\n\n        try w.objectField(\"ignored\");\n        try w.write(false);\n\n        try w.objectField(\"name\");\n        try w.beginObject();\n        try w.objectField(\"type\");\n        try w.write(\"computedString\");\n        try w.objectField(\"value\");\n\n        // Write marker text directly based on list type\n        switch (list_type) {\n            .ul, .menu => try w.write(\"• \"),\n            .ol => {\n                // Calculate the list item number by counting preceding li siblings\n                var count: usize = 1;\n                var it = parent.childrenIterator();\n                while (it.next()) |child| {\n                    if (child == li_node) break;\n                    if (child.is(DOMNode.Element.Html) == null) continue;\n                    const child_el = child.as(DOMNode.Element);\n                    if (child_el.getTag() == .li) count += 1;\n                }\n\n                // Sanity check: lists with >9999 items are unrealistic\n                if (count > 9999) return error.ListTooLong;\n\n                // Use a small stack buffer to format the number (max \"9999. \" = 6 chars)\n                var buf: [6]u8 = undefined;\n                const marker_text = try std.fmt.bufPrint(&buf, \"{d}. \", .{count});\n                try w.write(marker_text);\n            },\n            else => unreachable,\n        }\n\n        try w.objectField(\"sources\");\n        try w.beginArray();\n        try w.beginObject();\n        try w.objectField(\"type\");\n        try w.write(\"contents\");\n        try w.endObject();\n        try w.endArray();\n        try w.endObject();\n\n        try w.objectField(\"properties\");\n        try w.beginArray();\n        try w.endArray();\n\n        // Get the parent node ID for the parentId field\n        const li_registered = try self.registry.register(li_node);\n        try w.objectField(\"parentId\");\n        try w.write(li_registered.id);\n\n        try w.objectField(\"childIds\");\n        try w.beginArray();\n        try w.endArray();\n\n        try w.endObject();\n    }\n\n    const AXValue = union(enum) {\n        role: []const u8,\n        string: []const u8,\n        computedString: []const u8,\n        integer: usize,\n        boolean: bool,\n        booleanOrUndefined: bool,\n        token: []const u8,\n        // TODO not implemented:\n        // tristate, idrefList, node, nodeList, number, tokenList,\n        // domRelation, internalRole, valueUndefined,\n    };\n\n    fn writeAXSource(_: *const Writer, source: AXSource, w: anytype) !void {\n        try w.objectField(\"sources\");\n        try w.beginArray();\n        try w.beginObject();\n\n        // attribute, implicit, style, contents, placeholder, relatedElement\n        const source_type = switch (source) {\n            .aria_labelledby => blk: {\n                try w.objectField(\"attribute\");\n                try w.write(@tagName(source));\n                break :blk \"relatedElement\";\n            },\n            .aria_label, .alt, .title, .placeholder, .value => blk: {\n                // No sure if it's correct for .value case.\n                try w.objectField(\"attribute\");\n                try w.write(@tagName(source));\n                break :blk \"attribute\";\n            },\n            // Chrome sends the content AXValue *again* in the source.\n            // But It seems useless to me.\n            //\n            // w.objectField(\"value\");\n            // self.writeAXValue(.{ .type = .computedString, .value = value.value }, w);\n            .contents => \"contents\",\n            .label_element, .label_wrap => \"TODO\", // TODO\n        };\n        try w.objectField(\"type\");\n        try w.write(source_type);\n\n        try w.endObject();\n        try w.endArray();\n    }\n\n    fn writeAXValue(_: *const Writer, value: AXValue, w: anytype) !void {\n        try w.beginObject();\n        try w.objectField(\"type\");\n        try w.write(@tagName(std.meta.activeTag(value)));\n\n        try w.objectField(\"value\");\n        switch (value) {\n            .integer => |v| {\n                // CDP spec requires integer values to be serialized as strings.\n                // 20 bytes is enough for the decimal representation of a 64-bit integer.\n                var buf: [20]u8 = undefined;\n                const s = try std.fmt.bufPrint(&buf, \"{d}\", .{v});\n                try w.write(s);\n            },\n            inline else => |v| try w.write(v),\n        }\n\n        try w.endObject();\n    }\n\n    const AXProperty = struct {\n        // zig fmt: off\n        name: enum(u8) {\n            actions, busy, disabled, editable, focusable, focused, hidden,\n            hiddenRoot, invalid, keyshortcuts, settable, roledescription, live,\n            atomic, relevant, root, autocomplete, hasPopup, level,\n            multiselectable, orientation, multiline, readonly, required,\n            valuemin, valuemax, valuetext, checked, expanded, modal, pressed,\n            selected, activedescendant, controls, describedby, details,\n            errormessage, flowto, labelledby, owns, url,\n            activeFullscreenElement, activeModalDialog, activeAriaModalDialog,\n            ariaHiddenElement, ariaHiddenSubtree, emptyAlt, emptyText,\n            inertElement, inertSubtree, labelContainer, labelFor, notRendered,\n            notVisible, presentationalRole, probablyPresentational,\n            inactiveCarouselTabContent, uninteresting,\n        },\n        // zig fmt: on\n        value: AXValue,\n    };\n\n    fn writeAXProperties(self: *const Writer, axnode: AXNode, w: anytype) !void {\n        const dom_node = axnode.dom;\n        const page = self.page;\n        switch (dom_node._type) {\n            .document => |document| {\n                const uri = document.getURL(page);\n                try self.writeAXProperty(.{ .name = .url, .value = .{ .string = uri } }, w);\n                try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                try self.writeAXProperty(.{ .name = .focused, .value = .{ .booleanOrUndefined = true } }, w);\n                return;\n            },\n            .cdata => return,\n            .element => |el| switch (el.getTag()) {\n                .h1 => try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = 1 } }, w),\n                .h2 => try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = 2 } }, w),\n                .h3 => try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = 3 } }, w),\n                .h4 => try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = 4 } }, w),\n                .h5 => try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = 5 } }, w),\n                .h6 => try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = 6 } }, w),\n                .img => {\n                    const img = el.as(DOMNode.Element.Html.Image);\n                    const uri = try img.getSrc(self.page);\n                    if (uri.len == 0) return;\n                    try self.writeAXProperty(.{ .name = .url, .value = .{ .string = uri } }, w);\n                },\n                .anchor => {\n                    const a = el.as(DOMNode.Element.Html.Anchor);\n                    const uri = try a.getHref(self.page);\n                    if (uri.len == 0) return;\n                    try self.writeAXProperty(.{ .name = .url, .value = .{ .string = uri } }, w);\n                    try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                },\n                .input => {\n                    const input = el.as(DOMNode.Element.Html.Input);\n                    const is_disabled = el.hasAttributeSafe(comptime .wrap(\"disabled\"));\n\n                    switch (input._input_type) {\n                        .text, .email, .tel, .url, .search, .password, .number => {\n                            if (is_disabled) {\n                                try self.writeAXProperty(.{ .name = .disabled, .value = .{ .boolean = true } }, w);\n                            }\n                            try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = \"false\" } }, w);\n                            if (!is_disabled) {\n                                try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                            }\n                            try self.writeAXProperty(.{ .name = .editable, .value = .{ .token = \"plaintext\" } }, w);\n                            if (!is_disabled) {\n                                try self.writeAXProperty(.{ .name = .settable, .value = .{ .booleanOrUndefined = true } }, w);\n                            }\n                            try self.writeAXProperty(.{ .name = .multiline, .value = .{ .boolean = false } }, w);\n                            try self.writeAXProperty(.{ .name = .readonly, .value = .{ .boolean = el.hasAttributeSafe(comptime .wrap(\"readonly\")) } }, w);\n                            try self.writeAXProperty(.{ .name = .required, .value = .{ .boolean = el.hasAttributeSafe(comptime .wrap(\"required\")) } }, w);\n                        },\n                        .button, .submit, .reset, .image => {\n                            try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = \"false\" } }, w);\n                            if (!is_disabled) {\n                                try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                            }\n                        },\n                        .checkbox, .radio => {\n                            try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = \"false\" } }, w);\n                            if (!is_disabled) {\n                                try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                            }\n                            const is_checked = el.hasAttributeSafe(comptime .wrap(\"checked\"));\n                            try self.writeAXProperty(.{ .name = .checked, .value = .{ .token = if (is_checked) \"true\" else \"false\" } }, w);\n                        },\n                        else => {},\n                    }\n                },\n                .textarea => {\n                    const is_disabled = el.hasAttributeSafe(comptime .wrap(\"disabled\"));\n\n                    try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = \"false\" } }, w);\n                    if (!is_disabled) {\n                        try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                    }\n                    try self.writeAXProperty(.{ .name = .editable, .value = .{ .token = \"plaintext\" } }, w);\n                    if (!is_disabled) {\n                        try self.writeAXProperty(.{ .name = .settable, .value = .{ .booleanOrUndefined = true } }, w);\n                    }\n                    try self.writeAXProperty(.{ .name = .multiline, .value = .{ .boolean = true } }, w);\n                    try self.writeAXProperty(.{ .name = .readonly, .value = .{ .boolean = el.hasAttributeSafe(comptime .wrap(\"readonly\")) } }, w);\n                    try self.writeAXProperty(.{ .name = .required, .value = .{ .boolean = el.hasAttributeSafe(comptime .wrap(\"required\")) } }, w);\n                },\n                .select => {\n                    const is_disabled = el.hasAttributeSafe(comptime .wrap(\"disabled\"));\n\n                    try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = \"false\" } }, w);\n                    if (!is_disabled) {\n                        try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                    }\n                    try self.writeAXProperty(.{ .name = .hasPopup, .value = .{ .token = \"menu\" } }, w);\n                    try self.writeAXProperty(.{ .name = .expanded, .value = .{ .booleanOrUndefined = false } }, w);\n                },\n                .option => {\n                    const option = el.as(DOMNode.Element.Html.Option);\n                    try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n\n                    // Check if this option is selected by examining the parent select\n                    const is_selected = blk: {\n                        // First check if explicitly selected\n                        if (option.getSelected()) break :blk true;\n\n                        // Check if implicitly selected (first enabled option in select with no explicit selection)\n                        const parent = dom_node._parent orelse break :blk false;\n                        const parent_el = parent.as(DOMNode.Element);\n                        if (parent_el.getTag() != .select) break :blk false;\n\n                        const select = parent_el.as(DOMNode.Element.Html.Select);\n                        const selected_idx = select.getSelectedIndex();\n\n                        // Find this option's index\n                        var idx: i32 = 0;\n                        var it = parent.childrenIterator();\n                        while (it.next()) |child| {\n                            if (child.is(DOMNode.Element.Html.Option) == null) continue;\n                            if (child == dom_node) {\n                                break :blk idx == selected_idx;\n                            }\n                            idx += 1;\n                        }\n                        break :blk false;\n                    };\n\n                    if (is_selected) {\n                        try self.writeAXProperty(.{ .name = .selected, .value = .{ .booleanOrUndefined = true } }, w);\n                    }\n                },\n                .button => {\n                    const is_disabled = el.hasAttributeSafe(comptime .wrap(\"disabled\"));\n                    try self.writeAXProperty(.{ .name = .invalid, .value = .{ .token = \"false\" } }, w);\n                    if (!is_disabled) {\n                        try self.writeAXProperty(.{ .name = .focusable, .value = .{ .booleanOrUndefined = true } }, w);\n                    }\n                },\n                .hr => {\n                    try self.writeAXProperty(.{ .name = .settable, .value = .{ .booleanOrUndefined = true } }, w);\n                    try self.writeAXProperty(.{ .name = .orientation, .value = .{ .token = \"horizontal\" } }, w);\n                },\n                .li => {\n                    // Calculate level by counting list ancestors (ul, ol, menu)\n                    var level: usize = 0;\n                    var current = dom_node._parent;\n                    while (current) |node| {\n                        if (node.is(DOMNode.Element) == null) {\n                            current = node._parent;\n                            continue;\n                        }\n                        const current_el = node.as(DOMNode.Element);\n                        switch (current_el.getTag()) {\n                            .ul, .ol, .menu => level += 1,\n                            else => {},\n                        }\n                        current = node._parent;\n                    }\n                    try self.writeAXProperty(.{ .name = .level, .value = .{ .integer = level } }, w);\n                },\n                else => {},\n            },\n            else => |tag| {\n                log.debug(.cdp, \"invalid tag\", .{ .tag = tag });\n                return error.InvalidTag;\n            },\n        }\n    }\n\n    fn writeAXProperty(self: *const Writer, value: AXProperty, w: anytype) !void {\n        try w.beginObject();\n        try w.objectField(\"name\");\n        try w.write(@tagName(value.name));\n        try w.objectField(\"value\");\n        try self.writeAXValue(value.value, w);\n        try w.endObject();\n    }\n\n    // write a node. returns true if children must be written.\n    fn writeNode(self: *const Writer, id: u32, axn: AXNode, w: anytype) !bool {\n        // ignore empty texts\n        try w.beginObject();\n\n        try w.objectField(\"nodeId\");\n        try w.write(id);\n\n        try w.objectField(\"backendDOMNodeId\");\n        try w.write(id);\n\n        try w.objectField(\"role\");\n        try self.writeAXValue(.{ .role = try axn.getRole() }, w);\n\n        const ignore = axn.isIgnore(self.page);\n        try w.objectField(\"ignored\");\n        try w.write(ignore);\n\n        if (ignore) {\n            // Ignore reasons\n            try w.objectField(\"ignoredReasons\");\n            try w.beginArray();\n            try w.beginObject();\n            try w.objectField(\"name\");\n            try w.write(\"uninteresting\");\n            try w.objectField(\"value\");\n            try self.writeAXValue(.{ .boolean = true }, w);\n            try w.endObject();\n            try w.endArray();\n        } else {\n            // Name\n            try w.objectField(\"name\");\n            try w.beginObject();\n            try w.objectField(\"type\");\n            try w.write(@tagName(.computedString));\n            try w.objectField(\"value\");\n            const source = try axn.writeName(w, self.page);\n            if (source) |s| {\n                try self.writeAXSource(s, w);\n            }\n            try w.endObject();\n\n            // Value (for form controls)\n            try self.writeNodeValue(axn, w);\n\n            // Properties\n            try w.objectField(\"properties\");\n            try w.beginArray();\n            try self.writeAXProperties(axn, w);\n            try w.endArray();\n        }\n\n        const n = axn.dom;\n\n        // Parent\n        if (n._parent) |p| {\n            const parent_node = try self.registry.register(p);\n            try w.objectField(\"parentId\");\n            try w.write(parent_node.id);\n        }\n\n        // Children\n        const write_children = axn.ignoreChildren() == false;\n        const skip_text = ignoreText(axn.dom);\n\n        try w.objectField(\"childIds\");\n        try w.beginArray();\n        if (write_children) {\n            var registry = self.registry;\n            var it = n.childrenIterator();\n            while (it.next()) |child| {\n                // ignore non-elements or text.\n                if (child.is(DOMNode.Element.Html) == null and (child.is(DOMNode.CData.Text) == null or skip_text)) {\n                    continue;\n                }\n\n                const child_node = try registry.register(child);\n                try w.write(child_node.id);\n            }\n        }\n        try w.endArray();\n\n        try w.endObject();\n\n        return write_children;\n    }\n\n    fn writeNodeValue(self: *const Writer, axnode: AXNode, w: anytype) !void {\n        const node = axnode.dom;\n\n        if (node.is(DOMNode.Element.Html) == null) {\n            return;\n        }\n\n        const el = node.as(DOMNode.Element);\n\n        const value: ?[]const u8 = switch (el.getTag()) {\n            .input => blk: {\n                const input = el.as(DOMNode.Element.Html.Input);\n                const val = input.getValue();\n                if (val.len == 0) break :blk null;\n                break :blk val;\n            },\n            .textarea => blk: {\n                const textarea = el.as(DOMNode.Element.Html.TextArea);\n                const val = textarea.getValue();\n                if (val.len == 0) break :blk null;\n                break :blk val;\n            },\n            .select => blk: {\n                const select = el.as(DOMNode.Element.Html.Select);\n                const val = select.getValue(self.page);\n                if (val.len == 0) break :blk null;\n                break :blk val;\n            },\n            else => null,\n        };\n\n        if (value) |val| {\n            try w.objectField(\"value\");\n            try self.writeAXValue(.{ .string = val }, w);\n        }\n    }\n};\n\npub const AXRole = enum(u8) {\n    // zig fmt: off\n    none, article, banner, blockquote, button, caption, cell, checkbox, code, color,\n    columnheader, combobox, complementary, contentinfo, date, definition, deletion,\n    dialog, document, emphasis, figure, file, form, group, heading, image, insertion,\n    link, list, listbox, listitem, main, marquee, menuitem, meter, month, navigation, option,\n    paragraph, presentation, progressbar, radio, region, row, rowgroup,\n    rowheader, searchbox, separator, slider, spinbutton, status, strong,\n    subscript, superscript, @\"switch\", table, term, textbox, time, RootWebArea, LineBreak,\n    StaticText,\n    // zig fmt: on\n\n    fn fromNode(node: *DOMNode) !AXRole {\n        return switch (node._type) {\n            .document => return .RootWebArea, // Chrome specific.\n            .cdata => |cd| {\n                if (cd.is(DOMNode.CData.Text) == null) {\n                    log.debug(.cdp, \"invalid tag\", .{ .tag = cd });\n                    return error.InvalidTag;\n                }\n\n                return .StaticText;\n            },\n            .element => |el| switch (el.getTag()) {\n                // Navigation & Structure\n                .nav => .navigation,\n                .main => .main,\n                .aside => .complementary,\n                // TODO conditions:\n                // .banner Not descendant of article, aside, main, nav, section\n                // (none) When descendant of article, aside, main, nav, section\n                .header => .banner,\n                // TODO conditions:\n                // contentinfo Not descendant of article, aside, main, nav, section\n                // (none)  When descendant of article, aside, main, nav, section\n                .footer => .contentinfo,\n                // TODO conditions:\n                // region Has accessible name (aria-label, aria-labelledby, or title) |\n                // (none) No accessible name                                          |\n                .section => .region,\n                .article, .hgroup => .article,\n                .address => .group,\n\n                // Headings\n                .h1, .h2, .h3, .h4, .h5, .h6 => .heading,\n                .ul, .ol, .menu => .list,\n                .li => .listitem,\n                .dt => .term,\n                .dd => .definition,\n\n                // Forms & Inputs\n                // TODO conditions:\n                //  form  Has accessible name\n                //  (none) No accessible name\n                .form => .form,\n                .input => {\n                    const input = el.as(DOMNode.Element.Html.Input);\n                    return switch (input._input_type) {\n                        .tel, .url, .email, .text => .textbox,\n                        .image, .reset, .button, .submit => .button,\n                        .radio => .radio,\n                        .range => .slider,\n                        .number => .spinbutton,\n                        .search => .searchbox,\n                        .checkbox => .checkbox,\n                        .color => .color,\n                        .date => .date,\n                        .file => .file,\n                        .month => .month,\n                        .@\"datetime-local\", .week, .time => .combobox,\n                        // zig fmt: off\n                        .password, .hidden => .none,\n                        // zig fmt: on\n                    };\n                },\n                .textarea => .textbox,\n                .select => {\n                    if (el.getAttributeSafe(comptime .wrap(\"multiple\")) != null) {\n                        return .listbox;\n                    }\n                    if (el.getAttributeSafe(comptime .wrap(\"size\"))) |size| {\n                        if (!std.ascii.eqlIgnoreCase(size, \"1\")) {\n                            return .listbox;\n                        }\n                    }\n                    return .combobox;\n                },\n                .option => .option,\n                .optgroup, .fieldset => .group,\n                .button => .button,\n                .output => .status,\n                .progress => .progressbar,\n                .meter => .meter,\n                .datalist => .listbox,\n\n                // Interactive Elements\n                .anchor, .area => {\n                    if (el.getAttributeSafe(comptime .wrap(\"href\")) == null) {\n                        return .none;\n                    }\n\n                    return .link;\n                },\n                .details => .group,\n                .summary => .button,\n                .dialog => .dialog,\n\n                // Media\n                .img => .image,\n                .figure => .figure,\n\n                // Tables\n                .table => .table,\n                .caption => .caption,\n                .thead, .tbody, .tfoot => .rowgroup,\n                .tr => .row,\n                .th => {\n                    if (el.getAttributeSafe(comptime .wrap(\"scope\"))) |scope| {\n                        if (std.ascii.eqlIgnoreCase(scope, \"row\")) {\n                            return .rowheader;\n                        }\n                    }\n                    return .columnheader;\n                },\n                .td => .cell,\n\n                // Text & Semantics\n                .p => .paragraph,\n                .hr => .separator,\n                .blockquote => .blockquote,\n                .code => .code,\n                .em => .emphasis,\n                .strong => .strong,\n                .s, .del => .deletion,\n                .ins => .insertion,\n                .sub => .subscript,\n                .sup => .superscript,\n                .time => .time,\n                .dfn => .term,\n\n                // Document Structure\n                .html => .none,\n                .body => .none,\n\n                // Deprecated/Obsolete Elements\n                .marquee => .marquee,\n\n                .br => .LineBreak,\n\n                else => .none,\n            },\n            else => |tag| {\n                log.debug(.cdp, \"invalid tag\", .{ .tag = tag });\n                return error.InvalidTag;\n            },\n        };\n    }\n};\n\ndom: *DOMNode,\nrole_attr: ?[]const u8,\n\npub fn fromNode(dom: *DOMNode) AXNode {\n    return .{\n        .dom = dom,\n        .role_attr = blk: {\n            if (dom.is(DOMNode.Element.Html) == null) {\n                break :blk null;\n            }\n            const elt = dom.as(DOMNode.Element);\n            break :blk elt.getAttributeSafe(comptime .wrap(\"role\"));\n        },\n    };\n}\n\nconst AXSource = enum(u8) {\n    aria_labelledby,\n    aria_label,\n    label_element, // <label for=\"...\">\n    label_wrap, // <label><input></label>\n    alt, // img alt attribute\n    title, // title attribute\n    placeholder, // input placeholder\n    contents, // text content\n    value, // input value\n};\n\npub fn getName(self: AXNode, page: *Page, allocator: std.mem.Allocator) !?[]const u8 {\n    var aw: std.Io.Writer.Allocating = .init(allocator);\n    defer aw.deinit();\n\n    // writeName expects a std.json.Stringify instance.\n    const TextCaptureWriter = struct {\n        aw: *std.Io.Writer.Allocating,\n        writer: *std.Io.Writer,\n\n        pub fn write(w: @This(), val: anytype) !void {\n            const T = @TypeOf(val);\n            if (T == []const u8 or T == [:0]const u8 or T == *const [val.len]u8) {\n                try w.aw.writer.writeAll(val);\n            } else if (comptime std.meta.hasMethod(T, \"format\")) {\n                try std.fmt.format(w.aw.writer, \"{s}\", .{val});\n            } else {\n                // Ignore unexpected types (e.g. booleans) to avoid garbage output\n            }\n        }\n\n        // Mock JSON Stringifier lifecycle methods\n        pub fn beginWriteRaw(_: @This()) !void {}\n        pub fn endWriteRaw(_: @This()) void {}\n    };\n\n    const w: TextCaptureWriter = .{ .aw = &aw, .writer = &aw.writer };\n\n    const source = try self.writeName(w, page);\n    if (source != null) {\n        // Remove literal quotes inserted by writeString.\n        var raw_text = std.mem.trim(u8, aw.written(), \"\\\"\");\n        raw_text = std.mem.trim(u8, raw_text, &std.ascii.whitespace);\n        return try allocator.dupe(u8, raw_text);\n    }\n\n    return null;\n}\n\nfn writeName(axnode: AXNode, w: anytype, page: *Page) !?AXSource {\n    const node = axnode.dom;\n\n    return switch (node._type) {\n        .document => |doc| switch (doc._type) {\n            .html => |doc_html| {\n                try w.write(try doc_html.getTitle(page));\n                return .title;\n            },\n            else => null,\n        },\n        .cdata => |cd| switch (cd._type) {\n            .text => |*text| {\n                try writeString(text.getWholeText(), w);\n                return .contents;\n            },\n            else => null,\n        },\n        .element => |el| {\n            // Handle aria-labelledby attribute (highest priority)\n            if (el.getAttributeSafe(.wrap(\"aria-labelledby\"))) |labelledby| {\n                // Get the document to look up elements by ID\n                const doc = node.ownerDocument(page) orelse return null;\n\n                // Parse space-separated list of IDs and concatenate their text content\n                var it = std.mem.splitScalar(u8, labelledby, ' ');\n                var has_content = false;\n\n                var buf = std.Io.Writer.Allocating.init(page.call_arena);\n                while (it.next()) |id| {\n                    const trimmed_id = std.mem.trim(u8, id, &std.ascii.whitespace);\n                    if (trimmed_id.len == 0) continue;\n\n                    if (doc.getElementById(trimmed_id, page)) |referenced_el| {\n                        // Get the text content of the referenced element\n                        try referenced_el.getInnerText(&buf.writer);\n                        try buf.writer.writeByte(' ');\n                        has_content = true;\n                    }\n                }\n\n                if (has_content) {\n                    try writeString(buf.written(), w);\n                    return .aria_labelledby;\n                }\n            }\n\n            if (el.getAttributeSafe(comptime .wrap(\"aria-label\"))) |aria_label| {\n                try w.write(aria_label);\n                return .aria_label;\n            }\n\n            if (el.getAttributeSafe(comptime .wrap(\"alt\"))) |alt| {\n                try w.write(alt);\n                return .alt;\n            }\n\n            switch (el.getTag()) {\n                .br => {\n                    try writeString(\"\\n\", w);\n                    return .contents;\n                },\n                .input => {\n                    const input = el.as(DOMNode.Element.Html.Input);\n                    switch (input._input_type) {\n                        .reset, .button, .submit => |t| {\n                            const v = input.getValue();\n                            if (v.len > 0) {\n                                try w.write(input.getValue());\n                            } else {\n                                try w.write(@tagName(t));\n                            }\n\n                            return .value;\n                        },\n                        else => {},\n                    }\n                    // TODO Check for <label> with matching \"for\" attribute\n                    // TODO Check if input is wrapped in a <label>\n                },\n                // zig fmt: off\n                .textarea, .select, .img, .audio, .video, .iframe, .embed,\n                .object, .progress, .meter, .main, .nav, .aside, .header,\n                .footer, .form, .section, .article, .ul, .ol, .dl, .menu,\n                .thead, .tbody, .tfoot, .tr, .td, .div, .span, .p, .details, .li,\n                .style, .script, .html, .body,\n                // zig fmt: on\n                => {},\n                else => {\n                    // write text content if exists.\n                    var buf: std.Io.Writer.Allocating = .init(page.call_arena);\n                    try writeAccessibleNameFallback(node, &buf.writer, page);\n                    if (buf.written().len > 0) {\n                        try writeString(buf.written(), w);\n                        return .contents;\n                    }\n                },\n            }\n\n            if (el.getAttributeSafe(comptime .wrap(\"title\"))) |title| {\n                try w.write(title);\n                return .title;\n            }\n\n            if (el.getAttributeSafe(comptime .wrap(\"placeholder\"))) |placeholder| {\n                try w.write(placeholder);\n                return .placeholder;\n            }\n\n            try w.write(\"\");\n            return null;\n        },\n        else => {\n            try w.write(\"\");\n            return null;\n        },\n    };\n}\n\nfn writeAccessibleNameFallback(node: *DOMNode, writer: *std.Io.Writer, page: *Page) !void {\n    var it = node.childrenIterator();\n    while (it.next()) |child| {\n        switch (child._type) {\n            .cdata => |cd| switch (cd._type) {\n                .text => |*text| {\n                    const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace);\n                    if (content.len > 0) {\n                        try writer.writeAll(content);\n                        try writer.writeByte(' ');\n                    }\n                },\n                else => {},\n            },\n            .element => |el| {\n                if (el.getTag() == .img) {\n                    if (el.getAttributeSafe(.wrap(\"alt\"))) |alt| {\n                        try writer.writeAll(alt);\n                        try writer.writeByte(' ');\n                    }\n                } else if (el.getTag() == .svg) {\n                    // Try to find a <title> inside SVG\n                    var sit = child.childrenIterator();\n                    while (sit.next()) |s_child| {\n                        if (s_child.is(DOMNode.Element)) |s_el| {\n                            if (std.mem.eql(u8, s_el.getTagNameLower(), \"title\")) {\n                                try writeAccessibleNameFallback(s_child, writer, page);\n                                try writer.writeByte(' ');\n                            }\n                        }\n                    }\n                } else {\n                    if (!el.getTag().isMetadata()) {\n                        try writeAccessibleNameFallback(child, writer, page);\n                    }\n                }\n            },\n            else => {},\n        }\n    }\n}\n\nfn isHidden(elt: *DOMNode.Element) bool {\n    if (elt.getAttributeSafe(comptime .wrap(\"aria-hidden\"))) |value| {\n        if (std.mem.eql(u8, value, \"true\")) {\n            return true;\n        }\n    }\n\n    if (elt.hasAttributeSafe(comptime .wrap(\"hidden\"))) {\n        return true;\n    }\n\n    if (elt.hasAttributeSafe(comptime .wrap(\"inert\"))) {\n        return true;\n    }\n\n    // TODO Check if aria-hidden ancestor exists\n    // TODO Check CSS visibility (if you have access to computed styles)\n\n    return false;\n}\n\nfn ignoreText(node: *DOMNode) bool {\n    if (node.is(DOMNode.Element.Html) == null) {\n        return true;\n    }\n\n    const elt = node.as(DOMNode.Element);\n    // Only ignore text for structural/container elements that typically\n    // don't have meaningful direct text content\n    return switch (elt.getTag()) {\n        // zig fmt: off\n        // Structural containers\n        .html, .body, .head,\n        // Lists (text is in li elements, not in ul/ol)\n        .ul, .ol, .menu,\n        // Tables (text is in cells, not in table/tbody/thead/tfoot/tr)\n        .table, .thead, .tbody, .tfoot, .tr,\n        // Form containers\n        .form, .fieldset, .datalist,\n        // Grouping elements\n        .details, .figure,\n        // Other containers\n        .select, .optgroup, .colgroup, .script,\n        => true,\n        // zig fmt: on\n        // All other elements should include their text content\n        else => false,\n    };\n}\n\nfn ignoreChildren(self: AXNode) bool {\n    const node = self.dom;\n    if (node.is(DOMNode.Element.Html) == null) {\n        return false;\n    }\n\n    const elt = node.as(DOMNode.Element);\n    return switch (elt.getTag()) {\n        .head, .script, .style => true,\n        else => false,\n    };\n}\n\nfn isIgnore(self: AXNode, page: *Page) bool {\n    const node = self.dom;\n    const role_attr = self.role_attr;\n\n    // Don't ignore non-Element node: CData, Document...\n    const elt = node.is(DOMNode.Element) orelse return false;\n    // Ignore non-HTML elements: svg...\n    if (elt._type != .html) {\n        return true;\n    }\n\n    const tag = elt.getTag();\n    switch (tag) {\n        // zig fmt: off\n        .script, .style, .meta, .link, .title, .base, .head, .noscript,\n        .template, .param, .source, .track, .datalist, .col, .colgroup, .html,\n        .body\n        => return true,\n        // zig fmt: on\n        .img => {\n            // Check for empty decorative images\n            const alt_ = elt.getAttributeSafe(comptime .wrap(\"alt\"));\n            if (alt_ == null or alt_.?.len == 0) {\n                return true;\n            }\n        },\n        .input => {\n            // Check for hidden inputs\n            const input = elt.as(DOMNode.Element.Html.Input);\n            if (input._input_type == .hidden) {\n                return true;\n            }\n        },\n        else => {},\n    }\n\n    if (role_attr) |role| {\n        if (std.ascii.eqlIgnoreCase(role, \"none\") or std.ascii.eqlIgnoreCase(role, \"presentation\")) {\n            return true;\n        }\n    }\n\n    if (isHidden(elt)) {\n        return true;\n    }\n\n    // Generic containers with no semantic value\n    if (tag == .div or tag == .span) {\n        const has_role = elt.hasAttributeSafe(comptime .wrap(\"role\"));\n        const has_aria_label = elt.hasAttributeSafe(comptime .wrap(\"aria-label\"));\n        const has_aria_labelledby = elt.hasAttributeSafe(.wrap(\"aria-labelledby\"));\n\n        if (!has_role and !has_aria_label and !has_aria_labelledby) {\n            // Check if it has any non-ignored children\n            var it = node.childrenIterator();\n            while (it.next()) |child| {\n                const axn = AXNode.fromNode(child);\n                if (!axn.isIgnore(page)) {\n                    return false;\n                }\n            }\n\n            return true;\n        }\n    }\n\n    return false;\n}\n\npub fn getRole(self: AXNode) ![]const u8 {\n    if (self.role_attr) |role_value| {\n        // TODO the role can have multiple comma separated values.\n        return role_value;\n    }\n\n    const role_implicit = try AXRole.fromNode(self.dom);\n\n    return @tagName(role_implicit);\n}\n\n// Replace successives whitespaces with one withespace.\n// Trims left and right according to the options.\n// Returns true if the string ends with a trimmed whitespace.\nfn writeString(s: []const u8, w: anytype) !void {\n    try w.beginWriteRaw();\n    try w.writer.writeByte('\\\"');\n    try stripWhitespaces(s, w.writer);\n    try w.writer.writeByte('\\\"');\n    w.endWriteRaw();\n}\n\n// string written is json encoded.\nfn stripWhitespaces(s: []const u8, writer: anytype) !void {\n    var start: usize = 0;\n    var prev_w: ?bool = null;\n    var is_w: bool = undefined;\n\n    for (s, 0..) |c, i| {\n        is_w = std.ascii.isWhitespace(c);\n\n        // Detect the first char type.\n        if (prev_w == null) {\n            prev_w = is_w;\n        }\n        // The current char is the same kind of char, the chunk continues.\n        if (prev_w.? == is_w) {\n            continue;\n        }\n\n        // Starting here, the chunk changed.\n        if (is_w) {\n            // We have a chunk of non-whitespaces, we write it as it.\n            try jsonStringify.encodeJsonStringChars(s[start..i], .{}, writer);\n        } else {\n            // We have a chunk of whitespaces, replace with one space,\n            // depending the position.\n            if (start > 0) {\n                try writer.writeByte(' ');\n            }\n        }\n        // Start the new chunk.\n        prev_w = is_w;\n        start = i;\n    }\n    // Write the reminder chunk.\n    if (!is_w) {\n        // last chunk is non whitespaces.\n        try jsonStringify.encodeJsonStringChars(s[start..], .{}, writer);\n    }\n}\n\ntest \"AXnode: stripWhitespaces\" {\n    const allocator = std.testing.allocator;\n\n    const TestCase = struct {\n        value: []const u8,\n        expected: []const u8,\n    };\n\n    const test_cases = [_]TestCase{\n        .{ .value = \"   \", .expected = \"\" },\n        .{ .value = \"   \", .expected = \"\" },\n        .{ .value = \"foo bar\", .expected = \"foo bar\" },\n        .{ .value = \"foo  bar\", .expected = \"foo bar\" },\n        .{ .value = \"  foo bar\", .expected = \"foo bar\" },\n        .{ .value = \"foo bar  \", .expected = \"foo bar\" },\n        .{ .value = \"  foo bar  \", .expected = \"foo bar\" },\n        .{ .value = \"foo\\n\\tbar\", .expected = \"foo bar\" },\n        .{ .value = \"\\tfoo bar   baz   \\t\\n yeah\\r\\n\", .expected = \"foo bar baz yeah\" },\n        // string must be json encoded.\n        .{ .value = \"\\\"foo\\\"\", .expected = \"\\\\\\\"foo\\\\\\\"\" },\n    };\n\n    var buffer = std.io.Writer.Allocating.init(allocator);\n    defer buffer.deinit();\n\n    for (test_cases) |test_case| {\n        buffer.clearRetainingCapacity();\n        try stripWhitespaces(test_case.value, &buffer.writer);\n        try std.testing.expectEqualStrings(test_case.expected, buffer.written());\n    }\n}\n\nconst testing = @import(\"testing.zig\");\ntest \"AXNode: writer\" {\n    var registry = Node.Registry.init(testing.allocator);\n    defer registry.deinit();\n\n    var page = try testing.pageTest(\"cdp/dom3.html\");\n    defer page._session.removePage();\n    var doc = page.window._document;\n\n    const node = try registry.register(doc.asNode());\n    const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{\n        .root = node,\n        .registry = &registry,\n        .page = page,\n    }, .{});\n    defer testing.allocator.free(json);\n\n    // Check that the document node is present with proper structure\n    const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{});\n    defer parsed.deinit();\n\n    const nodes = parsed.value.array.items;\n    try testing.expect(nodes.len > 0);\n\n    // First node should be the document\n    const doc_node = nodes[0].object;\n    try testing.expectEqual(1, doc_node.get(\"nodeId\").?.integer);\n    try testing.expectEqual(1, doc_node.get(\"backendDOMNodeId\").?.integer);\n    try testing.expectEqual(false, doc_node.get(\"ignored\").?.bool);\n\n    const role = doc_node.get(\"role\").?.object;\n    try testing.expectEqual(\"role\", role.get(\"type\").?.string);\n    try testing.expectEqual(\"RootWebArea\", role.get(\"value\").?.string);\n\n    const name = doc_node.get(\"name\").?.object;\n    try testing.expectEqual(\"computedString\", name.get(\"type\").?.string);\n    try testing.expectEqual(\"Test Page\", name.get(\"value\").?.string);\n\n    // Check properties array exists\n    const properties = doc_node.get(\"properties\").?.array.items;\n    try testing.expect(properties.len >= 1);\n\n    // Check childIds array exists\n    const child_ids = doc_node.get(\"childIds\").?.array.items;\n    try testing.expect(child_ids.len > 0);\n\n    // Find the h1 node and verify its level property is serialized as a string\n    for (nodes) |node_val| {\n        const obj = node_val.object;\n        const role_obj = obj.get(\"role\") orelse continue;\n        const role_val = role_obj.object.get(\"value\") orelse continue;\n        if (!std.mem.eql(u8, role_val.string, \"heading\")) continue;\n\n        const props = obj.get(\"properties\").?.array.items;\n        for (props) |prop| {\n            const prop_obj = prop.object;\n            const name_str = prop_obj.get(\"name\").?.string;\n            if (!std.mem.eql(u8, name_str, \"level\")) continue;\n            const level_value = prop_obj.get(\"value\").?.object;\n            try testing.expectEqual(\"integer\", level_value.get(\"type\").?.string);\n            // CDP spec: integer values must be serialized as strings\n            try testing.expectEqual(\"1\", level_value.get(\"value\").?.string);\n            return;\n        }\n    }\n    return error.HeadingNodeNotFound;\n}\n"
  },
  {
    "path": "src/cdp/Node.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst log = @import(\"../log.zig\");\nconst Page = @import(\"../browser/Page.zig\");\nconst DOMNode = @import(\"../browser/webapi/Node.zig\");\n\npub const Id = u32;\n\nconst Node = @This();\n\nid: Id,\ndom: *DOMNode,\nset_child_nodes_event: bool,\n\n// Whenever we send a node to the client, we register it here for future lookup.\n// We maintain a node -> id and id -> node lookup.\npub const Registry = struct {\n    node_id: u32,\n    allocator: Allocator,\n    arena: std.heap.ArenaAllocator,\n    node_pool: std.heap.MemoryPool(Node),\n    lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node),\n    lookup_by_node: std.HashMapUnmanaged(*DOMNode, *Node, NodeContext, std.hash_map.default_max_load_percentage),\n\n    pub fn init(allocator: Allocator) Registry {\n        return .{\n            .node_id = 1,\n            .lookup_by_id = .{},\n            .lookup_by_node = .{},\n            .allocator = allocator,\n            .arena = std.heap.ArenaAllocator.init(allocator),\n            .node_pool = std.heap.MemoryPool(Node).init(allocator),\n        };\n    }\n\n    pub fn deinit(self: *Registry) void {\n        const allocator = self.allocator;\n        self.lookup_by_id.deinit(allocator);\n        self.lookup_by_node.deinit(allocator);\n        self.node_pool.deinit();\n        self.arena.deinit();\n    }\n\n    pub fn reset(self: *Registry) void {\n        self.lookup_by_id.clearRetainingCapacity();\n        self.lookup_by_node.clearRetainingCapacity();\n        _ = self.arena.reset(.{ .retain_with_limit = 1024 });\n        _ = self.node_pool.reset(.{ .retain_with_limit = 1024 });\n    }\n\n    pub fn register(self: *Registry, dom_node: *DOMNode) !*Node {\n        const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, dom_node);\n        if (node_lookup_gop.found_existing) {\n            return node_lookup_gop.value_ptr.*;\n        }\n\n        // on error, we're probably going to abort the entire browser context\n        // but, just in case, let's try to keep things tidy.\n        errdefer _ = self.lookup_by_node.remove(dom_node);\n\n        const node = try self.node_pool.create();\n        errdefer self.node_pool.destroy(node);\n\n        const id = self.node_id;\n        self.node_id = id + 1;\n\n        node.* = .{\n            .id = id,\n            .dom = dom_node,\n            .set_child_nodes_event = false,\n        };\n\n        node_lookup_gop.value_ptr.* = node;\n        try self.lookup_by_id.putNoClobber(self.allocator, id, node);\n        return node;\n    }\n};\n\nconst NodeContext = struct {\n    pub fn hash(_: NodeContext, dom_node: *DOMNode) u64 {\n        return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(dom_node)));\n    }\n\n    pub fn eql(_: NodeContext, a: *DOMNode, b: *DOMNode) bool {\n        return @intFromPtr(a) == @intFromPtr(b);\n    }\n};\n\n// Searches are a 3 step process:\n// 1 - Dom.performSearch\n// 2 - Dom.getSearchResults\n// 3 - Dom.discardSearchResults\n//\n// For a given browser context, we can have multiple active searches. I.e.\n// performSearch could be called multiple times without getSearchResults or\n// discardSearchResults being called. We keep these active searches in the\n// browser context's node_search_list, which is a SearchList. Since we don't\n// expect many active searches (mostly just 1), a list is fine to scan through.\npub const Search = struct {\n    name: []const u8,\n    node_ids: []const Id,\n\n    pub const List = struct {\n        search_id: u16 = 0,\n        registry: *Registry,\n        arena: std.heap.ArenaAllocator,\n        searches: std.ArrayList(Search) = .{},\n\n        pub fn init(allocator: Allocator, registry: *Registry) List {\n            return .{\n                .registry = registry,\n                .arena = std.heap.ArenaAllocator.init(allocator),\n            };\n        }\n\n        pub fn deinit(self: *List) void {\n            self.arena.deinit();\n        }\n\n        pub fn reset(self: *List) void {\n            self.search_id = 0;\n            self.searches = .{};\n            _ = self.arena.reset(.{ .retain_with_limit = 4096 });\n        }\n\n        pub fn create(self: *List, nodes: []const *DOMNode) !Search {\n            const id = self.search_id;\n            defer self.search_id = id +% 1;\n\n            const arena = self.arena.allocator();\n\n            const name = switch (id) {\n                0 => \"0\",\n                1 => \"1\",\n                2 => \"2\",\n                3 => \"3\",\n                4 => \"4\",\n                5 => \"5\",\n                6 => \"6\",\n                7 => \"7\",\n                8 => \"8\",\n                9 => \"9\",\n                else => try std.fmt.allocPrint(arena, \"{d}\", .{id}),\n            };\n\n            var registry = self.registry;\n            const node_ids = try arena.alloc(Id, nodes.len);\n            for (nodes, node_ids) |node, *node_id| {\n                node_id.* = (try registry.register(node)).id;\n            }\n\n            const search = Search{\n                .name = name,\n                .node_ids = node_ids,\n            };\n            try self.searches.append(arena, search);\n            return search;\n        }\n\n        pub fn remove(self: *List, name: []const u8) void {\n            for (self.searches.items, 0..) |search, i| {\n                if (std.mem.eql(u8, name, search.name)) {\n                    _ = self.searches.swapRemove(i);\n                    return;\n                }\n            }\n        }\n\n        pub fn get(self: *const List, name: []const u8) ?Search {\n            for (self.searches.items) |search| {\n                if (std.mem.eql(u8, name, search.name)) {\n                    return search;\n                }\n            }\n            return null;\n        }\n    };\n};\n\n// Need a custom writer, because we can't just serialize the node as-is.\n// Sometimes we want to serializ the node without chidren, sometimes with just\n// its direct children, and sometimes the entire tree.\n// (For now, we only support direct children)\n\npub const Writer = struct {\n    depth: i32,\n    exclude_root: bool,\n    root: *const Node,\n    registry: *Registry,\n\n    pub const Opts = struct {\n        depth: i32 = 0,\n        exclude_root: bool = false,\n    };\n\n    pub fn jsonStringify(self: *const Writer, w: anytype) error{WriteFailed}!void {\n        if (self.exclude_root) {\n            _ = self.writeChildren(self.root, 1, w) catch |err| {\n                log.err(.cdp, \"node writeChildren\", .{ .err = err });\n                return error.WriteFailed;\n            };\n        } else {\n            self.toJSON(self.root, 0, w) catch |err| {\n                // The only error our jsonStringify method can return is\n                // @TypeOf(w).Error. In other words, our code can't return its own\n                // error, we can only return a writer error. Kinda sucks.\n                log.err(.cdp, \"node toJSON stringify\", .{ .err = err });\n                return error.WriteFailed;\n            };\n        }\n    }\n\n    fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void {\n        try w.beginObject();\n        try self.writeCommon(node, false, w);\n\n        try w.objectField(\"children\");\n        const child_count = try self.writeChildren(node, depth, w);\n        try w.objectField(\"childNodeCount\");\n        try w.write(child_count);\n\n        try w.endObject();\n    }\n\n    fn writeChildren(self: *const Writer, node: *const Node, depth: usize, w: anytype) anyerror!usize {\n        var count: usize = 0;\n        var it = node.dom.childrenIterator();\n\n        var registry = self.registry;\n        const full_child = self.depth < 0 or self.depth < depth;\n\n        try w.beginArray();\n        while (it.next()) |dom_child| {\n            const child_node = try registry.register(dom_child);\n            if (full_child) {\n                try self.toJSON(child_node, depth + 1, w);\n            } else {\n                try w.beginObject();\n                try self.writeCommon(child_node, true, w);\n                try w.endObject();\n            }\n            count += 1;\n        }\n        try w.endArray();\n\n        return count;\n    }\n\n    fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void {\n        try w.objectField(\"nodeId\");\n        try w.write(node.id);\n\n        try w.objectField(\"backendNodeId\");\n        try w.write(node.id);\n\n        const dom_node = node.dom;\n\n        if (dom_node._parent) |dom_parent| {\n            const parent_node = try self.registry.register(dom_parent);\n            try w.objectField(\"parentId\");\n            try w.write(parent_node.id);\n        }\n\n        if (dom_node.is(DOMNode.Element)) |element| {\n            if (element.hasAttributes()) {\n                try w.objectField(\"attributes\");\n                try w.beginArray();\n                var it = element.attributeIterator();\n                while (it.next()) |attr| {\n                    try w.write(attr._name.str());\n                    try w.write(attr._value.str());\n                }\n                try w.endArray();\n            }\n\n            try w.objectField(\"localName\");\n            try w.write(element.getLocalName());\n        } else {\n            try w.objectField(\"localName\");\n            try w.write(\"\");\n        }\n\n        try w.objectField(\"nodeType\");\n        try w.write(dom_node.getNodeType());\n\n        try w.objectField(\"nodeName\");\n        var name_buf: [Page.BUF_SIZE]u8 = undefined;\n        try w.write(dom_node.getNodeName(&name_buf));\n\n        try w.objectField(\"nodeValue\");\n        if (dom_node.getNodeValue()) |nv| {\n            try w.write(nv.str());\n        } else {\n            try w.write(\"\");\n        }\n\n        if (include_child_count) {\n            try w.objectField(\"childNodeCount\");\n            try w.write(dom_node.getChildrenCount());\n        }\n\n        try w.objectField(\"documentURL\");\n        try w.write(null);\n\n        try w.objectField(\"baseURL\");\n        try w.write(null);\n\n        try w.objectField(\"xmlVersion\");\n        try w.write(\"\");\n\n        try w.objectField(\"compatibilityMode\");\n        try w.write(\"NoQuirksMode\");\n\n        try w.objectField(\"isScrollable\");\n        try w.write(false);\n    }\n};\n\nconst testing = @import(\"testing.zig\");\ntest \"cdp Node: Registry register\" {\n    var registry = Registry.init(testing.allocator);\n    defer registry.deinit();\n\n    try testing.expectEqual(0, registry.lookup_by_id.count());\n    try testing.expectEqual(0, registry.lookup_by_node.count());\n\n    var page = try testing.pageTest(\"cdp/registry1.html\");\n    defer page._session.removePage();\n    var doc = page.window._document;\n\n    {\n        const dom_node = (try doc.querySelector(.wrap(\"#a1\"), page)).?.asNode();\n        const node = try registry.register(dom_node);\n        const n1b = registry.lookup_by_id.get(1).?;\n        const n1c = registry.lookup_by_node.get(node.dom).?;\n        try testing.expectEqual(node, n1b);\n        try testing.expectEqual(node, n1c);\n\n        try testing.expectEqual(1, node.id);\n        try testing.expectEqual(dom_node, node.dom);\n    }\n\n    {\n        const dom_node = (try doc.querySelector(.wrap(\"p\"), page)).?.asNode();\n        const node = try registry.register(dom_node);\n        const n1b = registry.lookup_by_id.get(2).?;\n        const n1c = registry.lookup_by_node.get(node.dom).?;\n        try testing.expectEqual(node, n1b);\n        try testing.expectEqual(node, n1c);\n\n        try testing.expectEqual(2, node.id);\n        try testing.expectEqual(dom_node, node.dom);\n    }\n}\n\ntest \"cdp Node: search list\" {\n    var registry = Registry.init(testing.allocator);\n    defer registry.deinit();\n\n    var search_list = Search.List.init(testing.allocator, &registry);\n    defer search_list.deinit();\n\n    {\n        // empty search list, noops\n        search_list.remove(\"0\");\n        try testing.expectEqual(null, search_list.get(\"0\"));\n    }\n\n    {\n        // empty nodes\n        const s1 = try search_list.create(&.{});\n        try testing.expectEqual(\"0\", s1.name);\n        try testing.expectEqual(0, s1.node_ids.len);\n\n        const s2 = search_list.get(\"0\").?;\n        try testing.expectEqual(\"0\", s2.name);\n        try testing.expectEqual(0, s2.node_ids.len);\n\n        search_list.remove(\"0\");\n        try testing.expectEqual(null, search_list.get(\"0\"));\n    }\n\n    {\n        var page = try testing.pageTest(\"cdp/registry2.html\");\n        defer page._session.removePage();\n        var doc = page.window._document;\n\n        {\n            const l1 = try doc.querySelectorAll(.wrap(\"a\"), page);\n            defer l1.deinit(page._session);\n            const s1 = try search_list.create(l1._nodes);\n            try testing.expectEqual(\"1\", s1.name);\n            try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids);\n        }\n\n        try testing.expectEqual(2, registry.lookup_by_id.count());\n        try testing.expectEqual(2, registry.lookup_by_node.count());\n\n        {\n            const l2 = try doc.querySelectorAll(.wrap(\"#a1\"), page);\n            defer l2.deinit(page._session);\n            const s2 = try search_list.create(l2._nodes);\n            try testing.expectEqual(\"2\", s2.name);\n            try testing.expectEqualSlices(u32, &.{1}, s2.node_ids);\n        }\n\n        {\n            const l3 = try doc.querySelectorAll(.wrap(\"#a2\"), page);\n            defer l3.deinit(page._session);\n            const s3 = try search_list.create(l3._nodes);\n            try testing.expectEqual(\"3\", s3.name);\n            try testing.expectEqualSlices(u32, &.{2}, s3.node_ids);\n        }\n\n        try testing.expectEqual(2, registry.lookup_by_id.count());\n        try testing.expectEqual(2, registry.lookup_by_node.count());\n    }\n}\n\ntest \"cdp Node: Writer\" {\n    var registry = Registry.init(testing.allocator);\n    defer registry.deinit();\n\n    var page = try testing.pageTest(\"cdp/registry3.html\");\n    defer page._session.removePage();\n    var doc = page.window._document;\n\n    {\n        const node = try registry.register(doc.asNode());\n        const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{\n            .root = node,\n            .depth = 0,\n            .exclude_root = false,\n            .registry = &registry,\n        }, .{});\n        defer testing.allocator.free(json);\n\n        try testing.expectJson(.{\n            .nodeId = 1,\n            .backendNodeId = 1,\n            .nodeType = 9,\n            .nodeName = \"#document\",\n            .localName = \"\",\n            .nodeValue = \"\",\n            .documentURL = null,\n            .baseURL = null,\n            .xmlVersion = \"\",\n            .isScrollable = false,\n            .compatibilityMode = \"NoQuirksMode\",\n            .childNodeCount = 1,\n            .children = &.{.{\n                .nodeId = 2,\n                .backendNodeId = 2,\n                .nodeType = 1,\n                .nodeName = \"HTML\",\n                .localName = \"html\",\n                .nodeValue = \"\",\n                .childNodeCount = 2,\n                .documentURL = null,\n                .baseURL = null,\n                .xmlVersion = \"\",\n                .compatibilityMode = \"NoQuirksMode\",\n                .isScrollable = false,\n            }},\n        }, json);\n    }\n\n    {\n        const node = registry.lookup_by_id.get(2).?;\n        const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{\n            .root = node,\n            .depth = 1,\n            .exclude_root = false,\n            .registry = &registry,\n        }, .{});\n        defer testing.allocator.free(json);\n\n        try testing.expectJson(.{\n            .nodeId = 2,\n            .backendNodeId = 2,\n            .nodeType = 1,\n            .nodeName = \"HTML\",\n            .localName = \"html\",\n            .nodeValue = \"\",\n            .childNodeCount = 2,\n            .documentURL = null,\n            .baseURL = null,\n            .xmlVersion = \"\",\n            .compatibilityMode = \"NoQuirksMode\",\n            .isScrollable = false,\n            .children = &.{ .{\n                .nodeId = 3,\n                .backendNodeId = 3,\n                .nodeType = 1,\n                .nodeName = \"HEAD\",\n                .localName = \"head\",\n                .nodeValue = \"\",\n                .childNodeCount = 0,\n                .documentURL = null,\n                .baseURL = null,\n                .xmlVersion = \"\",\n                .compatibilityMode = \"NoQuirksMode\",\n                .isScrollable = false,\n                .parentId = 2,\n            }, .{\n                .nodeId = 4,\n                .backendNodeId = 4,\n                .nodeType = 1,\n                .nodeName = \"BODY\",\n                .localName = \"body\",\n                .nodeValue = \"\",\n                .childNodeCount = 3,\n                .documentURL = null,\n                .baseURL = null,\n                .xmlVersion = \"\",\n                .compatibilityMode = \"NoQuirksMode\",\n                .isScrollable = false,\n                .parentId = 2,\n            } },\n        }, json);\n    }\n\n    {\n        const node = registry.lookup_by_id.get(2).?;\n        const json = try std.json.Stringify.valueAlloc(testing.allocator, Writer{\n            .root = node,\n            .depth = -1,\n            .exclude_root = true,\n            .registry = &registry,\n        }, .{});\n        defer testing.allocator.free(json);\n\n        try testing.expectJson(&.{ .{\n            .nodeId = 3,\n            .backendNodeId = 3,\n            .nodeType = 1,\n            .nodeName = \"HEAD\",\n            .localName = \"head\",\n            .nodeValue = \"\",\n            .childNodeCount = 0,\n            .documentURL = null,\n            .baseURL = null,\n            .xmlVersion = \"\",\n            .compatibilityMode = \"NoQuirksMode\",\n            .isScrollable = false,\n            .parentId = 2,\n        }, .{\n            .nodeId = 4,\n            .backendNodeId = 4,\n            .nodeType = 1,\n            .nodeName = \"BODY\",\n            .localName = \"body\",\n            .nodeValue = \"\",\n            .childNodeCount = 3,\n            .documentURL = null,\n            .baseURL = null,\n            .xmlVersion = \"\",\n            .compatibilityMode = \"NoQuirksMode\",\n            .isScrollable = false,\n            .children = &.{ .{\n                .nodeId = 5,\n                .localName = \"a\",\n                .childNodeCount = 0,\n                .attributes = &.{ \"id\", \"a1\" },\n                .parentId = 4,\n            }, .{\n                .nodeId = 6,\n                .localName = \"div\",\n                .childNodeCount = 1,\n                .parentId = 4,\n                .children = &.{.{\n                    .nodeId = 7,\n                    .localName = \"a\",\n                    .childNodeCount = 0,\n                    .parentId = 6,\n                    .attributes = &.{ \"id\", \"a2\" },\n                }},\n            }, .{\n                .nodeId = 8,\n                .backendNodeId = 8,\n                .nodeName = \"#text\",\n                .localName = \"\",\n                .childNodeCount = 0,\n                .parentId = 4,\n                .nodeValue = \"\\n\",\n            } },\n        } }, json);\n    }\n}\n"
  },
  {
    "path": "src/cdp/cdp.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst Allocator = std.mem.Allocator;\nconst json = std.json;\n\nconst log = @import(\"../log.zig\");\nconst js = @import(\"../browser/js/js.zig\");\n\nconst App = @import(\"../App.zig\");\nconst Browser = @import(\"../browser/Browser.zig\");\nconst Session = @import(\"../browser/Session.zig\");\nconst HttpClient = @import(\"../browser/HttpClient.zig\");\nconst Page = @import(\"../browser/Page.zig\");\nconst Incrementing = @import(\"id.zig\").Incrementing;\nconst Notification = @import(\"../Notification.zig\");\nconst InterceptState = @import(\"domains/fetch.zig\").InterceptState;\n\npub const URL_BASE = \"chrome://newtab/\";\n\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub const CDP = CDPT(struct {\n    const Client = *@import(\"../Server.zig\").Client;\n});\n\nconst SessionIdGen = Incrementing(u32, \"SID\");\nconst TargetIdGen = Incrementing(u32, \"TID\");\nconst BrowserContextIdGen = Incrementing(u32, \"BID\");\n\n// Generic so that we can inject mocks into it.\npub fn CDPT(comptime TypeProvider: type) type {\n    return struct {\n        // Used for sending message to the client and closing on error\n        client: TypeProvider.Client,\n\n        allocator: Allocator,\n\n        // The active browser\n        browser: Browser,\n\n        // when true, any target creation must be attached.\n        target_auto_attach: bool = false,\n\n        target_id_gen: TargetIdGen = .{},\n        session_id_gen: SessionIdGen = .{},\n        browser_context_id_gen: BrowserContextIdGen = .{},\n\n        browser_context: ?BrowserContext(Self),\n\n        // Re-used arena for processing a message. We're assuming that we're getting\n        // 1 message at a time.\n        message_arena: std.heap.ArenaAllocator,\n\n        // Used for processing notifications within a browser context.\n        notification_arena: std.heap.ArenaAllocator,\n\n        // Valid for 1 page navigation (what CDP calls a \"renderer\")\n        page_arena: std.heap.ArenaAllocator,\n\n        // Valid for the entire lifetime of the BrowserContext. Should minimize\n        // (or altogether elimiate) our use of this.\n        browser_context_arena: std.heap.ArenaAllocator,\n\n        const Self = @This();\n\n        pub fn init(app: *App, http_client: *HttpClient, client: TypeProvider.Client) !Self {\n            const allocator = app.allocator;\n            const browser = try Browser.init(app, .{\n                .env = .{ .with_inspector = true },\n                .http_client = http_client,\n            });\n            errdefer browser.deinit();\n\n            return .{\n                .client = client,\n                .browser = browser,\n                .allocator = allocator,\n                .browser_context = null,\n                .page_arena = std.heap.ArenaAllocator.init(allocator),\n                .message_arena = std.heap.ArenaAllocator.init(allocator),\n                .notification_arena = std.heap.ArenaAllocator.init(allocator),\n                .browser_context_arena = std.heap.ArenaAllocator.init(allocator),\n            };\n        }\n\n        pub fn deinit(self: *Self) void {\n            if (self.browser_context) |*bc| {\n                bc.deinit();\n            }\n            self.browser.deinit();\n            self.page_arena.deinit();\n            self.message_arena.deinit();\n            self.notification_arena.deinit();\n            self.browser_context_arena.deinit();\n        }\n\n        pub fn handleMessage(self: *Self, msg: []const u8) bool {\n            // if there's an error, it's already been logged\n            self.processMessage(msg) catch return false;\n            return true;\n        }\n\n        pub fn processMessage(self: *Self, msg: []const u8) !void {\n            const arena = &self.message_arena;\n            defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 });\n            return self.dispatch(arena.allocator(), self, msg);\n        }\n\n        // @newhttp\n        // A bit hacky right now. The main server loop doesn't unblock for\n        // scheduled task. So we run this directly in order to process any\n        // timeouts (or http events) which are ready to be processed.\n        pub fn pageWait(self: *Self, ms: u32) Session.WaitResult {\n            const session = &(self.browser.session orelse return .no_page);\n            return session.wait(ms);\n        }\n\n        // Called from above, in processMessage which handles client messages\n        // but can also be called internally. For example, Target.sendMessageToTarget\n        // calls back into dispatch to capture the response.\n        pub fn dispatch(self: *Self, arena: Allocator, sender: anytype, str: []const u8) !void {\n            const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{\n                .ignore_unknown_fields = true,\n            }) catch return error.InvalidJSON;\n\n            var command = Command(Self, @TypeOf(sender)){\n                .input = .{\n                    .json = str,\n                    .id = input.id,\n                    .action = \"\",\n                    .params = input.params,\n                    .session_id = input.sessionId,\n                },\n                .cdp = self,\n                .arena = arena,\n                .sender = sender,\n                .browser_context = if (self.browser_context) |*bc| bc else null,\n            };\n\n            // See dispatchStartupCommand for more info on this.\n            var is_startup = false;\n            if (input.sessionId) |input_session_id| {\n                if (std.mem.eql(u8, input_session_id, \"STARTUP\")) {\n                    is_startup = true;\n                } else if (self.isValidSessionId(input_session_id) == false) {\n                    return command.sendError(-32001, \"Unknown sessionId\", .{});\n                }\n            }\n\n            if (is_startup) {\n                dispatchStartupCommand(&command, input.method) catch |err| {\n                    command.sendError(-31999, @errorName(err), .{}) catch return err;\n                };\n            } else {\n                dispatchCommand(&command, input.method) catch |err| {\n                    command.sendError(-31998, @errorName(err), .{}) catch return err;\n                };\n            }\n        }\n\n        // A CDP session isn't 100% fully driven by the driver. There's are\n        // independent actions that the browser is expected to take. For example\n        // Puppeteer expects the browser to startup a tab and thus have existing\n        // targets.\n        // To this end, we create a [very] dummy BrowserContext, Target and\n        // Session. There isn't actually a BrowserContext, just a special id.\n        // When messages are received with the \"STARTUP\" sessionId, we do\n        // \"special\" handling - the bare minimum we need to do until the driver\n        // switches to a real BrowserContext.\n        // (I can imagine this logic will become driver-specific)\n        fn dispatchStartupCommand(command: anytype, method: []const u8) !void {\n            // Stagehand parses the response and error if we don't return a\n            // correct one for this call.\n            if (std.mem.eql(u8, method, \"Page.getFrameTree\")) {\n                return command.sendResult(.{\n                    .frameTree = .{\n                        .frame = .{\n                            .id = \"TID-STARTUP\",\n                            .loaderId = \"LOADERID24DD2FD56CF1EF33C965C79C\",\n                            .securityOrigin = URL_BASE,\n                            .url = \"about:blank\",\n                            .secureContextType = \"Secure\",\n                        },\n                    },\n                }, .{});\n            }\n\n            return command.sendResult(null, .{});\n        }\n\n        fn dispatchCommand(command: anytype, method: []const u8) !void {\n            const domain = blk: {\n                const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {\n                    return error.InvalidMethod;\n                };\n                command.input.action = method[i + 1 ..];\n                break :blk method[0..i];\n            };\n\n            switch (domain.len) {\n                2 => switch (@as(u16, @bitCast(domain[0..2].*))) {\n                    asUint(u16, \"LP\") => return @import(\"domains/lp.zig\").processMessage(command),\n                    else => {},\n                },\n                3 => switch (@as(u24, @bitCast(domain[0..3].*))) {\n                    asUint(u24, \"DOM\") => return @import(\"domains/dom.zig\").processMessage(command),\n                    asUint(u24, \"Log\") => return @import(\"domains/log.zig\").processMessage(command),\n                    asUint(u24, \"CSS\") => return @import(\"domains/css.zig\").processMessage(command),\n                    else => {},\n                },\n                4 => switch (@as(u32, @bitCast(domain[0..4].*))) {\n                    asUint(u32, \"Page\") => return @import(\"domains/page.zig\").processMessage(command),\n                    else => {},\n                },\n                5 => switch (@as(u40, @bitCast(domain[0..5].*))) {\n                    asUint(u40, \"Fetch\") => return @import(\"domains/fetch.zig\").processMessage(command),\n                    asUint(u40, \"Input\") => return @import(\"domains/input.zig\").processMessage(command),\n                    else => {},\n                },\n                6 => switch (@as(u48, @bitCast(domain[0..6].*))) {\n                    asUint(u48, \"Target\") => return @import(\"domains/target.zig\").processMessage(command),\n                    else => {},\n                },\n                7 => switch (@as(u56, @bitCast(domain[0..7].*))) {\n                    asUint(u56, \"Browser\") => return @import(\"domains/browser.zig\").processMessage(command),\n                    asUint(u56, \"Runtime\") => return @import(\"domains/runtime.zig\").processMessage(command),\n                    asUint(u56, \"Network\") => return @import(\"domains/network.zig\").processMessage(command),\n                    asUint(u56, \"Storage\") => return @import(\"domains/storage.zig\").processMessage(command),\n                    else => {},\n                },\n                8 => switch (@as(u64, @bitCast(domain[0..8].*))) {\n                    asUint(u64, \"Security\") => return @import(\"domains/security.zig\").processMessage(command),\n                    else => {},\n                },\n                9 => switch (@as(u72, @bitCast(domain[0..9].*))) {\n                    asUint(u72, \"Emulation\") => return @import(\"domains/emulation.zig\").processMessage(command),\n                    asUint(u72, \"Inspector\") => return @import(\"domains/inspector.zig\").processMessage(command),\n                    else => {},\n                },\n                11 => switch (@as(u88, @bitCast(domain[0..11].*))) {\n                    asUint(u88, \"Performance\") => return @import(\"domains/performance.zig\").processMessage(command),\n                    else => {},\n                },\n                13 => switch (@as(u104, @bitCast(domain[0..13].*))) {\n                    asUint(u104, \"Accessibility\") => return @import(\"domains/accessibility.zig\").processMessage(command),\n                    else => {},\n                },\n\n                else => {},\n            }\n\n            return error.UnknownDomain;\n        }\n\n        fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {\n            const browser_context = &(self.browser_context orelse return false);\n            const session_id = browser_context.session_id orelse return false;\n            return std.mem.eql(u8, session_id, input_session_id);\n        }\n\n        pub fn createBrowserContext(self: *Self) ![]const u8 {\n            if (self.browser_context != null) {\n                return error.AlreadyExists;\n            }\n            const id = self.browser_context_id_gen.next();\n\n            self.browser_context = @as(BrowserContext(Self), undefined);\n            const browser_context = &self.browser_context.?;\n\n            try BrowserContext(Self).init(browser_context, id, self);\n            return id;\n        }\n\n        pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {\n            const bc = &(self.browser_context orelse return false);\n            if (std.mem.eql(u8, bc.id, browser_context_id) == false) {\n                return false;\n            }\n            bc.deinit();\n            self.browser.closeSession();\n            self.browser_context = null;\n            return true;\n        }\n\n        const SendEventOpts = struct {\n            session_id: ?[]const u8 = null,\n        };\n        pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void {\n            return self.sendJSON(.{\n                .method = method,\n                .params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p,\n                .sessionId = opts.session_id,\n            });\n        }\n\n        pub fn sendJSON(self: *Self, message: anytype) !void {\n            return self.client.sendJSON(message, .{\n                .emit_null_optional_fields = false,\n            });\n        }\n    };\n}\n\npub fn BrowserContext(comptime CDP_T: type) type {\n    const Node = @import(\"Node.zig\");\n    const AXNode = @import(\"AXNode.zig\");\n\n    return struct {\n        id: []const u8,\n        cdp: *CDP_T,\n\n        // Represents the browser session. There is no equivalent in CDP. For\n        // all intents and purpose, from CDP's point of view our Browser and\n        // our Session more or less maps to a BrowserContext. THIS HAS ZERO\n        // RELATION TO SESSION_ID\n        session: *Session,\n\n        // Tied to the lifetime of the BrowserContext\n        arena: Allocator,\n\n        // Tied to the lifetime of 1 page rendered in the BrowserContext.\n        page_arena: Allocator,\n\n        // From the parent's notification_arena.allocator(). Most of the CDP\n        // code paths deal with a cmd which has its own arena (from the\n        // message_arena). But notifications happen outside of the typical CDP\n        // request->response, and thus don't have a cmd and don't have an arena.\n        notification_arena: Allocator,\n\n        // Maps to our Page. (There are other types of targets, but we only\n        // deal with \"pages\" for now). Since we only allow 1 open page at a\n        // time, we only have 1 target_id.\n        target_id: ?[14]u8,\n\n        // The CDP session_id. After the target/page is created, the client\n        // \"attaches\" to it (either explicitly or automatically). We return a\n        // \"sessionId\" which identifies this link. `sessionId` is the how\n        // the CDP client informs us what it's trying to manipulate. Because we\n        // only support 1 BrowserContext at a time, and 1 page at a time, this\n        // is all pretty straightforward, but it still needs to be enforced, i.e.\n        // if we get a request with a sessionId that doesn't match the current one\n        // we should reject it.\n        session_id: ?[]const u8,\n\n        security_origin: []const u8,\n        page_life_cycle_events: bool,\n        secure_context_type: []const u8,\n        node_registry: Node.Registry,\n        node_search_list: Node.Search.List,\n\n        inspector_session: *js.Inspector.Session,\n        isolated_worlds: std.ArrayList(*IsolatedWorld),\n\n        http_proxy_changed: bool = false,\n\n        // Extra headers to add to all requests.\n        extra_headers: std.ArrayList([*c]const u8) = .empty,\n\n        intercept_state: InterceptState,\n\n        // When network is enabled, we'll capture the transfer.id -> body\n        // This is awfully memory intensive, but our underlying http client and\n        // its users (script manager and page) correctly do not hold the body\n        // memory longer than they have to. In fact, the main request is only\n        // ever streamed. So if CDP is the only thing that needs bodies in\n        // memory for an arbitrary amount of time, then that's where we're going\n        // to store the,\n        captured_responses: std.AutoHashMapUnmanaged(usize, std.ArrayList(u8)),\n\n        notification: *Notification,\n\n        const Self = @This();\n\n        fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {\n            const allocator = cdp.allocator;\n\n            // Create notification for this BrowserContext\n            const notification = try Notification.init(allocator);\n            errdefer notification.deinit();\n\n            const session = try cdp.browser.newSession(notification);\n\n            const browser = &cdp.browser;\n            const inspector_session = browser.env.inspector.?.startSession(self);\n            errdefer browser.env.inspector.?.stopSession();\n\n            var registry = Node.Registry.init(allocator);\n            errdefer registry.deinit();\n\n            self.* = .{\n                .id = id,\n                .cdp = cdp,\n                .target_id = null,\n                .session_id = null,\n                .session = session,\n                .security_origin = URL_BASE,\n                .secure_context_type = \"Secure\", // TODO = enum\n                .page_life_cycle_events = false, // TODO; Target based value\n                .node_registry = registry,\n                .node_search_list = undefined,\n                .isolated_worlds = .empty,\n                .inspector_session = inspector_session,\n                .page_arena = cdp.page_arena.allocator(),\n                .arena = cdp.browser_context_arena.allocator(),\n                .notification_arena = cdp.notification_arena.allocator(),\n                .intercept_state = try InterceptState.init(allocator),\n                .captured_responses = .empty,\n                .notification = notification,\n            };\n            self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);\n            errdefer self.deinit();\n\n            try notification.register(.page_remove, self, onPageRemove);\n            try notification.register(.page_created, self, onPageCreated);\n            try notification.register(.page_navigate, self, onPageNavigate);\n            try notification.register(.page_navigated, self, onPageNavigated);\n            try notification.register(.page_frame_created, self, onPageFrameCreated);\n        }\n\n        pub fn deinit(self: *Self) void {\n            const browser = &self.cdp.browser;\n            const env = &browser.env;\n\n            // resetContextGroup detach the inspector from all contexts.\n            // It appends async tasks, so we make sure we run the message loop\n            // before deinit it.\n            env.inspector.?.resetContextGroup();\n            env.inspector.?.stopSession();\n\n            // abort all intercepted requests before closing the sesion/page\n            // since some of these might callback into the page/scriptmanager\n            for (self.intercept_state.pendingTransfers()) |transfer| {\n                transfer.abort(error.ClientDisconnect);\n            }\n\n            for (self.isolated_worlds.items) |world| {\n                world.deinit();\n            }\n            self.isolated_worlds.clearRetainingCapacity();\n\n            // do this before closeSession, since we don't want to process any\n            // new notification (Or maybe, instead of the deinit above, we just\n            // rely on those notifications to do our normal cleanup?)\n\n            self.notification.unregisterAll(self);\n\n            // If the session has a page, we need to clear it first. The page\n            // context is always nested inside of the isolated world context,\n            // so we need to shutdown the page one first.\n            browser.closeSession();\n\n            self.node_registry.deinit();\n            self.node_search_list.deinit();\n            self.notification.deinit();\n\n            if (self.http_proxy_changed) {\n                // has to be called after browser.closeSession, since it won't\n                // work if there are active connections.\n                browser.http_client.restoreOriginalProxy() catch |err| {\n                    log.warn(.http, \"restoreOriginalProxy\", .{ .err = err });\n                };\n            }\n            self.intercept_state.deinit();\n        }\n\n        pub fn reset(self: *Self) void {\n            self.node_registry.reset();\n            self.node_search_list.reset();\n        }\n\n        pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {\n            const browser = &self.cdp.browser;\n            const arena = try browser.arena_pool.acquire();\n            errdefer browser.arena_pool.release(arena);\n\n            const world = try arena.create(IsolatedWorld);\n            world.* = .{\n                .arena = arena,\n                .context = null,\n                .browser = browser,\n                .name = try arena.dupe(u8, world_name),\n                .grant_universal_access = grant_universal_access,\n            };\n\n            try self.isolated_worlds.append(self.arena, world);\n\n            return world;\n        }\n\n        pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer {\n            return .{\n                .root = root,\n                .depth = opts.depth,\n                .exclude_root = opts.exclude_root,\n                .registry = &self.node_registry,\n            };\n        }\n\n        pub fn axnodeWriter(self: *Self, root: *const Node, opts: AXNode.Writer.Opts) !AXNode.Writer {\n            const page = self.session.currentPage() orelse return error.PageNotLoaded;\n            _ = opts;\n            return .{\n                .page = page,\n                .root = root,\n                .registry = &self.node_registry,\n            };\n        }\n\n        pub fn getURL(self: *const Self) ?[:0]const u8 {\n            const page = self.session.currentPage() orelse return null;\n            const url = page.url;\n            return if (url.len == 0) null else url;\n        }\n\n        pub fn getTitle(self: *const Self) ?[]const u8 {\n            const page = self.session.currentPage() orelse return null;\n            return page.getTitle() catch |err| {\n                log.err(.cdp, \"page title\", .{ .err = err });\n                return null;\n            };\n        }\n\n        pub fn networkEnable(self: *Self) !void {\n            try self.notification.register(.http_request_fail, self, onHttpRequestFail);\n            try self.notification.register(.http_request_start, self, onHttpRequestStart);\n            try self.notification.register(.http_request_done, self, onHttpRequestDone);\n            try self.notification.register(.http_response_data, self, onHttpResponseData);\n            try self.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone);\n        }\n\n        pub fn networkDisable(self: *Self) void {\n            self.notification.unregister(.http_request_fail, self);\n            self.notification.unregister(.http_request_start, self);\n            self.notification.unregister(.http_request_done, self);\n            self.notification.unregister(.http_response_data, self);\n            self.notification.unregister(.http_response_header_done, self);\n        }\n\n        pub fn fetchEnable(self: *Self, authRequests: bool) !void {\n            try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept);\n            if (authRequests) {\n                try self.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired);\n            }\n        }\n\n        pub fn fetchDisable(self: *Self) void {\n            self.notification.unregister(.http_request_intercept, self);\n            self.notification.unregister(.http_request_auth_required, self);\n        }\n\n        pub fn lifecycleEventsEnable(self: *Self) !void {\n            self.page_life_cycle_events = true;\n            try self.notification.register(.page_network_idle, self, onPageNetworkIdle);\n            try self.notification.register(.page_network_almost_idle, self, onPageNetworkAlmostIdle);\n        }\n\n        pub fn lifecycleEventsDisable(self: *Self) void {\n            self.page_life_cycle_events = false;\n            self.notification.unregister(.page_network_idle, self);\n            self.notification.unregister(.page_network_almost_idle, self);\n        }\n\n        pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            try @import(\"domains/page.zig\").pageRemove(self);\n        }\n\n        pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/page.zig\").pageCreated(self, page);\n        }\n\n        pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/page.zig\").pageNavigate(self, msg);\n        }\n\n        pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            defer self.resetNotificationArena();\n            return @import(\"domains/page.zig\").pageNavigated(self.notification_arena, self, msg);\n        }\n\n        pub fn onPageFrameCreated(ctx: *anyopaque, msg: *const Notification.PageFrameCreated) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/page.zig\").pageFrameCreated(self, msg);\n        }\n\n        pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/page.zig\").pageNetworkIdle(self, msg);\n        }\n\n        pub fn onPageNetworkAlmostIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkAlmostIdle) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/page.zig\").pageNetworkAlmostIdle(self, msg);\n        }\n\n        pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            try @import(\"domains/network.zig\").httpRequestStart(self, msg);\n        }\n\n        pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            try @import(\"domains/fetch.zig\").requestIntercept(self, msg);\n        }\n\n        pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/network.zig\").httpRequestFail(self, msg);\n        }\n\n        pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            defer self.resetNotificationArena();\n            return @import(\"domains/network.zig\").httpResponseHeaderDone(self.notification_arena, self, msg);\n        }\n\n        pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            return @import(\"domains/network.zig\").httpRequestDone(self, msg);\n        }\n\n        pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            const arena = self.page_arena;\n\n            const id = msg.transfer.id;\n            const gop = try self.captured_responses.getOrPut(arena, id);\n            if (!gop.found_existing) {\n                gop.value_ptr.* = .{};\n            }\n            try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));\n        }\n\n        pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void {\n            const self: *Self = @ptrCast(@alignCast(ctx));\n            defer self.resetNotificationArena();\n            try @import(\"domains/fetch.zig\").requestAuthRequired(self, data);\n        }\n\n        fn resetNotificationArena(self: *Self) void {\n            defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });\n        }\n\n        pub fn callInspector(self: *const Self, msg: []const u8) void {\n            self.inspector_session.send(msg);\n            self.session.browser.env.runMicrotasks();\n        }\n\n        pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {\n            sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| {\n                log.err(.cdp, \"send inspector response\", .{ .err = err });\n            };\n        }\n\n        pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {\n            if (log.enabled(.cdp, .debug)) {\n                // msg should be {\"method\":<method>,...\n                lp.assert(std.mem.startsWith(u8, msg, \"{\\\"method\\\":\"), \"onInspectorEvent prefix\", .{});\n                const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {\n                    log.err(.cdp, \"invalid inspector event\", .{ .msg = msg });\n                    return;\n                };\n                const method = msg[10..method_end];\n                log.debug(.cdp, \"inspector event\", .{ .method = method });\n            }\n\n            sendInspectorMessage(@ptrCast(@alignCast(ctx)), msg) catch |err| {\n                log.err(.cdp, \"send inspector event\", .{ .err = err });\n            };\n        }\n\n        // This is hacky x 2. First, we create the JSON payload by gluing our\n        // session_id onto it. Second, we're much more client/websocket aware than\n        // we should be.\n        fn sendInspectorMessage(self: *Self, msg: []const u8) !void {\n            const session_id = self.session_id orelse {\n                // We no longer have an active session. What should we do\n                // in this case?\n                return;\n            };\n\n            const cdp = self.cdp;\n            const allocator = cdp.client.sendAllocator();\n\n            const field = \",\\\"sessionId\\\":\\\"\";\n\n            // + 1 for the closing quote after the session id\n            // + 10 for the max websocket header\n            const message_len = msg.len + session_id.len + 1 + field.len + 10;\n\n            var buf: std.ArrayList(u8) = .{};\n            buf.ensureTotalCapacity(allocator, message_len) catch |err| {\n                log.err(.cdp, \"inspector buffer\", .{ .err = err });\n                return;\n            };\n\n            // reserve 10 bytes for websocket header\n            buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });\n\n            // -1  because we dont' want the closing brace '}'\n            buf.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]);\n            buf.appendSliceAssumeCapacity(field);\n            buf.appendSliceAssumeCapacity(session_id);\n            buf.appendSliceAssumeCapacity(\"\\\"}\");\n            if (comptime IS_DEBUG) {\n                std.debug.assert(buf.items.len == message_len);\n            }\n\n            try cdp.client.sendJSONRaw(buf);\n        }\n    };\n}\n\n/// see: https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world\n/// The current understanding. An isolated world lives in the same isolate, but a separated context.\n/// Clients create this to be able to create variables and run code without interfering with the\n/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after\n/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed\n/// in the isolated world by using its Context ID or the worldName.\n/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.\n/// An isolated world has it's own instance of globals like Window.\n/// Generally the client needs to resolve a node into the isolated world to be able to work with it.\n/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.\nconst IsolatedWorld = struct {\n    arena: Allocator,\n    browser: *Browser,\n    name: []const u8,\n    context: ?*js.Context = null,\n    grant_universal_access: bool,\n\n    pub fn deinit(self: *IsolatedWorld) void {\n        self.removeContext() catch {};\n        self.browser.arena_pool.release(self.arena);\n    }\n\n    pub fn removeContext(self: *IsolatedWorld) !void {\n        const ctx = self.context orelse return error.NoIsolatedContextToRemove;\n        self.browser.env.destroyContext(ctx);\n        self.context = null;\n    }\n\n    // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML\n    // (assuming grantUniveralAccess will be set to True!).\n    // We just created the world and the page. The page's state lives in the session, but is update on navigation.\n    // This also means this pointer becomes invalid after removePage until a new page is created.\n    // Currently we have only 1 page/frame and thus also only 1 state in the isolate world.\n    pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context {\n        if (self.context == null) {\n            self.context = try self.browser.env.createContext(page);\n        } else {\n            log.warn(.cdp, \"not implemented\", .{\n                .feature = \"createContext: Not implemented second isolated context creation\",\n                .info = \"reuse existing context\",\n            });\n        }\n        return self.context.?;\n    }\n};\n\n// This is a generic because when we send a result we have two different\n// behaviors. Normally, we're sending the result to the client. But in some cases\n// we want to capture the result. So we want the command.sendResult to be\n// generic.\npub fn Command(comptime CDP_T: type, comptime Sender: type) type {\n    return struct {\n        // A misc arena that can be used for any allocation for processing\n        // the message\n        arena: Allocator,\n\n        // reference to our CDP instance\n        cdp: *CDP_T,\n\n        // The browser context this command targets\n        browser_context: ?*BrowserContext(CDP_T),\n\n        // The command input (the id, optional session_id, params, ...)\n        input: Input,\n\n        // In most cases, Sender is going to be cdp itself. We'll call\n        // sender.sendJSON() and CDP will send it to the client. But some\n        // comamnds are dispatched internally, in which cases the Sender will\n        // be code to capture the data that we were \"sending\".\n        sender: Sender,\n\n        const Self = @This();\n\n        pub fn params(self: *const Self, comptime T: type) !?T {\n            if (self.input.params) |p| {\n                return try json.parseFromSliceLeaky(\n                    T,\n                    self.arena,\n                    p.raw,\n                    .{ .ignore_unknown_fields = true },\n                );\n            }\n            return null;\n        }\n\n        pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {\n            _ = try self.cdp.createBrowserContext();\n            self.browser_context = &(self.cdp.browser_context.?);\n            return self.browser_context.?;\n        }\n\n        const SendResultOpts = struct {\n            include_session_id: bool = true,\n        };\n        pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void {\n            return self.sender.sendJSON(.{\n                .id = self.input.id,\n                .result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result,\n                .sessionId = if (opts.include_session_id) self.input.session_id else null,\n            });\n        }\n\n        const SendEventOpts = struct {\n            session_id: ?[]const u8 = null,\n        };\n        pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: CDP_T.SendEventOpts) !void {\n            // Events ALWAYS go to the client. self.sender should not be used\n            return self.cdp.sendEvent(method, p, opts);\n        }\n\n        const SendErrorOpts = struct {\n            include_session_id: bool = true,\n        };\n        pub fn sendError(self: *Self, code: i32, message: []const u8, opts: SendErrorOpts) !void {\n            return self.sender.sendJSON(.{\n                .id = self.input.id,\n                .@\"error\" = .{ .code = code, .message = message },\n                .sessionId = if (opts.include_session_id) self.input.session_id else null,\n            });\n        }\n\n        const Input = struct {\n            // When we reply to a message, we echo back the message id\n            id: ?i64,\n\n            // The \"action\" of the message.Given a method of \"LOG.enable\", the\n            // action is \"enable\"\n            action: []const u8,\n\n            // See notes in BrowserContext about session_id\n            session_id: ?[]const u8,\n\n            // Unparsed / untyped input.params.\n            params: ?InputParams,\n\n            // The full raw json input\n            json: []const u8,\n        };\n    };\n}\n\n// When we parse a JSON message from the client, this is the structure\n// we always expect\nconst InputMessage = struct {\n    id: ?i64 = null,\n    method: []const u8,\n    params: ?InputParams = null,\n    sessionId: ?[]const u8 = null,\n};\n\n// The JSON \"params\" field changes based on the \"method\". Initially, we just\n// capture the raw json object (including the opening and closing braces).\n// Then, when we're processing the message, and we know what type it is, we\n// can parse it (in Disaptch(T).params).\nconst InputParams = struct {\n    raw: []const u8,\n\n    pub fn jsonParse(\n        _: Allocator,\n        scanner: *json.Scanner,\n        _: json.ParseOptions,\n    ) !InputParams {\n        const height = scanner.stackHeight();\n\n        const start = scanner.cursor;\n        if (try scanner.next() != .object_begin) {\n            return error.UnexpectedToken;\n        }\n        try scanner.skipUntilStackHeight(height);\n        const end = scanner.cursor;\n\n        return .{ .raw = scanner.input[start..end] };\n    }\n};\n\nfn asUint(comptime T: type, comptime string: []const u8) T {\n    return @bitCast(string[0..string.len].*);\n}\n\nconst testing = @import(\"testing.zig\");\ntest \"cdp: invalid json\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    try testing.expectError(error.InvalidJSON, ctx.processMessage(\"invalid\"));\n\n    // method is required\n    try testing.expectError(error.InvalidJSON, ctx.processMessage(.{}));\n\n    try ctx.processMessage(.{\n        .method = \"Target\",\n    });\n    try ctx.expectSentError(-31998, \"InvalidMethod\", .{});\n\n    try ctx.processMessage(.{\n        .method = \"Unknown.domain\",\n    });\n    try ctx.expectSentError(-31998, \"UnknownDomain\", .{});\n\n    try ctx.processMessage(.{\n        .method = \"Target.over9000\",\n    });\n    try ctx.expectSentError(-31998, \"UnknownMethod\", .{});\n}\n\ntest \"cdp: invalid sessionId\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        // we have no browser context\n        try ctx.processMessage(.{ .method = \"Hi\", .sessionId = \"nope\" });\n        try ctx.expectSentError(-32001, \"Unknown sessionId\", .{});\n    }\n\n    {\n        // we have a brower context but no session_id\n        _ = try ctx.loadBrowserContext(.{});\n        try ctx.processMessage(.{ .method = \"Hi\", .sessionId = \"BC-Has-No-SessionId\" });\n        try ctx.expectSentError(-32001, \"Unknown sessionId\", .{});\n    }\n\n    {\n        // we have a brower context with a different session_id\n        _ = try ctx.loadBrowserContext(.{ .session_id = \"SESS-2\" });\n        try ctx.processMessage(.{ .method = \"Hi\", .sessionId = \"SESS-1\" });\n        try ctx.expectSentError(-32001, \"Unknown sessionId\", .{});\n    }\n}\n\ntest \"cdp: STARTUP sessionId\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        // we have no browser context\n        try ctx.processMessage(.{ .id = 2, .method = \"Hi\", .sessionId = \"STARTUP\" });\n        try ctx.expectSentResult(null, .{ .id = 2, .index = 0, .session_id = \"STARTUP\" });\n    }\n\n    {\n        // we have a brower context but no session_id\n        _ = try ctx.loadBrowserContext(.{});\n        try ctx.processMessage(.{ .id = 3, .method = \"Hi\", .sessionId = \"STARTUP\" });\n        try ctx.expectSentResult(null, .{ .id = 3, .index = 0, .session_id = \"STARTUP\" });\n    }\n\n    {\n        // we have a brower context with a different session_id\n        _ = try ctx.loadBrowserContext(.{ .session_id = \"SESS-2\" });\n        try ctx.processMessage(.{ .id = 4, .method = \"Hi\", .sessionId = \"STARTUP\" });\n        try ctx.expectSentResult(null, .{ .id = 4, .index = 0, .session_id = \"STARTUP\" });\n    }\n}\n"
  },
  {
    "path": "src/cdp/domains/accessibility.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst id = @import(\"../id.zig\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        disable,\n        getFullAXTree,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return enable(cmd),\n        .disable => return disable(cmd),\n        .getFullAXTree => return getFullAXTree(cmd),\n    }\n}\nfn enable(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\nfn disable(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\nfn getFullAXTree(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        depth: ?i32 = null,\n        frameId: ?[]const u8 = null,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const session = bc.session;\n\n    const page = blk: {\n        const frame_id = params.frameId orelse {\n            break :blk session.currentPage() orelse return error.PageNotLoaded;\n        };\n        const page_frame_id = try id.toPageId(.frame_id, frame_id);\n        break :blk session.findPageByFrameId(page_frame_id) orelse {\n            return cmd.sendError(-32000, \"Frame with the given id does not belong to the target.\", .{});\n        };\n    };\n\n    const doc = page.window._document.asNode();\n    const node = try bc.node_registry.register(doc);\n\n    return cmd.sendResult(.{ .nodes = try bc.axnodeWriter(node, .{}) }, .{});\n}\n"
  },
  {
    "path": "src/cdp/domains/browser.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\n// TODO: hard coded data\nconst PROTOCOL_VERSION = \"1.3\";\nconst REVISION = \"@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4\";\n\n// CDP_USER_AGENT const is used by the CDP server only to identify itself to\n// the CDP clients.\n// Many clients check the CDP server is a Chrome browser.\n//\n// CDP_USER_AGENT const is not used by the browser for the HTTP client (see\n// src/http/client.zig) nor exposed to the JS (see\n// src/browser/html/navigator.zig).\nconst CDP_USER_AGENT = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\";\nconst PRODUCT = \"Chrome/124.0.6367.29\";\n\nconst JS_VERSION = \"12.4.254.8\";\nconst DEV_TOOLS_WINDOW_ID = 1923710101;\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        getVersion,\n        setPermission,\n        setWindowBounds,\n        resetPermissions,\n        grantPermissions,\n        getWindowForTarget,\n        setDownloadBehavior,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .getVersion => return getVersion(cmd),\n        .setPermission => return setPermission(cmd),\n        .setWindowBounds => return setWindowBounds(cmd),\n        .resetPermissions => return resetPermissions(cmd),\n        .grantPermissions => return grantPermissions(cmd),\n        .getWindowForTarget => return getWindowForTarget(cmd),\n        .setDownloadBehavior => return setDownloadBehavior(cmd),\n    }\n}\n\nfn getVersion(cmd: anytype) !void {\n    // TODO: pre-serialize?\n    return cmd.sendResult(.{\n        .protocolVersion = PROTOCOL_VERSION,\n        .product = PRODUCT,\n        .revision = REVISION,\n        .userAgent = CDP_USER_AGENT,\n        .jsVersion = JS_VERSION,\n    }, .{ .include_session_id = false });\n}\n\n// TODO: noop method\nfn setDownloadBehavior(cmd: anytype) !void {\n    // const params = (try cmd.params(struct {\n    //     behavior: []const u8,\n    //     browserContextId: ?[]const u8 = null,\n    //     downloadPath: ?[]const u8 = null,\n    //     eventsEnabled: ?bool = null,\n    // })) orelse return error.InvalidParams;\n\n    return cmd.sendResult(null, .{ .include_session_id = false });\n}\n\nfn getWindowForTarget(cmd: anytype) !void {\n    // const params = (try cmd.params(struct {\n    //     targetId: ?[]const u8 = null,\n    // })) orelse return error.InvalidParams;\n\n    return cmd.sendResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{\n        .windowState = \"normal\",\n    } }, .{});\n}\n\n// TODO: noop method\nfn setWindowBounds(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn grantPermissions(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn setPermission(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn resetPermissions(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"cdp.browser: getVersion\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    try ctx.processMessage(.{\n        .id = 32,\n        .method = \"Browser.getVersion\",\n    });\n\n    try ctx.expectSentCount(1);\n    try ctx.expectSentResult(.{\n        .protocolVersion = PROTOCOL_VERSION,\n        .product = PRODUCT,\n        .revision = REVISION,\n        .userAgent = CDP_USER_AGENT,\n        .jsVersion = JS_VERSION,\n    }, .{ .id = 32, .index = 0, .session_id = null });\n}\n\ntest \"cdp.browser: getWindowForTarget\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    try ctx.processMessage(.{\n        .id = 33,\n        .method = \"Browser.getWindowForTarget\",\n    });\n\n    try ctx.expectSentCount(1);\n    try ctx.expectSentResult(.{\n        .windowId = DEV_TOOLS_WINDOW_ID,\n        .bounds = .{ .windowState = \"normal\" },\n    }, .{ .id = 33, .index = 0, .session_id = null });\n}\n"
  },
  {
    "path": "src/cdp/domains/css.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return cmd.sendResult(null, .{}),\n    }\n}\n"
  },
  {
    "path": "src/cdp/domains/dom.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst id = @import(\"../id.zig\");\nconst log = @import(\"../../log.zig\");\nconst Node = @import(\"../Node.zig\");\nconst DOMNode = @import(\"../../browser/webapi/Node.zig\");\nconst Selector = @import(\"../../browser/webapi/selector/Selector.zig\");\n\nconst dump = @import(\"../../browser/dump.zig\");\nconst js = @import(\"../../browser/js/js.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        getDocument,\n        performSearch,\n        getSearchResults,\n        discardSearchResults,\n        querySelector,\n        querySelectorAll,\n        resolveNode,\n        describeNode,\n        scrollIntoViewIfNeeded,\n        getContentQuads,\n        getBoxModel,\n        requestChildNodes,\n        getFrameOwner,\n        getOuterHTML,\n        requestNode,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return cmd.sendResult(null, .{}),\n        .getDocument => return getDocument(cmd),\n        .performSearch => return performSearch(cmd),\n        .getSearchResults => return getSearchResults(cmd),\n        .discardSearchResults => return discardSearchResults(cmd),\n        .querySelector => return querySelector(cmd),\n        .querySelectorAll => return querySelectorAll(cmd),\n        .resolveNode => return resolveNode(cmd),\n        .describeNode => return describeNode(cmd),\n        .scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd),\n        .getContentQuads => return getContentQuads(cmd),\n        .getBoxModel => return getBoxModel(cmd),\n        .requestChildNodes => return requestChildNodes(cmd),\n        .getFrameOwner => return getFrameOwner(cmd),\n        .getOuterHTML => return getOuterHTML(cmd),\n        .requestNode => return requestNode(cmd),\n    }\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument\nfn getDocument(cmd: anytype) !void {\n    const Params = struct {\n        // CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome\n        depth: i32 = 3,\n        pierce: bool = false,\n    };\n    const params = try cmd.params(Params) orelse Params{};\n\n    if (params.pierce) {\n        log.warn(.not_implemented, \"DOM.getDocument\", .{ .param = \"pierce\" });\n    }\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node = try bc.node_registry.register(page.window._document.asNode());\n    return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch\nfn performSearch(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        query: []const u8,\n        includeUserAgentShadowDOM: ?bool = null,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n    const list = try Selector.querySelectorAll(page.window._document.asNode(), params.query, page);\n    defer list.deinit(page._session);\n\n    const search = try bc.node_search_list.create(list._nodes);\n\n    // dispatch setChildNodesEvents to inform the client of the subpart of node\n    // tree covering the results.\n    try dispatchSetChildNodes(cmd, list._nodes);\n\n    return cmd.sendResult(.{\n        .searchId = search.name,\n        .resultCount = @as(u32, @intCast(search.node_ids.len)),\n    }, .{});\n}\n\n// dispatchSetChildNodes send the setChildNodes event for the whole DOM tree\n// hierarchy of each nodes.\n// We dispatch event in the reverse order: from the top level to the direct parents.\n// We should dispatch a node only if it has never been sent.\nfn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void {\n    const arena = cmd.arena;\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const session_id = bc.session_id orelse return error.SessionIdNotLoaded;\n\n    var parents: std.ArrayList(*Node) = .empty;\n    for (dom_nodes) |dom_node| {\n        var current = dom_node;\n        while (true) {\n            const parent_node = current._parent orelse break;\n\n            const node = try bc.node_registry.register(parent_node);\n            if (node.set_child_nodes_event) {\n                break;\n            }\n            try parents.append(arena, node);\n            current = parent_node;\n        }\n    }\n\n    const plen = parents.items.len;\n    if (plen == 0) {\n        return;\n    }\n\n    var i: usize = plen;\n    // We're going to iterate in reverse order from how we added them.\n    // This ensures that we're emitting the tree of nodes top-down.\n    while (i > 0) {\n        i -= 1;\n        const node = parents.items[i];\n        // Although our above loop won't add an already-sent node to `parents`\n        // this can still be true because two nodes can share the same parent node\n        // so we might have just sent the node a previous iteration of this loop\n        if (node.set_child_nodes_event) continue;\n\n        node.set_child_nodes_event = true;\n\n        // If the node has no parent, it's the root node.\n        // We don't dispatch event for it because we assume the root node is\n        // dispatched via the DOM.getDocument command.\n        const dom_parent = node.dom._parent orelse continue;\n\n        // Retrieve the parent from the registry.\n        const parent_node = try bc.node_registry.register(dom_parent);\n\n        try cmd.sendEvent(\"DOM.setChildNodes\", .{\n            .parentId = parent_node.id,\n            .nodes = .{bc.nodeWriter(node, .{})},\n        }, .{\n            .session_id = session_id,\n        });\n    }\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults\nfn discardSearchResults(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        searchId: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    bc.node_search_list.remove(params.searchId);\n    return cmd.sendResult(null, .{});\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults\nfn getSearchResults(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        searchId: []const u8,\n        fromIndex: u32,\n        toIndex: u32,\n    })) orelse return error.InvalidParams;\n\n    if (params.fromIndex >= params.toIndex) {\n        return error.BadIndices;\n    }\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    const search = bc.node_search_list.get(params.searchId) orelse {\n        return error.SearchResultNotFound;\n    };\n\n    const node_ids = search.node_ids;\n\n    if (params.fromIndex >= node_ids.len) return error.BadFromIndex;\n    if (params.toIndex > node_ids.len) return error.BadToIndex;\n\n    return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{});\n}\n\nfn querySelector(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: Node.Id,\n        selector: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {\n        return cmd.sendError(-32000, \"Could not find node with given id\", .{});\n    };\n\n    const element = try Selector.querySelector(node.dom, params.selector, page) orelse return error.NodeNotFoundForGivenId;\n    const dom_node = element.asNode();\n    const registered_node = try bc.node_registry.register(dom_node);\n\n    // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results.\n    var array = [1]*DOMNode{dom_node};\n    try dispatchSetChildNodes(cmd, array[0..]);\n\n    return cmd.sendResult(.{\n        .nodeId = registered_node.id,\n    }, .{});\n}\n\nfn querySelectorAll(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: Node.Id,\n        selector: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {\n        return cmd.sendError(-32000, \"Could not find node with given id\", .{});\n    };\n\n    const selected_nodes = try Selector.querySelectorAll(node.dom, params.selector, page);\n    defer selected_nodes.deinit(page._session);\n\n    const nodes = selected_nodes._nodes;\n\n    const node_ids = try cmd.arena.alloc(Node.Id, nodes.len);\n    for (nodes, node_ids) |selected_node, *node_id| {\n        node_id.* = (try bc.node_registry.register(selected_node)).id;\n    }\n\n    // Dispatch setChildNodesEvents to inform the client of the subpart of node tree covering the results.\n    try dispatchSetChildNodes(cmd, nodes);\n\n    return cmd.sendResult(.{\n        .nodeIds = node_ids,\n    }, .{});\n}\n\nfn resolveNode(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?u32 = null,\n        objectGroup: ?[]const u8 = null,\n        executionContextId: ?u32 = null,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    var ls: ?js.Local.Scope = null;\n    defer if (ls) |*_ls| {\n        _ls.deinit();\n    };\n\n    if (params.executionContextId) |context_id| blk: {\n        ls = undefined;\n        page.js.localScope(&ls.?);\n        if (ls.?.local.debugContextId() == context_id) {\n            break :blk;\n        }\n        // not the default scope, check the other ones\n        for (bc.isolated_worlds.items) |isolated_world| {\n            ls.?.deinit();\n            ls = null;\n\n            const ctx = (isolated_world.context orelse return error.ContextNotFound);\n            ls = undefined;\n            ctx.localScope(&ls.?);\n            if (ls.?.local.debugContextId() == context_id) {\n                break :blk;\n            }\n        } else return error.ContextNotFound;\n    } else {\n        ls = undefined;\n        page.js.localScope(&ls.?);\n    }\n\n    const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;\n    const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;\n\n    // node._node is a *DOMNode we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement\n    // So we use the Node.Union when retrieve the value from the environment\n    const remote_object = try bc.inspector_session.getRemoteObject(\n        &ls.?.local,\n        params.objectGroup orelse \"\",\n        node.dom,\n    );\n    defer remote_object.deinit();\n\n    const arena = cmd.arena;\n    return cmd.sendResult(.{ .object = .{\n        .type = try remote_object.getType(arena),\n        .subtype = try remote_object.getSubtype(arena),\n        .className = try remote_object.getClassName(arena),\n        .description = try remote_object.getDescription(arena),\n        .objectId = try remote_object.getObjectId(arena),\n    } }, .{});\n}\n\nfn describeNode(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?Node.Id = null,\n        objectId: ?[]const u8 = null,\n        depth: i32 = 1,\n        pierce: bool = false,\n    })) orelse return error.InvalidParams;\n\n    if (params.pierce) {\n        log.warn(.not_implemented, \"DOM.describeNode\", .{ .param = \"pierce\" });\n    }\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);\n\n    return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});\n}\n\n// An array of quad vertices, x immediately followed by y for each point, points clock-wise.\n// Note Y points downward\n// We are assuming the start/endpoint is not repeated.\nconst Quad = [8]f64;\n\nconst BoxModel = struct {\n    content: Quad,\n    padding: Quad,\n    border: Quad,\n    margin: Quad,\n    width: i32,\n    height: i32,\n    // shapeOutside: ?ShapeOutsideInfo,\n};\n\nfn rectToQuad(rect: DOMNode.Element.DOMRect) Quad {\n    return Quad{\n        rect._x,\n        rect._y,\n        rect._x + rect._width,\n        rect._y,\n        rect._x + rect._width,\n        rect._y + rect._height,\n        rect._x,\n        rect._y + rect._height,\n    };\n}\n\nfn scrollIntoViewIfNeeded(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?u32 = null,\n        objectId: ?[]const u8 = null,\n        rect: ?DOMNode.Element.DOMRect = null,\n    })) orelse return error.InvalidParams;\n    // Only 1 of nodeId, backendNodeId, objectId may be set, but chrome just takes the first non-null\n\n    // We retrieve the node to at least check if it exists and is valid.\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);\n\n    switch (node.dom._type) {\n        .element => {},\n        .document => {},\n        .cdata => {},\n        else => return error.NodeDoesNotHaveGeometry,\n    }\n\n    return cmd.sendResult(null, .{});\n}\n\nfn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {\n    const input_node_id = node_id orelse backend_node_id;\n    if (input_node_id) |input_node_id_| {\n        return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;\n    }\n    if (object_id) |object_id_| {\n        const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        // Retrieve the object from which ever context it is in.\n        const parser_node = try bc.inspector_session.getNodePtr(arena, object_id_, &ls.local);\n        return try bc.node_registry.register(@ptrCast(@alignCast(parser_node)));\n    }\n    return error.MissingParams;\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads\n// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface\nfn getContentQuads(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?Node.Id = null,\n        objectId: ?[]const u8 = null,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);\n\n    // TODO likely if the following CSS properties are set the quads should be empty\n    // visibility: hidden\n    // display: none\n\n    const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;\n    // TODO implement for document or text\n    // Most likely document would require some hierachgy in the renderer. It is left unimplemented till we have a good example.\n    // Text may be tricky, multiple quads in case of multiple lines? empty quads of text  = \"\"?\n    // Elements like SVGElement may have multiple quads.\n\n    const quad = rectToQuad(element.getBoundingClientRect(page));\n    return cmd.sendResult(.{ .quads = &.{quad} }, .{});\n}\n\nfn getBoxModel(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?u32 = null,\n        objectId: ?[]const u8 = null,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);\n\n    // TODO implement for document or text\n    const element = node.dom.is(DOMNode.Element) orelse return error.NodeIsNotAnElement;\n\n    const rect = element.getBoundingClientRect(page);\n    const quad = rectToQuad(rect);\n    const zero = [_]f64{0.0} ** 8;\n\n    return cmd.sendResult(.{ .model = BoxModel{\n        .content = quad,\n        .padding = zero,\n        .border = zero,\n        .margin = zero,\n        .width = @intFromFloat(rect._width),\n        .height = @intFromFloat(rect._height),\n    } }, .{});\n}\n\nfn requestChildNodes(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: Node.Id,\n        depth: i32 = 1,\n        pierce: bool = false,\n    })) orelse return error.InvalidParams;\n\n    if (params.depth == 0) return error.InvalidParams;\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const session_id = bc.session_id orelse return error.SessionIdNotLoaded;\n    const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {\n        return error.InvalidNode;\n    };\n\n    try cmd.sendEvent(\"DOM.setChildNodes\", .{\n        .parentId = node.id,\n        .nodes = bc.nodeWriter(node, .{ .depth = params.depth, .exclude_root = true }),\n    }, .{\n        .session_id = session_id,\n    });\n\n    return cmd.sendResult(null, .{});\n}\n\nfn getFrameOwner(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        frameId: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page_frame_id = try id.toPageId(.frame_id, params.frameId);\n\n    const page = bc.session.findPageByFrameId(page_frame_id) orelse {\n        return cmd.sendError(-32000, \"Frame with the given id does not belong to the target.\", .{});\n    };\n\n    const node = try bc.node_registry.register(page.window._document.asNode());\n    return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});\n}\n\nfn getOuterHTML(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?Node.Id = null,\n        objectId: ?[]const u8 = null,\n        includeShadowDOM: bool = false,\n    })) orelse return error.InvalidParams;\n\n    if (params.includeShadowDOM) {\n        log.warn(.not_implemented, \"DOM.getOuterHTML\", .{ .param = \"includeShadowDOM\" });\n    }\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);\n\n    var aw = std.Io.Writer.Allocating.init(cmd.arena);\n    try dump.deep(node.dom, .{}, &aw.writer, page);\n\n    return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});\n}\n\nfn requestNode(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        objectId: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const node = try getNode(cmd.arena, bc, null, null, params.objectId);\n\n    return cmd.sendResult(.{ .nodeId = node.id }, .{});\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"cdp.dom: getSearchResults unknown search id\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    try ctx.processMessage(.{\n        .id = 8,\n        .method = \"DOM.getSearchResults\",\n        .params = .{ .searchId = \"Nope\", .fromIndex = 0, .toIndex = 10 },\n    });\n    try ctx.expectSentError(-31998, \"BrowserContextNotLoaded\", .{ .id = 8 });\n}\n\ntest \"cdp.dom: search flow\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-A\", .url = \"cdp/dom1.html\" });\n\n    try ctx.processMessage(.{\n        .id = 12,\n        .method = \"DOM.performSearch\",\n        .params = .{ .query = \"p\" },\n    });\n    try ctx.expectSentResult(.{ .searchId = \"0\", .resultCount = 2 }, .{ .id = 12 });\n\n    {\n        // getSearchResults\n        try ctx.processMessage(.{\n            .id = 13,\n            .method = \"DOM.getSearchResults\",\n            .params = .{ .searchId = \"0\", .fromIndex = 0, .toIndex = 2 },\n        });\n        try ctx.expectSentResult(.{ .nodeIds = &.{ 1, 2 } }, .{ .id = 13 });\n\n        // different fromIndex\n        try ctx.processMessage(.{\n            .id = 14,\n            .method = \"DOM.getSearchResults\",\n            .params = .{ .searchId = \"0\", .fromIndex = 1, .toIndex = 2 },\n        });\n        try ctx.expectSentResult(.{ .nodeIds = &.{2} }, .{ .id = 14 });\n\n        // different toIndex\n        try ctx.processMessage(.{\n            .id = 15,\n            .method = \"DOM.getSearchResults\",\n            .params = .{ .searchId = \"0\", .fromIndex = 0, .toIndex = 1 },\n        });\n        try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 15 });\n    }\n\n    try ctx.processMessage(.{\n        .id = 16,\n        .method = \"DOM.discardSearchResults\",\n        .params = .{ .searchId = \"0\" },\n    });\n    try ctx.expectSentResult(null, .{ .id = 16 });\n\n    // make sure the delete actually did something\n    try ctx.processMessage(.{\n        .id = 17,\n        .method = \"DOM.getSearchResults\",\n        .params = .{ .searchId = \"0\", .fromIndex = 0, .toIndex = 1 },\n    });\n    try ctx.expectSentError(-31998, \"SearchResultNotFound\", .{ .id = 17 });\n}\n\ntest \"cdp.dom: querySelector unknown search id\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-A\", .url = \"cdp/dom1.html\" });\n\n    try ctx.processMessage(.{\n        .id = 9,\n        .method = \"DOM.querySelector\",\n        .params = .{ .nodeId = 99, .selector = \"\" },\n    });\n    try ctx.expectSentError(-32000, \"Could not find node with given id\", .{});\n\n    try ctx.processMessage(.{\n        .id = 9,\n        .method = \"DOM.querySelectorAll\",\n        .params = .{ .nodeId = 99, .selector = \"\" },\n    });\n    try ctx.expectSentError(-32000, \"Could not find node with given id\", .{});\n}\n\ntest \"cdp.dom: querySelector Node not found\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-A\", .url = \"cdp/dom1.html\" });\n\n    try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry\n        .id = 3,\n        .method = \"DOM.performSearch\",\n        .params = .{ .query = \"p\" },\n    });\n    try ctx.expectSentResult(.{ .searchId = \"0\", .resultCount = 2 }, .{ .id = 3 });\n\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"DOM.querySelector\",\n        .params = .{ .nodeId = 1, .selector = \"a\" },\n    });\n    try ctx.expectSentError(-31998, \"NodeNotFoundForGivenId\", .{ .id = 4 });\n\n    try ctx.processMessage(.{\n        .id = 5,\n        .method = \"DOM.querySelectorAll\",\n        .params = .{ .nodeId = 1, .selector = \"a\" },\n    });\n    try ctx.expectSentResult(.{ .nodeIds = &[_]u32{} }, .{ .id = 5 });\n}\n\ntest \"cdp.dom: querySelector Nodes found\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-A\", .url = \"cdp/dom2.html\" });\n\n    try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry\n        .id = 3,\n        .method = \"DOM.performSearch\",\n        .params = .{ .query = \"div\" },\n    });\n    try ctx.expectSentResult(.{ .searchId = \"0\", .resultCount = 1 }, .{ .id = 3 });\n\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"DOM.querySelector\",\n        .params = .{ .nodeId = 1, .selector = \"p\" },\n    });\n    try ctx.expectSentEvent(\"DOM.setChildNodes\", null, .{});\n    try ctx.expectSentResult(.{ .nodeId = 7 }, .{ .id = 4 });\n\n    try ctx.processMessage(.{\n        .id = 5,\n        .method = \"DOM.querySelectorAll\",\n        .params = .{ .nodeId = 1, .selector = \"p\" },\n    });\n    try ctx.expectSentEvent(\"DOM.setChildNodes\", null, .{});\n    try ctx.expectSentResult(.{ .nodeIds = &.{7} }, .{ .id = 5 });\n}\n\ntest \"cdp.dom: getBoxModel\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-A\", .url = \"cdp/dom2.html\" });\n\n    try ctx.processMessage(.{ // Hacky way to make sure nodeId 1 exists in the registry\n        .id = 3,\n        .method = \"DOM.getDocument\",\n    });\n\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"DOM.querySelector\",\n        .params = .{ .nodeId = 1, .selector = \"p\" },\n    });\n    try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 });\n\n    try ctx.processMessage(.{\n        .id = 5,\n        .method = \"DOM.getBoxModel\",\n        .params = .{ .nodeId = 6 },\n    });\n    try ctx.expectSentResult(.{ .model = BoxModel{\n        .content = Quad{ 10.0, 10.0, 15.0, 10.0, 15.0, 15.0, 10.0, 15.0 },\n        .padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },\n        .border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },\n        .margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },\n        .width = 5,\n        .height = 5,\n    } }, .{ .id = 5 });\n}\n"
  },
  {
    "path": "src/cdp/domains/emulation.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst log = @import(\"../../log.zig\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        setEmulatedMedia,\n        setFocusEmulationEnabled,\n        setDeviceMetricsOverride,\n        setTouchEmulationEnabled,\n        setUserAgentOverride,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .setEmulatedMedia => return setEmulatedMedia(cmd),\n        .setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),\n        .setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),\n        .setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),\n        .setUserAgentOverride => return setUserAgentOverride(cmd),\n    }\n}\n\n// TODO: noop method\nfn setEmulatedMedia(cmd: anytype) !void {\n    // const input = (try const incoming.params(struct {\n    //     media: ?[]const u8 = null,\n    //     features: ?[]struct{\n    //         name: []const u8,\n    //         value: [] const u8\n    //     } = null,\n    // })) orelse return error.InvalidParams;\n\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn setFocusEmulationEnabled(cmd: anytype) !void {\n    // const input = (try const incoming.params(struct {\n    //     enabled: bool,\n    // })) orelse return error.InvalidParams;\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn setDeviceMetricsOverride(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn setTouchEmulationEnabled(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\nfn setUserAgentOverride(cmd: anytype) !void {\n    log.info(.app, \"setUserAgentOverride ignored\", .{});\n    return cmd.sendResult(null, .{});\n}\n"
  },
  {
    "path": "src/cdp/domains/fetch.zig",
    "content": "// Copyright (C) 2023-2025    Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst id = @import(\"../id.zig\");\nconst log = @import(\"../../log.zig\");\nconst network = @import(\"network.zig\");\n\nconst HttpClient = @import(\"../../browser/HttpClient.zig\");\nconst net_http = @import(\"../../network/http.zig\");\nconst Notification = @import(\"../../Notification.zig\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        disable,\n        enable,\n        continueRequest,\n        failRequest,\n        fulfillRequest,\n        continueWithAuth,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .disable => return disable(cmd),\n        .enable => return enable(cmd),\n        .continueRequest => return continueRequest(cmd),\n        .continueWithAuth => return continueWithAuth(cmd),\n        .failRequest => return failRequest(cmd),\n        .fulfillRequest => return fulfillRequest(cmd),\n    }\n}\n\n// Stored in CDP\npub const InterceptState = struct {\n    allocator: Allocator,\n    waiting: std.AutoArrayHashMapUnmanaged(u32, *HttpClient.Transfer),\n\n    pub fn init(allocator: Allocator) !InterceptState {\n        return .{\n            .waiting = .empty,\n            .allocator = allocator,\n        };\n    }\n\n    pub fn empty(self: *const InterceptState) bool {\n        return self.waiting.count() == 0;\n    }\n\n    pub fn put(self: *InterceptState, transfer: *HttpClient.Transfer) !void {\n        return self.waiting.put(self.allocator, transfer.id, transfer);\n    }\n\n    pub fn remove(self: *InterceptState, request_id: u32) ?*HttpClient.Transfer {\n        const entry = self.waiting.fetchSwapRemove(request_id) orelse return null;\n        return entry.value;\n    }\n\n    pub fn deinit(self: *InterceptState) void {\n        self.waiting.deinit(self.allocator);\n    }\n\n    pub fn pendingTransfers(self: *const InterceptState) []*HttpClient.Transfer {\n        return self.waiting.values();\n    }\n};\n\nconst RequestPattern = struct {\n    // Wildcards ('*' -> zero or more, '?' -> exactly one) are allowed.\n    // Escape character is backslash. Omitting is equivalent to \"*\".\n    urlPattern: []const u8 = \"*\",\n    resourceType: ?ResourceType = null,\n    requestStage: RequestStage = .Request,\n};\nconst ResourceType = enum {\n    Document,\n    Stylesheet,\n    Image,\n    Media,\n    Font,\n    Script,\n    TextTrack,\n    XHR,\n    Fetch,\n    Prefetch,\n    EventSource,\n    WebSocket,\n    Manifest,\n    SignedExchange,\n    Ping,\n    CSPViolationReport,\n    Preflight,\n    FedCM,\n    Other,\n};\nconst RequestStage = enum {\n    Request,\n    Response,\n};\n\nconst EnableParam = struct {\n    patterns: []RequestPattern = &.{},\n    handleAuthRequests: bool = false,\n};\nconst ErrorReason = enum {\n    Failed,\n    Aborted,\n    TimedOut,\n    AccessDenied,\n    ConnectionClosed,\n    ConnectionReset,\n    ConnectionRefused,\n    ConnectionAborted,\n    ConnectionFailed,\n    NameNotResolved,\n    InternetDisconnected,\n    AddressUnreachable,\n    BlockedByClient,\n    BlockedByResponse,\n};\n\nfn disable(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    bc.fetchDisable();\n    return cmd.sendResult(null, .{});\n}\n\nfn enable(cmd: anytype) !void {\n    const params = (try cmd.params(EnableParam)) orelse EnableParam{};\n    if (!arePatternsSupported(params.patterns)) {\n        log.warn(.not_implemented, \"Fetch.enable\", .{ .params = \"pattern\" });\n        return cmd.sendResult(null, .{});\n    }\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    try bc.fetchEnable(params.handleAuthRequests);\n\n    return cmd.sendResult(null, .{});\n}\n\nfn arePatternsSupported(patterns: []RequestPattern) bool {\n    if (patterns.len == 0) {\n        return true;\n    }\n    if (patterns.len > 1) {\n        return false;\n    }\n\n    // While we don't support patterns, yet, both Playwright and Puppeteer send\n    // a default pattern which happens to be what we support:\n    // [{\"urlPattern\":\"*\",\"requestStage\":\"Request\"}]\n    // So, rather than erroring on this case because we don't support patterns,\n    // we'll allow it, because this pattern is how it works as-is.\n    const pattern = patterns[0];\n    if (!std.mem.eql(u8, pattern.urlPattern, \"*\")) {\n        return false;\n    }\n    if (pattern.resourceType != null) {\n        return false;\n    }\n    if (pattern.requestStage != .Request) {\n        return false;\n    }\n    return true;\n}\n\npub fn requestIntercept(bc: anytype, intercept: *const Notification.RequestIntercept) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n\n    // We keep it around to wait for modifications to the request.\n    // NOTE: we assume whomever created the request created it with a lifetime of the Page.\n    // TODO: What to do when receiving replies for a previous page's requests?\n\n    const transfer = intercept.transfer;\n    try bc.intercept_state.put(transfer);\n\n    try bc.cdp.sendEvent(\"Fetch.requestPaused\", .{\n        .requestId = &id.toInterceptId(transfer.id),\n        .frameId = &id.toFrameId(transfer.req.frame_id),\n        .request = network.TransferAsRequestWriter.init(transfer),\n        .resourceType = switch (transfer.req.resource_type) {\n            .script => \"Script\",\n            .xhr => \"XHR\",\n            .document => \"Document\",\n            .fetch => \"Fetch\",\n        },\n        .networkId = &id.toRequestId(transfer.id), // matches the Network REQ-ID\n    }, .{ .session_id = session_id });\n\n    log.debug(.cdp, \"request intercept\", .{\n        .state = \"paused\",\n        .id = transfer.id,\n        .url = transfer.url,\n    });\n    // Await either continueRequest, failRequest or fulfillRequest\n\n    intercept.wait_for_interception.* = true;\n}\n\nfn continueRequest(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(struct {\n        requestId: []const u8, // INT-{d}\"\n        url: ?[]const u8 = null,\n        method: ?[]const u8 = null,\n        postData: ?[]const u8 = null,\n        headers: ?[]const net_http.Header = null,\n        interceptResponse: bool = false,\n    })) orelse return error.InvalidParams;\n\n    if (params.interceptResponse) {\n        return error.NotImplemented;\n    }\n\n    var intercept_state = &bc.intercept_state;\n    const request_id = try idFromRequestId(params.requestId);\n    const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;\n\n    log.debug(.cdp, \"request intercept\", .{\n        .state = \"continue\",\n        .id = transfer.id,\n        .url = transfer.url,\n        .new_url = params.url,\n    });\n\n    const arena = transfer.arena.allocator();\n    // Update the request with the new parameters\n    if (params.url) |url| {\n        try transfer.updateURL(try arena.dupeZ(u8, url));\n    }\n    if (params.method) |method| {\n        transfer.req.method = std.meta.stringToEnum(net_http.Method, method) orelse return error.InvalidParams;\n    }\n\n    if (params.headers) |headers| {\n        // Not obvious, but cmd.arena is safe here, since the headers will get\n        // duped by libcurl. transfer.arena is more obvious/safe, but cmd.arena\n        // is more efficient (it's re-used)\n        try transfer.replaceRequestHeaders(cmd.arena, headers);\n    }\n\n    if (params.postData) |b| {\n        const decoder = std.base64.standard.Decoder;\n        const body = try arena.alloc(u8, try decoder.calcSizeForSlice(b));\n        try decoder.decode(body, b);\n        transfer.req.body = body;\n    }\n\n    try bc.cdp.browser.http_client.continueTransfer(transfer);\n    return cmd.sendResult(null, .{});\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#type-AuthChallengeResponse\nconst AuthChallengeResponse = enum {\n    Default,\n    CancelAuth,\n    ProvideCredentials,\n};\n\nfn continueWithAuth(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(struct {\n        requestId: []const u8, // \"INT-{d}\"\n        authChallengeResponse: struct {\n            response: AuthChallengeResponse,\n            username: []const u8 = \"\",\n            password: []const u8 = \"\",\n        },\n    })) orelse return error.InvalidParams;\n\n    var intercept_state = &bc.intercept_state;\n    const request_id = try idFromRequestId(params.requestId);\n    const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;\n\n    log.debug(.cdp, \"request intercept\", .{\n        .state = \"continue with auth\",\n        .id = transfer.id,\n        .response = params.authChallengeResponse.response,\n    });\n\n    if (params.authChallengeResponse.response != .ProvideCredentials) {\n        transfer.abortAuthChallenge();\n        return cmd.sendResult(null, .{});\n    }\n\n    // cancel the request, deinit the transfer on error.\n    errdefer transfer.abortAuthChallenge();\n\n    // restart the request with the provided credentials.\n    const arena = transfer.arena.allocator();\n    transfer.updateCredentials(\n        try std.fmt.allocPrintSentinel(arena, \"{s}:{s}\", .{\n            params.authChallengeResponse.username,\n            params.authChallengeResponse.password,\n        }, 0),\n    );\n\n    transfer.reset();\n    try bc.cdp.browser.http_client.continueTransfer(transfer);\n    return cmd.sendResult(null, .{});\n}\n\nfn fulfillRequest(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    const params = (try cmd.params(struct {\n        requestId: []const u8, // \"INT-{d}\"\n        responseCode: u16,\n        responseHeaders: ?[]const net_http.Header = null,\n        binaryResponseHeaders: ?[]const u8 = null,\n        body: ?[]const u8 = null,\n        responsePhrase: ?[]const u8 = null,\n    })) orelse return error.InvalidParams;\n\n    if (params.binaryResponseHeaders != null) {\n        log.warn(.not_implemented, \"Fetch.fulfillRequest\", .{ .param = \"binaryResponseHeaders\" });\n        return error.NotImplemented;\n    }\n\n    var intercept_state = &bc.intercept_state;\n    const request_id = try idFromRequestId(params.requestId);\n    const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;\n\n    log.debug(.cdp, \"request intercept\", .{\n        .state = \"fulfilled\",\n        .id = transfer.id,\n        .url = transfer.url,\n        .status = params.responseCode,\n        .body = params.body != null,\n    });\n\n    var body: ?[]const u8 = null;\n    if (params.body) |b| {\n        const decoder = std.base64.standard.Decoder;\n        const buf = try transfer.arena.allocator().alloc(u8, try decoder.calcSizeForSlice(b));\n        try decoder.decode(buf, b);\n        body = buf;\n    }\n\n    try bc.cdp.browser.http_client.fulfillTransfer(transfer, params.responseCode, params.responseHeaders orelse &.{}, body);\n\n    return cmd.sendResult(null, .{});\n}\n\nfn failRequest(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(struct {\n        requestId: []const u8, // \"INT-{d}\"\n        errorReason: ErrorReason,\n    })) orelse return error.InvalidParams;\n\n    var intercept_state = &bc.intercept_state;\n    const request_id = try idFromRequestId(params.requestId);\n\n    const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;\n    defer bc.cdp.browser.http_client.abortTransfer(transfer);\n\n    log.info(.cdp, \"request intercept\", .{\n        .state = \"fail\",\n        .id = request_id,\n        .url = transfer.url,\n        .reason = params.errorReason,\n    });\n    return cmd.sendResult(null, .{});\n}\n\npub fn requestAuthRequired(bc: anytype, intercept: *const Notification.RequestAuthRequired) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n\n    // We keep it around to wait for modifications to the request.\n    // NOTE: we assume whomever created the request created it with a lifetime of the Page.\n    // TODO: What to do when receiving replies for a previous page's requests?\n\n    const transfer = intercept.transfer;\n    try bc.intercept_state.put(transfer);\n\n    const challenge = transfer._auth_challenge orelse return error.NullAuthChallenge;\n\n    try bc.cdp.sendEvent(\"Fetch.authRequired\", .{\n        .requestId = &id.toInterceptId(transfer.id),\n        .frameId = &id.toFrameId(transfer.req.frame_id),\n        .request = network.TransferAsRequestWriter.init(transfer),\n        .resourceType = switch (transfer.req.resource_type) {\n            .script => \"Script\",\n            .xhr => \"XHR\",\n            .document => \"Document\",\n            .fetch => \"Fetch\",\n        },\n        .authChallenge = .{\n            .origin = \"\", // TODO get origin, could be the proxy address for example.\n            .source = if (challenge.source) |s| (if (s == .server) \"Server\" else \"Proxy\") else \"\",\n            .scheme = if (challenge.scheme) |s| (if (s == .digest) \"digest\" else \"basic\") else \"\",\n            .realm = challenge.realm orelse \"\",\n        },\n        .networkId = &id.toRequestId(transfer.id),\n    }, .{ .session_id = session_id });\n\n    log.debug(.cdp, \"request auth required\", .{\n        .state = \"paused\",\n        .id = transfer.id,\n        .url = transfer.url,\n    });\n    // Await continueWithAuth\n\n    intercept.wait_for_interception.* = true;\n}\n\n// Get u32 from requestId which is formatted as: \"INT-{d}\"\nfn idFromRequestId(request_id: []const u8) !u32 {\n    if (!std.mem.startsWith(u8, request_id, \"INT-\")) {\n        return error.InvalidParams;\n    }\n    return std.fmt.parseInt(u32, request_id[4..], 10) catch return error.InvalidParams;\n}\n"
  },
  {
    "path": "src/cdp/domains/input.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        dispatchKeyEvent,\n        dispatchMouseEvent,\n        insertText,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .dispatchKeyEvent => return dispatchKeyEvent(cmd),\n        .dispatchMouseEvent => return dispatchMouseEvent(cmd),\n        .insertText => return insertText(cmd),\n    }\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent\nfn dispatchKeyEvent(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        type: Type,\n        key: []const u8 = \"\",\n        code: ?[]const u8 = null,\n        modifiers: u4 = 0,\n        // Many optional parameters are not implemented yet, see documentation url.\n\n        const Type = enum {\n            keyDown,\n            keyUp,\n            rawKeyDown,\n            char,\n        };\n    })) orelse return error.InvalidParams;\n\n    try cmd.sendResult(null, .{});\n\n    // quickly ignore types we know we don't handle\n    switch (params.type) {\n        .keyUp, .rawKeyDown, .char => return,\n        .keyDown => {},\n    }\n\n    const bc = cmd.browser_context orelse return;\n    const page = bc.session.currentPage() orelse return;\n\n    const KeyboardEvent = @import(\"../../browser/webapi/event/KeyboardEvent.zig\");\n    const keyboard_event = try KeyboardEvent.initTrusted(comptime .wrap(\"keydown\"), .{\n        .key = params.key,\n        .code = params.code,\n        .altKey = params.modifiers & 1 == 1,\n        .ctrlKey = params.modifiers & 2 == 2,\n        .metaKey = params.modifiers & 4 == 4,\n        .shiftKey = params.modifiers & 8 == 8,\n    }, page);\n    try page.triggerKeyboard(keyboard_event);\n    // result already sent\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent\nfn dispatchMouseEvent(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        x: f64,\n        y: f64,\n        type: Type,\n        // Many optional parameters are not implemented yet, see documentation url.\n\n        const Type = enum {\n            mousePressed,\n            mouseReleased,\n            mouseMoved,\n            mouseWheel,\n        };\n    })) orelse return error.InvalidParams;\n\n    try cmd.sendResult(null, .{});\n\n    // quickly ignore types we know we don't handle\n    switch (params.type) {\n        .mouseMoved, .mouseWheel, .mouseReleased => return,\n        else => {},\n    }\n\n    const bc = cmd.browser_context orelse return;\n    const page = bc.session.currentPage() orelse return;\n    try page.triggerMouseClick(params.x, params.y);\n    // result already sent\n}\n\n// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText\nfn insertText(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        text: []const u8, // The text to insert\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return;\n    const page = bc.session.currentPage() orelse return;\n\n    try page.insertText(params.text);\n\n    try cmd.sendResult(null, .{});\n}\n"
  },
  {
    "path": "src/cdp/domains/inspector.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        disable,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return cmd.sendResult(null, .{}),\n        .disable => return cmd.sendResult(null, .{}),\n    }\n}\n"
  },
  {
    "path": "src/cdp/domains/log.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        disable,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable, .disable => return cmd.sendResult(null, .{}),\n    }\n}\n"
  },
  {
    "path": "src/cdp/domains/lp.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst log = @import(\"../../log.zig\");\nconst markdown = lp.markdown;\nconst SemanticTree = lp.SemanticTree;\nconst interactive = lp.interactive;\nconst structured_data = lp.structured_data;\nconst Node = @import(\"../Node.zig\");\nconst DOMNode = @import(\"../../browser/webapi/Node.zig\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        getMarkdown,\n        getSemanticTree,\n        getInteractiveElements,\n        getStructuredData,\n        clickNode,\n        fillNode,\n        scrollNode,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .getMarkdown => return getMarkdown(cmd),\n        .getSemanticTree => return getSemanticTree(cmd),\n        .getInteractiveElements => return getInteractiveElements(cmd),\n        .getStructuredData => return getStructuredData(cmd),\n        .clickNode => return clickNode(cmd),\n        .fillNode => return fillNode(cmd),\n        .scrollNode => return scrollNode(cmd),\n    }\n}\n\nfn getSemanticTree(cmd: anytype) !void {\n    const Params = struct {\n        format: ?enum { text } = null,\n        prune: ?bool = null,\n        interactiveOnly: ?bool = null,\n        backendNodeId: ?Node.Id = null,\n        maxDepth: ?u32 = null,\n    };\n    const params = (try cmd.params(Params)) orelse Params{};\n\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const dom_node = if (params.backendNodeId) |nodeId|\n        (bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom\n    else\n        page.document.asNode();\n\n    var st = SemanticTree{\n        .dom_node = dom_node,\n        .registry = &bc.node_registry,\n        .page = page,\n        .arena = cmd.arena,\n        .prune = params.prune orelse true,\n        .interactive_only = params.interactiveOnly orelse false,\n        .max_depth = params.maxDepth orelse std.math.maxInt(u32) - 1,\n    };\n\n    if (params.format) |format| {\n        if (format == .text) {\n            var aw: std.Io.Writer.Allocating = .init(cmd.arena);\n            defer aw.deinit();\n            try st.textStringify(&aw.writer);\n\n            return cmd.sendResult(.{\n                .semanticTree = aw.written(),\n            }, .{});\n        }\n    }\n\n    return cmd.sendResult(.{\n        .semanticTree = st,\n    }, .{});\n}\n\nfn getMarkdown(cmd: anytype) !void {\n    const Params = struct {\n        nodeId: ?Node.Id = null,\n    };\n    const params = (try cmd.params(Params)) orelse Params{};\n\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const dom_node = if (params.nodeId) |nodeId|\n        (bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom\n    else\n        page.document.asNode();\n\n    var aw: std.Io.Writer.Allocating = .init(cmd.arena);\n    defer aw.deinit();\n    try markdown.dump(dom_node, .{}, &aw.writer, page);\n\n    return cmd.sendResult(.{\n        .markdown = aw.written(),\n    }, .{});\n}\n\nfn getInteractiveElements(cmd: anytype) !void {\n    const Params = struct {\n        nodeId: ?Node.Id = null,\n    };\n    const params = (try cmd.params(Params)) orelse Params{};\n\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const root = if (params.nodeId) |nodeId|\n        (bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom\n    else\n        page.document.asNode();\n\n    const elements = try interactive.collectInteractiveElements(root, cmd.arena, page);\n\n    // Register nodes so nodeIds are valid for subsequent CDP calls.\n    var node_ids: std.ArrayList(Node.Id) = try .initCapacity(cmd.arena, elements.len);\n    for (elements) |el| {\n        const registered = try bc.node_registry.register(el.node);\n        node_ids.appendAssumeCapacity(registered.id);\n    }\n\n    return cmd.sendResult(.{\n        .elements = elements,\n        .nodeIds = node_ids.items,\n    }, .{});\n}\n\nfn getStructuredData(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const data = try structured_data.collectStructuredData(\n        page.document.asNode(),\n        cmd.arena,\n        page,\n    );\n\n    return cmd.sendResult(.{\n        .structuredData = data,\n    }, .{});\n}\n\nfn clickNode(cmd: anytype) !void {\n    const Params = struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?Node.Id = null,\n    };\n    const params = (try cmd.params(Params)) orelse return error.InvalidParam;\n\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;\n    const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;\n\n    lp.actions.click(node.dom, page) catch |err| {\n        if (err == error.InvalidNodeType) return error.InvalidParam;\n        return error.InternalError;\n    };\n\n    return cmd.sendResult(.{}, .{});\n}\n\nfn fillNode(cmd: anytype) !void {\n    const Params = struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?Node.Id = null,\n        text: []const u8,\n    };\n    const params = (try cmd.params(Params)) orelse return error.InvalidParam;\n\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;\n    const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;\n\n    lp.actions.fill(node.dom, params.text, page) catch |err| {\n        if (err == error.InvalidNodeType) return error.InvalidParam;\n        return error.InternalError;\n    };\n\n    return cmd.sendResult(.{}, .{});\n}\n\nfn scrollNode(cmd: anytype) !void {\n    const Params = struct {\n        nodeId: ?Node.Id = null,\n        backendNodeId: ?Node.Id = null,\n        x: ?i32 = null,\n        y: ?i32 = null,\n    };\n    const params = (try cmd.params(Params)) orelse return error.InvalidParam;\n\n    const bc = cmd.browser_context orelse return error.NoBrowserContext;\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const maybe_node_id = params.nodeId orelse params.backendNodeId;\n\n    var target_node: ?*DOMNode = null;\n    if (maybe_node_id) |node_id| {\n        const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;\n        target_node = node.dom;\n    }\n\n    lp.actions.scroll(target_node, params.x, params.y, page) catch |err| {\n        if (err == error.InvalidNodeType) return error.InvalidParam;\n        return error.InternalError;\n    };\n\n    return cmd.sendResult(.{}, .{});\n}\nconst testing = @import(\"../testing.zig\");\ntest \"cdp.lp: getMarkdown\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    const bc = try ctx.loadBrowserContext(.{});\n    _ = try bc.session.createPage();\n\n    try ctx.processMessage(.{\n        .id = 1,\n        .method = \"LP.getMarkdown\",\n    });\n\n    const result = ctx.client.?.sent.items[0].object.get(\"result\").?.object;\n    try testing.expect(result.get(\"markdown\") != null);\n}\n\ntest \"cdp.lp: getInteractiveElements\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    const bc = try ctx.loadBrowserContext(.{});\n    _ = try bc.session.createPage();\n\n    try ctx.processMessage(.{\n        .id = 1,\n        .method = \"LP.getInteractiveElements\",\n    });\n\n    const result = ctx.client.?.sent.items[0].object.get(\"result\").?.object;\n    try testing.expect(result.get(\"elements\") != null);\n    try testing.expect(result.get(\"nodeIds\") != null);\n}\n\ntest \"cdp.lp: getStructuredData\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    const bc = try ctx.loadBrowserContext(.{});\n    _ = try bc.session.createPage();\n\n    try ctx.processMessage(.{\n        .id = 1,\n        .method = \"LP.getStructuredData\",\n    });\n\n    const result = ctx.client.?.sent.items[0].object.get(\"result\").?.object;\n    try testing.expect(result.get(\"structuredData\") != null);\n}\n\ntest \"cdp.lp: action tools\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    const bc = try ctx.loadBrowserContext(.{});\n    const page = try bc.session.createPage();\n    const url = \"http://localhost:9582/src/browser/tests/mcp_actions.html\";\n    try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });\n    _ = bc.session.wait(5000);\n\n    // Test Click\n    const btn = page.document.getElementById(\"btn\", page).?.asNode();\n    const btn_id = (try bc.node_registry.register(btn)).id;\n    try ctx.processMessage(.{\n        .id = 1,\n        .method = \"LP.clickNode\",\n        .params = .{ .backendNodeId = btn_id },\n    });\n\n    // Test Fill Input\n    const inp = page.document.getElementById(\"inp\", page).?.asNode();\n    const inp_id = (try bc.node_registry.register(inp)).id;\n    try ctx.processMessage(.{\n        .id = 2,\n        .method = \"LP.fillNode\",\n        .params = .{ .backendNodeId = inp_id, .text = \"hello\" },\n    });\n\n    // Test Fill Select\n    const sel = page.document.getElementById(\"sel\", page).?.asNode();\n    const sel_id = (try bc.node_registry.register(sel)).id;\n    try ctx.processMessage(.{\n        .id = 3,\n        .method = \"LP.fillNode\",\n        .params = .{ .backendNodeId = sel_id, .text = \"opt2\" },\n    });\n\n    // Test Scroll\n    const scrollbox = page.document.getElementById(\"scrollbox\", page).?.asNode();\n    const scrollbox_id = (try bc.node_registry.register(scrollbox)).id;\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"LP.scrollNode\",\n        .params = .{ .backendNodeId = scrollbox_id, .y = 50 },\n    });\n\n    // Evaluate assertions\n    var ls: lp.js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var try_catch: lp.js.TryCatch = undefined;\n    try_catch.init(&ls.local);\n    defer try_catch.deinit();\n\n    const result = try ls.local.compileAndRun(\"window.clicked === true && window.inputVal === 'hello' && window.changed === true && window.selChanged === 'opt2' && window.scrolled === true\", null);\n\n    try testing.expect(result.isTrue());\n}\n"
  },
  {
    "path": "src/cdp/domains/network.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst Allocator = std.mem.Allocator;\nconst log = @import(\"../../log.zig\");\n\nconst CdpStorage = @import(\"storage.zig\");\n\nconst id = @import(\"../id.zig\");\nconst URL = @import(\"../../browser/URL.zig\");\nconst Transfer = @import(\"../../browser/HttpClient.zig\").Transfer;\nconst Notification = @import(\"../../Notification.zig\");\nconst Mime = @import(\"../../browser/Mime.zig\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        disable,\n        setCacheDisabled,\n        setExtraHTTPHeaders,\n        setUserAgentOverride,\n        deleteCookies,\n        clearBrowserCookies,\n        setCookie,\n        setCookies,\n        getCookies,\n        getResponseBody,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return enable(cmd),\n        .disable => return disable(cmd),\n        .setCacheDisabled => return cmd.sendResult(null, .{}),\n        .setUserAgentOverride => return cmd.sendResult(null, .{}),\n        .setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),\n        .deleteCookies => return deleteCookies(cmd),\n        .clearBrowserCookies => return clearBrowserCookies(cmd),\n        .setCookie => return setCookie(cmd),\n        .setCookies => return setCookies(cmd),\n        .getCookies => return getCookies(cmd),\n        .getResponseBody => return getResponseBody(cmd),\n    }\n}\n\nfn enable(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    try bc.networkEnable();\n    return cmd.sendResult(null, .{});\n}\n\nfn disable(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    bc.networkDisable();\n    return cmd.sendResult(null, .{});\n}\n\nfn setExtraHTTPHeaders(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        headers: std.json.ArrayHashMap([]const u8),\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    // Copy the headers onto the browser context arena\n    const arena = bc.arena;\n    const extra_headers = &bc.extra_headers;\n\n    extra_headers.clearRetainingCapacity();\n    try extra_headers.ensureTotalCapacity(arena, params.headers.map.count());\n    var it = params.headers.map.iterator();\n    while (it.next()) |header| {\n        const header_string = try std.fmt.allocPrintSentinel(arena, \"{s}: {s}\", .{ header.key_ptr.*, header.value_ptr.* }, 0);\n        extra_headers.appendAssumeCapacity(header_string);\n    }\n\n    return cmd.sendResult(null, .{});\n}\n\nconst Cookie = @import(\"../../browser/webapi/storage/storage.zig\").Cookie;\n\n// Only matches the cookie on provided parameters\nfn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool {\n    if (!std.mem.eql(u8, cookie.name, name)) return false;\n\n    if (domain) |domain_| {\n        const c_no_dot = if (std.mem.startsWith(u8, cookie.domain, \".\")) cookie.domain[1..] else cookie.domain;\n        const d_no_dot = if (std.mem.startsWith(u8, domain_, \".\")) domain_[1..] else domain_;\n        if (!std.mem.eql(u8, c_no_dot, d_no_dot)) return false;\n    }\n    if (path) |path_| {\n        if (!std.mem.eql(u8, cookie.path, path_)) return false;\n    }\n    return true;\n}\n\nfn deleteCookies(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        name: []const u8,\n        url: ?[:0]const u8 = null,\n        domain: ?[]const u8 = null,\n        path: ?[]const u8 = null,\n        partitionKey: ?CdpStorage.CookiePartitionKey = null,\n    })) orelse return error.InvalidParams;\n    // Silently ignore partitionKey since we don't support partitioned cookies (CHIPS).\n    // This allows Puppeteer's page.setCookie() to work, which sends deleteCookies\n    // with partitionKey as part of its cookie-setting workflow.\n    if (params.partitionKey != null) {\n        log.warn(.not_implemented, \"partition key\", .{ .src = \"deleteCookies\" });\n    }\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const cookies = &bc.session.cookie_jar.cookies;\n\n    var index = cookies.items.len;\n    while (index > 0) {\n        index -= 1;\n        const cookie = &cookies.items[index];\n        const domain = try Cookie.parseDomain(cmd.arena, params.url, params.domain);\n        const path = try Cookie.parsePath(cmd.arena, params.url, params.path);\n\n        // We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.\n        // Similar to deduplicating with areCookiesEqual, except domain and path are optional.\n        if (cookieMatches(cookie, params.name, domain, path)) {\n            cookies.swapRemove(index).deinit();\n        }\n    }\n    return cmd.sendResult(null, .{});\n}\n\nfn clearBrowserCookies(cmd: anytype) !void {\n    if (try cmd.params(struct {}) != null) return error.InvalidParams;\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    bc.session.cookie_jar.clearRetainingCapacity();\n    return cmd.sendResult(null, .{});\n}\n\nfn setCookie(cmd: anytype) !void {\n    const params = (try cmd.params(\n        CdpStorage.CdpCookie,\n    )) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    try CdpStorage.setCdpCookie(&bc.session.cookie_jar, params);\n\n    try cmd.sendResult(.{ .success = true }, .{});\n}\n\nfn setCookies(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        cookies: []const CdpStorage.CdpCookie,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    for (params.cookies) |param| {\n        try CdpStorage.setCdpCookie(&bc.session.cookie_jar, param);\n    }\n\n    try cmd.sendResult(null, .{});\n}\n\nconst GetCookiesParam = struct {\n    urls: ?[]const [:0]const u8 = null,\n};\nfn getCookies(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};\n\n    // If not specified, use the URLs of the page and all of its subframes. TODO subframes\n    const page_url = if (bc.session.page) |page| page.url else null;\n    const param_urls = params.urls orelse &[_][:0]const u8{page_url orelse return error.InvalidParams};\n\n    var urls = try std.ArrayList(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);\n    for (param_urls) |url| {\n        urls.appendAssumeCapacity(.{\n            .host = try Cookie.parseDomain(cmd.arena, url, null),\n            .path = try Cookie.parsePath(cmd.arena, url, null),\n            .secure = URL.isHTTPS(url),\n        });\n    }\n\n    var jar = &bc.session.cookie_jar;\n    jar.removeExpired(null);\n    const writer = CdpStorage.CookieWriter{ .cookies = jar.cookies.items, .urls = urls.items };\n    try cmd.sendResult(.{ .cookies = writer }, .{});\n}\n\nfn getResponseBody(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        requestId: []const u8, // \"REQ-{d}\"\n    })) orelse return error.InvalidParams;\n\n    const request_id = try idFromRequestId(params.requestId);\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const buf = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound;\n\n    try cmd.sendResult(.{\n        .body = buf.items,\n        .base64Encoded = false,\n    }, .{});\n}\n\npub fn httpRequestFail(bc: anytype, msg: *const Notification.RequestFail) !void {\n    // It's possible that the request failed because we aborted when the client\n    // sent Target.closeTarget. In that case, bc.session_id will be cleared\n    // already, and we can skip sending these messages to the client.\n    const session_id = bc.session_id orelse return;\n\n    // Isn't possible to do a network request within a Browser (which our\n    // notification is tied to), without a page.\n    lp.assert(bc.session.page != null, \"CDP.network.httpRequestFail null page\", .{});\n\n    // We're missing a bunch of fields, but, for now, this seems like enough\n    try bc.cdp.sendEvent(\"Network.loadingFailed\", .{\n        .requestId = &id.toRequestId(msg.transfer.id),\n        // Seems to be what chrome answers with. I assume it depends on the type of error?\n        .type = \"Ping\",\n        .errorText = msg.err,\n        .canceled = false,\n    }, .{ .session_id = session_id });\n}\n\npub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n\n    const transfer = msg.transfer;\n    const req = &transfer.req;\n    const frame_id = req.frame_id;\n    const page = bc.session.findPageByFrameId(frame_id) orelse return;\n\n    // Modify request with extra CDP headers\n    for (bc.extra_headers.items) |extra| {\n        try req.headers.add(extra);\n    }\n\n    // We're missing a bunch of fields, but, for now, this seems like enough\n    try bc.cdp.sendEvent(\"Network.requestWillBeSent\", .{\n        .loaderId = &id.toLoaderId(transfer.id),\n        .requestId = &id.toRequestId(transfer.id),\n        .frameId = &id.toFrameId(frame_id),\n        .type = req.resource_type.string(),\n        .documentURL = page.url,\n        .request = TransferAsRequestWriter.init(transfer),\n        .initiator = .{ .type = \"other\" },\n        .redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo\n        .hasUserGesture = false,\n    }, .{ .session_id = session_id });\n}\n\npub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n\n    const transfer = msg.transfer;\n\n    // We're missing a bunch of fields, but, for now, this seems like enough\n    try bc.cdp.sendEvent(\"Network.responseReceived\", .{\n        .loaderId = &id.toLoaderId(transfer.id),\n        .requestId = &id.toRequestId(transfer.id),\n        .frameId = &id.toFrameId(transfer.req.frame_id),\n        .response = TransferAsResponseWriter.init(arena, msg.transfer),\n        .hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo\n    }, .{ .session_id = session_id });\n}\n\npub fn httpRequestDone(bc: anytype, msg: *const Notification.RequestDone) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n    const transfer = msg.transfer;\n    try bc.cdp.sendEvent(\"Network.loadingFinished\", .{\n        .requestId = &id.toRequestId(transfer.id),\n        .encodedDataLength = transfer.bytes_received,\n    }, .{ .session_id = session_id });\n}\n\npub const TransferAsRequestWriter = struct {\n    transfer: *Transfer,\n\n    pub fn init(transfer: *Transfer) TransferAsRequestWriter {\n        return .{\n            .transfer = transfer,\n        };\n    }\n\n    pub fn jsonStringify(self: *const TransferAsRequestWriter, jws: anytype) !void {\n        self._jsonStringify(jws) catch return error.WriteFailed;\n    }\n    fn _jsonStringify(self: *const TransferAsRequestWriter, jws: anytype) !void {\n        const transfer = self.transfer;\n\n        try jws.beginObject();\n        {\n            try jws.objectField(\"url\");\n            try jws.write(transfer.url);\n        }\n\n        {\n            const frag = URL.getHash(transfer.url);\n            if (frag.len > 0) {\n                try jws.objectField(\"urlFragment\");\n                try jws.write(frag);\n            }\n        }\n\n        {\n            try jws.objectField(\"method\");\n            try jws.write(@tagName(transfer.req.method));\n        }\n\n        {\n            try jws.objectField(\"hasPostData\");\n            try jws.write(transfer.req.body != null);\n        }\n\n        {\n            try jws.objectField(\"headers\");\n            try jws.beginObject();\n            var it = transfer.req.headers.iterator();\n            while (it.next()) |hdr| {\n                try jws.objectField(hdr.name);\n                try jws.write(hdr.value);\n            }\n            try jws.endObject();\n        }\n        try jws.endObject();\n    }\n};\n\nconst TransferAsResponseWriter = struct {\n    arena: Allocator,\n    transfer: *Transfer,\n\n    fn init(arena: Allocator, transfer: *Transfer) TransferAsResponseWriter {\n        return .{\n            .arena = arena,\n            .transfer = transfer,\n        };\n    }\n\n    pub fn jsonStringify(self: *const TransferAsResponseWriter, jws: anytype) !void {\n        self._jsonStringify(jws) catch return error.WriteFailed;\n    }\n\n    fn _jsonStringify(self: *const TransferAsResponseWriter, jws: anytype) !void {\n        const transfer = self.transfer;\n\n        try jws.beginObject();\n        {\n            try jws.objectField(\"url\");\n            try jws.write(transfer.url);\n        }\n\n        if (transfer.response_header) |*rh| {\n            // it should not be possible for this to be false, but I'm not\n            // feeling brave today.\n            const status = rh.status;\n            try jws.objectField(\"status\");\n            try jws.write(status);\n\n            try jws.objectField(\"statusText\");\n            try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse \"Unknown\");\n        }\n\n        {\n            const mime: Mime = blk: {\n                if (transfer.response_header.?.contentType()) |ct| {\n                    break :blk try Mime.parse(ct);\n                }\n                break :blk .unknown;\n            };\n\n            try jws.objectField(\"mimeType\");\n            try jws.write(mime.contentTypeString());\n            try jws.objectField(\"charset\");\n            try jws.write(mime.charsetString());\n        }\n\n        {\n            // chromedp doesn't like having duplicate header names. It's pretty\n            // common to get these from a server (e.g. for Cache-Control), but\n            // Chrome joins these. So we have to too.\n            const arena = self.arena;\n            var it = transfer.responseHeaderIterator();\n            var map: std.StringArrayHashMapUnmanaged([]const u8) = .empty;\n            while (it.next()) |hdr| {\n                const gop = try map.getOrPut(arena, hdr.name);\n                if (gop.found_existing) {\n                    // yes, chrome joins multi-value headers with a \\n\n                    gop.value_ptr.* = try std.mem.join(arena, \"\\n\", &.{ gop.value_ptr.*, hdr.value });\n                } else {\n                    gop.value_ptr.* = hdr.value;\n                }\n            }\n\n            try jws.objectField(\"headers\");\n            try jws.write(std.json.ArrayHashMap([]const u8){ .map = map });\n        }\n        try jws.endObject();\n    }\n};\n\nfn idFromRequestId(request_id: []const u8) !u64 {\n    if (!std.mem.startsWith(u8, request_id, \"REQ-\")) {\n        return error.InvalidParams;\n    }\n    return std.fmt.parseInt(u64, request_id[4..], 10) catch return error.InvalidParams;\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"cdp.network setExtraHTTPHeaders\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"NID-A\", .session_id = \"NESI-A\" });\n    // try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .url = \"about/blank\" } });\n\n    try ctx.processMessage(.{\n        .id = 3,\n        .method = \"Network.setExtraHTTPHeaders\",\n        .params = .{ .headers = .{ .foo = \"bar\" } },\n    });\n\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"Network.setExtraHTTPHeaders\",\n        .params = .{ .headers = .{ .food = \"bars\" } },\n    });\n\n    const bc = ctx.cdp().browser_context.?;\n    try testing.expectEqual(bc.extra_headers.items.len, 1);\n}\n\ntest \"cdp.Network: cookies\" {\n    const ResCookie = CdpStorage.ResCookie;\n    const CdpCookie = CdpStorage.CdpCookie;\n\n    var ctx = testing.context();\n    defer ctx.deinit();\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-S\" });\n\n    // Initially empty\n    try ctx.processMessage(.{\n        .id = 3,\n        .method = \"Network.getCookies\",\n        .params = .{ .urls = &[_][]const u8{\"https://example.com/pancakes\"} },\n    });\n    try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });\n\n    // Has cookies after setting them\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"Network.setCookie\",\n        .params = CdpCookie{ .name = \"test3\", .value = \"valuenot3\", .url = \"https://car.example.com/defnotpancakes\" },\n    });\n    try ctx.expectSentResult(null, .{ .id = 4 });\n    try ctx.processMessage(.{\n        .id = 5,\n        .method = \"Network.setCookies\",\n        .params = .{\n            .cookies = &[_]CdpCookie{\n                .{ .name = \"test3\", .value = \"value3\", .url = \"https://car.example.com/pan/cakes\" },\n                .{ .name = \"test4\", .value = \"value4\", .domain = \"example.com\", .path = \"/mango\" },\n            },\n        },\n    });\n    try ctx.expectSentResult(null, .{ .id = 5 });\n    try ctx.processMessage(.{\n        .id = 6,\n        .method = \"Network.getCookies\",\n        .params = .{ .urls = &[_][]const u8{\"https://car.example.com/pan/cakes\"} },\n    });\n    try ctx.expectSentResult(.{\n        .cookies = &[_]ResCookie{\n            .{ .name = \"test3\", .value = \"value3\", .domain = \"car.example.com\", .path = \"/\", .size = 11, .secure = true }, // No Pancakes!\n        },\n    }, .{ .id = 6 });\n\n    // deleteCookies\n    try ctx.processMessage(.{\n        .id = 7,\n        .method = \"Network.deleteCookies\",\n        .params = .{ .name = \"test3\", .domain = \"car.example.com\" },\n    });\n    try ctx.expectSentResult(null, .{ .id = 7 });\n    try ctx.processMessage(.{\n        .id = 8,\n        .method = \"Storage.getCookies\",\n        .params = .{ .browserContextId = \"BID-S\" },\n    });\n    // Just the untouched test4 should be in the result\n    try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{.{ .name = \"test4\", .value = \"value4\", .domain = \".example.com\", .path = \"/mango\", .size = 11 }} }, .{ .id = 8 });\n\n    // Empty after clearBrowserCookies\n    try ctx.processMessage(.{\n        .id = 9,\n        .method = \"Network.clearBrowserCookies\",\n    });\n    try ctx.expectSentResult(null, .{ .id = 9 });\n    try ctx.processMessage(.{\n        .id = 10,\n        .method = \"Storage.getCookies\",\n        .params = .{ .browserContextId = \"BID-S\" },\n    });\n    try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 });\n}\n"
  },
  {
    "path": "src/cdp/domains/page.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst screenshot_png = @embedFile(\"screenshot.png\");\n\nconst id = @import(\"../id.zig\");\nconst log = @import(\"../../log.zig\");\nconst js = @import(\"../../browser/js/js.zig\");\nconst URL = @import(\"../../browser/URL.zig\");\nconst Page = @import(\"../../browser/Page.zig\");\nconst timestampF = @import(\"../../datetime.zig\").timestamp;\nconst Notification = @import(\"../../Notification.zig\");\n\nconst Allocator = std.mem.Allocator;\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        getFrameTree,\n        setLifecycleEventsEnabled,\n        addScriptToEvaluateOnNewDocument,\n        createIsolatedWorld,\n        navigate,\n        stopLoading,\n        close,\n        captureScreenshot,\n        getLayoutMetrics,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return cmd.sendResult(null, .{}),\n        .getFrameTree => return getFrameTree(cmd),\n        .setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),\n        .addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),\n        .createIsolatedWorld => return createIsolatedWorld(cmd),\n        .navigate => return navigate(cmd),\n        .stopLoading => return cmd.sendResult(null, .{}),\n        .close => return close(cmd),\n        .captureScreenshot => return captureScreenshot(cmd),\n        .getLayoutMetrics => return getLayoutMetrics(cmd),\n    }\n}\n\nconst Frame = struct {\n    id: []const u8,\n    loaderId: []const u8,\n    url: []const u8,\n    domainAndRegistry: []const u8 = \"\",\n    securityOrigin: []const u8,\n    mimeType: []const u8 = \"text/html\",\n    adFrameStatus: struct {\n        adFrameType: []const u8 = \"none\",\n    } = .{},\n    secureContextType: []const u8,\n    crossOriginIsolatedContextType: []const u8 = \"NotIsolated\",\n    gatedAPIFeatures: [][]const u8 = &[0][]const u8{},\n};\n\nfn getFrameTree(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const target_id = bc.target_id orelse return error.TargetNotLoaded;\n\n    return cmd.sendResult(.{\n        .frameTree = .{\n            .frame = Frame{\n                .id = &target_id,\n                .securityOrigin = bc.security_origin,\n                .loaderId = \"LID-0000000001\",\n                .url = bc.getURL() orelse \"about:blank\",\n                .secureContextType = bc.secure_context_type,\n            },\n        },\n    }, .{});\n}\n\nfn setLifecycleEventsEnabled(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        enabled: bool,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    if (params.enabled == false) {\n        bc.lifecycleEventsDisable();\n        return cmd.sendResult(null, .{});\n    }\n\n    // Enable lifecycle events.\n    try bc.lifecycleEventsEnable();\n\n    // When we enable lifecycle events, we must dispatch events for all\n    // attached targets.\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    if (page._load_state == .complete) {\n        const frame_id = &id.toFrameId(page._frame_id);\n        const loader_id = &id.toLoaderId(page._req_id);\n\n        const now = timestampF(.monotonic);\n        try sendPageLifecycle(bc, \"DOMContentLoaded\", now, frame_id, loader_id);\n        try sendPageLifecycle(bc, \"load\", now, frame_id, loader_id);\n\n        const http_client = page._session.browser.http_client;\n        const http_active = http_client.active;\n        const total_network_activity = http_active + http_client.intercepted;\n        if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {\n            try sendPageLifecycle(bc, \"networkAlmostIdle\", now, frame_id, loader_id);\n        }\n        if (page._notified_network_idle.check(total_network_activity == 0)) {\n            try sendPageLifecycle(bc, \"networkIdle\", now, frame_id, loader_id);\n        }\n    }\n\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: hard coded method\n// With the command we receive a script we need to store and run for each new document.\n// Note that the worldName refers to the name given to the isolated world.\nfn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {\n    // const params = (try cmd.params(struct {\n    //     source: []const u8,\n    //     worldName: ?[]const u8 = null,\n    //     includeCommandLineAPI: bool = false,\n    //     runImmediately: bool = false,\n    // })) orelse return error.InvalidParams;\n\n    return cmd.sendResult(.{\n        .identifier = \"1\",\n    }, .{});\n}\n\nfn close(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    const target_id = bc.target_id orelse return error.TargetNotLoaded;\n\n    // can't be null if we have a target_id\n    lp.assert(bc.session.page != null, \"CDP.page.close null page\", .{});\n\n    try cmd.sendResult(.{}, .{});\n\n    // Following code is similar to target.closeTarget\n    //\n    // could be null, created but never attached\n    if (bc.session_id) |session_id| {\n        // Inspector.detached event\n        try cmd.sendEvent(\"Inspector.detached\", .{\n            .reason = \"Render process gone.\",\n        }, .{ .session_id = session_id });\n\n        // detachedFromTarget event\n        try cmd.sendEvent(\"Target.detachedFromTarget\", .{\n            .targetId = target_id,\n            .sessionId = session_id,\n            .reason = \"Render process gone.\",\n        }, .{});\n\n        bc.session_id = null;\n    }\n\n    bc.session.removePage();\n    for (bc.isolated_worlds.items) |world| {\n        world.deinit();\n    }\n    bc.isolated_worlds.clearRetainingCapacity();\n    bc.target_id = null;\n}\n\nfn createIsolatedWorld(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        frameId: []const u8,\n        worldName: []const u8,\n        grantUniveralAccess: bool = false,\n    })) orelse return error.InvalidParams;\n    if (!params.grantUniveralAccess) {\n        log.warn(.not_implemented, \"Page.createIsolatedWorld\", .{ .param = \"grantUniveralAccess\" });\n        // When grantUniveralAccess == false and the client attempts to resolve\n        // or otherwise access a DOM or other JS Object from another context that should fail.\n    }\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    const js_context = try world.createContext(page);\n    return cmd.sendResult(.{ .executionContextId = js_context.id }, .{});\n}\n\nfn navigate(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        url: [:0]const u8,\n        // referrer: ?[]const u8 = null,\n        // transitionType: ?[]const u8 = null, // TODO: enum\n        // frameId: ?[]const u8 = null,\n        // referrerPolicy: ?[]const u8 = null, // TODO: enum\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    // didn't create?\n    // const target_id = bc.target_id orelse return error.TargetIdNotLoaded;\n\n    // didn't attach?\n    if (bc.session_id == null) {\n        return error.SessionIdNotLoaded;\n    }\n\n    const session = bc.session;\n    var page = session.currentPage() orelse return error.PageNotLoaded;\n\n    if (page._load_state != .waiting) {\n        page = try session.replacePage();\n    }\n\n    const encoded_url = try URL.ensureEncoded(page.call_arena, params.url);\n    try page.navigate(encoded_url, .{\n        .reason = .address_bar,\n        .cdp_id = cmd.input.id,\n        .kind = .{ .push = null },\n    });\n}\n\npub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n    bc.reset();\n\n    const frame_id = &id.toFrameId(event.frame_id);\n    const loader_id = &id.toLoaderId(event.req_id);\n\n    var cdp = bc.cdp;\n    const reason_: ?[]const u8 = switch (event.opts.reason) {\n        .anchor => \"anchorClick\",\n        .script, .history, .navigation => \"scriptInitiated\",\n        .form => switch (event.opts.method) {\n            .GET => \"formSubmissionGet\",\n            .POST => \"formSubmissionPost\",\n            else => unreachable,\n        },\n        .address_bar => null,\n        .initialFrameNavigation => \"initialFrameNavigation\",\n    };\n    if (reason_) |reason| {\n        if (event.opts.reason != .initialFrameNavigation) {\n            try cdp.sendEvent(\"Page.frameScheduledNavigation\", .{\n                .frameId = frame_id,\n                .delay = 0,\n                .reason = reason,\n                .url = event.url,\n            }, .{ .session_id = session_id });\n        }\n        try cdp.sendEvent(\"Page.frameRequestedNavigation\", .{\n            .frameId = frame_id,\n            .reason = reason,\n            .url = event.url,\n            .disposition = \"currentTab\",\n        }, .{ .session_id = session_id });\n    }\n\n    // frameStartedNavigating event\n    try cdp.sendEvent(\"Page.frameStartedNavigating\", .{\n        .frameId = frame_id,\n        .url = event.url,\n        .loaderId = loader_id,\n        .navigationType = \"differentDocument\",\n    }, .{ .session_id = session_id });\n\n    // frameStartedLoading event\n    try cdp.sendEvent(\"Page.frameStartedLoading\", .{\n        .frameId = frame_id,\n    }, .{ .session_id = session_id });\n}\n\npub fn pageRemove(bc: anytype) !void {\n    // Clear all remote object mappings to prevent stale objectIds from being used\n    // after the context is destroy\n    bc.inspector_session.inspector.resetContextGroup();\n\n    // The main page is going to be removed, we need to remove contexts from other worlds first.\n    for (bc.isolated_worlds.items) |isolated_world| {\n        try isolated_world.removeContext();\n    }\n}\n\npub fn pageCreated(bc: anytype, page: *Page) !void {\n    _ = bc.cdp.page_arena.reset(.{ .retain_with_limit = 1024 * 512 });\n\n    for (bc.isolated_worlds.items) |isolated_world| {\n        _ = try isolated_world.createContext(page);\n    }\n    // Only retain captured responses until a navigation event. In CDP term,\n    // this is called a \"renderer\" and the cache-duration can be controlled via\n    // the Network.configureDurableMessages message (which we don't support)\n    bc.captured_responses = .empty;\n}\n\npub fn pageFrameCreated(bc: anytype, event: *const Notification.PageFrameCreated) !void {\n    const session_id = bc.session_id orelse return;\n\n    const cdp = bc.cdp;\n    const frame_id = &id.toFrameId(event.frame_id);\n\n    try cdp.sendEvent(\"Page.frameAttached\", .{ .params = .{\n        .frameId = frame_id,\n        .parentFrameId = &id.toFrameId(event.parent_id),\n    } }, .{ .session_id = session_id });\n\n    if (bc.page_life_cycle_events) {\n        try cdp.sendEvent(\"Page.lifecycleEvent\", LifecycleEvent{\n            .name = \"init\",\n            .frameId = frame_id,\n            .loaderId = &id.toLoaderId(event.frame_id),\n            .timestamp = event.timestamp,\n        }, .{ .session_id = session_id });\n    }\n}\n\npub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n\n    const timestamp = event.timestamp;\n    const frame_id = &id.toFrameId(event.frame_id);\n    const loader_id = &id.toLoaderId(event.req_id);\n\n    var cdp = bc.cdp;\n\n    // Drivers are sensitive to the order of events. Some more than others.\n    // The result for the Page.navigate seems like it _must_ come after\n    // the frameStartedLoading, but before any lifecycleEvent. So we\n    // unfortunately have to put the input_id ito the NavigateOpts which gets\n    // passed back into the notification.\n    if (event.opts.cdp_id) |input_id| {\n        try cdp.sendJSON(.{\n            .id = input_id,\n            .result = .{\n                .frameId = frame_id,\n                .loaderId = loader_id,\n            },\n            .sessionId = session_id,\n        });\n    }\n\n    if (bc.page_life_cycle_events) {\n        try cdp.sendEvent(\"Page.lifecycleEvent\", LifecycleEvent{\n            .name = \"init\",\n            .frameId = frame_id,\n            .loaderId = loader_id,\n            .timestamp = event.timestamp,\n        }, .{ .session_id = session_id });\n    }\n\n    const reason_: ?[]const u8 = switch (event.opts.reason) {\n        .anchor => \"anchorClick\",\n        .script, .history, .navigation => \"scriptInitiated\",\n        .form => switch (event.opts.method) {\n            .GET => \"formSubmissionGet\",\n            .POST => \"formSubmissionPost\",\n            else => unreachable,\n        },\n        .address_bar => null,\n        .initialFrameNavigation => \"initialFrameNavigation\",\n    };\n\n    if (reason_ != null) {\n        try cdp.sendEvent(\"Page.frameClearedScheduledNavigation\", .{\n            .frameId = frame_id,\n        }, .{ .session_id = session_id });\n    }\n\n    const page = bc.session.currentPage() orelse return error.PageNotLoaded;\n\n    // When we actually recreated the context we should have the inspector send\n    // this event, see: resetContextGroup Sending this event will tell the\n    // client that the context ids they had are invalid and the context shouls\n    // be dropped The client will expect us to send new contextCreated events,\n    // such that the client has new id's for the active contexts.\n    // Only send executionContextsCleared for main frame navigations. For child\n    // frames (iframes), clearing all contexts would destroy the main frame's\n    // context, causing Puppeteer's page.evaluate()/page.content() to hang\n    // forever.\n    if (event.frame_id == page._frame_id) {\n        try cdp.sendEvent(\"Runtime.executionContextsCleared\", null, .{ .session_id = session_id });\n    }\n\n    {\n        const aux_data = try std.fmt.allocPrint(arena, \"{{\\\"isDefault\\\":true,\\\"type\\\":\\\"default\\\",\\\"frameId\\\":\\\"{s}\\\",\\\"loaderId\\\":\\\"{s}\\\"}}\", .{ frame_id, loader_id });\n\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        bc.inspector_session.inspector.contextCreated(\n            &ls.local,\n            \"\",\n            page.origin orelse \"\",\n            aux_data,\n            true,\n        );\n    }\n    for (bc.isolated_worlds.items) |isolated_world| {\n        const aux_json = try std.fmt.allocPrint(arena, \"{{\\\"isDefault\\\":false,\\\"type\\\":\\\"isolated\\\",\\\"frameId\\\":\\\"{s}\\\",\\\"loaderId\\\":\\\"{s}\\\"}}\", .{ frame_id, loader_id });\n\n        // Calling contextCreated will assign a new Id to the context and send the contextCreated event\n\n        var ls: js.Local.Scope = undefined;\n        (isolated_world.context orelse continue).localScope(&ls);\n        defer ls.deinit();\n\n        bc.inspector_session.inspector.contextCreated(\n            &ls.local,\n            isolated_world.name,\n            \"://\",\n            aux_json,\n            false,\n        );\n    }\n\n    // frameNavigated event\n    try cdp.sendEvent(\"Page.frameNavigated\", .{\n        .type = \"Navigation\",\n        .frame = Frame{\n            .id = frame_id,\n            .url = event.url,\n            .loaderId = loader_id,\n            .securityOrigin = bc.security_origin,\n            .secureContextType = bc.secure_context_type,\n        },\n    }, .{ .session_id = session_id });\n\n    // The DOM.documentUpdated event must be send after the frameNavigated one.\n    // chromedp client expects to receive the events is this order.\n    // see https://github.com/chromedp/chromedp/issues/1558\n    try cdp.sendEvent(\"DOM.documentUpdated\", null, .{ .session_id = session_id });\n\n    // domContentEventFired event\n    // TODO: partially hard coded\n    try cdp.sendEvent(\n        \"Page.domContentEventFired\",\n        .{ .timestamp = timestamp },\n        .{ .session_id = session_id },\n    );\n\n    // lifecycle DOMContentLoaded event\n    // TODO: partially hard coded\n    if (bc.page_life_cycle_events) {\n        try cdp.sendEvent(\"Page.lifecycleEvent\", LifecycleEvent{\n            .timestamp = timestamp,\n            .name = \"DOMContentLoaded\",\n            .frameId = frame_id,\n            .loaderId = loader_id,\n        }, .{ .session_id = session_id });\n    }\n\n    // loadEventFired event\n    try cdp.sendEvent(\n        \"Page.loadEventFired\",\n        .{ .timestamp = timestamp },\n        .{ .session_id = session_id },\n    );\n\n    // lifecycle DOMContentLoaded event\n    if (bc.page_life_cycle_events) {\n        try cdp.sendEvent(\"Page.lifecycleEvent\", LifecycleEvent{\n            .timestamp = timestamp,\n            .name = \"load\",\n            .frameId = frame_id,\n            .loaderId = loader_id,\n        }, .{ .session_id = session_id });\n    }\n\n    // frameStoppedLoading\n    return cdp.sendEvent(\"Page.frameStoppedLoading\", .{\n        .frameId = frame_id,\n    }, .{ .session_id = session_id });\n}\n\npub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void {\n    return sendPageLifecycle(bc, \"networkIdle\", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id));\n}\n\npub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void {\n    return sendPageLifecycle(bc, \"networkAlmostIdle\", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id));\n}\n\nfn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u64, frame_id: []const u8, loader_id: []const u8) !void {\n    // detachTarget could be called, in which case, we still have a page doing\n    // things, but no session.\n    const session_id = bc.session_id orelse return;\n\n    return bc.cdp.sendEvent(\"Page.lifecycleEvent\", LifecycleEvent{\n        .name = name,\n        .frameId = frame_id,\n        .loaderId = loader_id,\n        .timestamp = timestamp,\n    }, .{ .session_id = session_id });\n}\n\nconst LifecycleEvent = struct {\n    frameId: []const u8,\n    loaderId: ?[]const u8,\n    name: []const u8,\n    timestamp: u64,\n};\n\nconst Viewport = struct {\n    x: f64,\n    y: f64,\n    width: f64,\n    height: f64,\n    scale: f64,\n};\n\nfn base64Encode(comptime input: []const u8) [std.base64.standard.Encoder.calcSize(input.len)]u8 {\n    const encoder = std.base64.standard.Encoder;\n    var buf: [encoder.calcSize(input.len)]u8 = undefined;\n    _ = encoder.encode(&buf, input);\n    return buf;\n}\n\nfn captureScreenshot(cmd: anytype) !void {\n    const Params = struct {\n        format: ?[]const u8 = \"png\",\n        quality: ?u8 = null,\n        clip: ?Viewport = null,\n        fromSurface: ?bool = false,\n        captureBeyondViewport: ?bool = false,\n        optimizeForSpeed: ?bool = false,\n    };\n    const params = try cmd.params(Params) orelse Params{};\n\n    const format = params.format orelse \"png\";\n\n    if (!std.mem.eql(u8, format, \"png\")) {\n        log.warn(.not_implemented, \"Page.captureScreenshot params\", .{ .format = format });\n        return cmd.sendError(-32000, \"unsupported screenshot format.\", .{});\n    }\n    if (params.quality != null) {\n        log.warn(.not_implemented, \"Page.captureScreenshot params\", .{ .quality = params.quality });\n    }\n    if (params.clip != null) {\n        log.warn(.not_implemented, \"Page.captureScreenshot params\", .{ .clip = params.clip });\n    }\n    if (params.fromSurface orelse false or params.captureBeyondViewport orelse false or params.optimizeForSpeed orelse false) {\n        log.warn(.not_implemented, \"Page.captureScreenshot params\", .{\n            .fromSurface = params.fromSurface,\n            .captureBeyondViewport = params.captureBeyondViewport,\n            .optimizeForSpeed = params.optimizeForSpeed,\n        });\n    }\n\n    return cmd.sendResult(.{\n        .data = base64Encode(screenshot_png),\n    }, .{});\n}\n\nfn getLayoutMetrics(cmd: anytype) !void {\n    const width = 1920;\n    const height = 1080;\n\n    return cmd.sendResult(.{\n        .layoutViewport = .{\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n        },\n        .visualViewport = .{\n            .offsetX = 0,\n            .offsetY = 0,\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n            .scale = 1,\n            .zoom = 1,\n        },\n        .contentSize = .{\n            .x = 0,\n            .y = 0,\n            .width = width,\n            .height = height,\n        },\n        .cssLayoutViewport = .{\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n        },\n        .cssVisualViewport = .{\n            .offsetX = 0,\n            .offsetY = 0,\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n            .scale = 1,\n            .zoom = 1,\n        },\n        .cssContentSize = .{\n            .x = 0,\n            .y = 0,\n            .width = width,\n            .height = height,\n        },\n    }, .{});\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"cdp.page: getFrameTree\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Page.getFrameTree\", .params = .{ .targetId = \"X\" } });\n        try ctx.expectSentError(-31998, \"BrowserContextNotLoaded\", .{ .id = 10 });\n    }\n\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\", .url = \"hi.html\", .target_id = \"FID-000000000X\".* });\n    {\n        try ctx.processMessage(.{ .id = 11, .method = \"Page.getFrameTree\" });\n        try ctx.expectSentResult(.{\n            .frameTree = .{\n                .frame = .{\n                    .id = \"FID-000000000X\",\n                    .loaderId = \"LID-0000000001\",\n                    .url = \"http://127.0.0.1:9582/src/browser/tests/hi.html\",\n                    .domainAndRegistry = \"\",\n                    .securityOrigin = bc.security_origin,\n                    .mimeType = \"text/html\",\n                    .adFrameStatus = .{\n                        .adFrameType = \"none\",\n                    },\n                    .secureContextType = bc.secure_context_type,\n                    .crossOriginIsolatedContextType = \"NotIsolated\",\n                    .gatedAPIFeatures = [_][]const u8{},\n                },\n            },\n        }, .{ .id = 11 });\n    }\n}\n\ntest \"cdp.page: captureScreenshot\" {\n    const LogFilter = @import(\"../../testing.zig\").LogFilter;\n    const filter: LogFilter = .init(&.{.not_implemented});\n    defer filter.deinit();\n\n    var ctx = testing.context();\n    defer ctx.deinit();\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Page.captureScreenshot\", .params = .{ .format = \"jpg\" } });\n        try ctx.expectSentError(-32000, \"unsupported screenshot format.\", .{ .id = 10 });\n    }\n\n    {\n        try ctx.processMessage(.{ .id = 11, .method = \"Page.captureScreenshot\" });\n        try ctx.expectSentResult(.{\n            .data = base64Encode(screenshot_png),\n        }, .{ .id = 11 });\n    }\n}\n\ntest \"cdp.page: getLayoutMetrics\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-9\", .url = \"hi.html\", .target_id = \"FID-000000000X\".* });\n\n    const width = 1920;\n    const height = 1080;\n\n    try ctx.processMessage(.{ .id = 12, .method = \"Page.getLayoutMetrics\" });\n    try ctx.expectSentResult(.{\n        .layoutViewport = .{\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n        },\n        .visualViewport = .{\n            .offsetX = 0,\n            .offsetY = 0,\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n            .scale = 1,\n            .zoom = 1,\n        },\n        .contentSize = .{\n            .x = 0,\n            .y = 0,\n            .width = width,\n            .height = height,\n        },\n        .cssLayoutViewport = .{\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n        },\n        .cssVisualViewport = .{\n            .offsetX = 0,\n            .offsetY = 0,\n            .pageX = 0,\n            .pageY = 0,\n            .clientWidth = width,\n            .clientHeight = height,\n            .scale = 1,\n            .zoom = 1,\n        },\n        .cssContentSize = .{\n            .x = 0,\n            .y = 0,\n            .width = width,\n            .height = height,\n        },\n    }, .{ .id = 12 });\n}\n"
  },
  {
    "path": "src/cdp/domains/performance.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        disable,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return cmd.sendResult(null, .{}),\n        .disable => return cmd.sendResult(null, .{}),\n    }\n}\n"
  },
  {
    "path": "src/cdp/domains/runtime.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        runIfWaitingForDebugger,\n        evaluate,\n        addBinding,\n        callFunctionOn,\n        releaseObject,\n        getProperties,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .runIfWaitingForDebugger => return cmd.sendResult(null, .{}),\n        else => return sendInspector(cmd, action),\n    }\n}\n\nfn sendInspector(cmd: anytype, action: anytype) !void {\n    // save script in file at debug mode\n    if (builtin.mode == .Debug) {\n        try logInspector(cmd, action);\n    }\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    // the result to return is handled directly by the inspector.\n    bc.callInspector(cmd.input.json);\n}\n\nfn logInspector(cmd: anytype, action: anytype) !void {\n    const script = switch (action) {\n        .evaluate => blk: {\n            const params = (try cmd.params(struct {\n                expression: []const u8,\n                // contextId: ?u8 = null,\n                // returnByValue: ?bool = null,\n                // awaitPromise: ?bool = null,\n                // userGesture: ?bool = null,\n            })) orelse return error.InvalidParams;\n\n            break :blk params.expression;\n        },\n        .callFunctionOn => blk: {\n            const params = (try cmd.params(struct {\n                functionDeclaration: []const u8,\n                // objectId: ?[]const u8 = null,\n                // executionContextId: ?u8 = null,\n                // arguments: ?[]struct {\n                //     value: ?[]const u8 = null,\n                //     objectId: ?[]const u8 = null,\n                // } = null,\n                // returnByValue: ?bool = null,\n                // awaitPromise: ?bool = null,\n                // userGesture: ?bool = null,\n            })) orelse return error.InvalidParams;\n\n            break :blk params.functionDeclaration;\n        },\n        else => return,\n    };\n    const id = cmd.input.id orelse return error.RequiredId;\n    const name = try std.fmt.allocPrint(cmd.arena, \"id_{d}.js\", .{id});\n\n    var dir = try std.fs.cwd().makeOpenPath(\".zig-cache/tmp\", .{});\n    defer dir.close();\n\n    const f = try dir.createFile(name, .{});\n    defer f.close();\n    try f.writeAll(script);\n}\n"
  },
  {
    "path": "src/cdp/domains/security.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        enable,\n        disable,\n        setIgnoreCertificateErrors,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .enable => return cmd.sendResult(null, .{}),\n        .disable => return cmd.sendResult(null, .{}),\n        .setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),\n    }\n}\n\nfn setIgnoreCertificateErrors(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        ignore: bool,\n    })) orelse return error.InvalidParams;\n\n    try cmd.cdp.browser.http_client.setTlsVerify(!params.ignore);\n    return cmd.sendResult(null, .{});\n}\n\nconst testing = @import(\"../testing.zig\");\n\ntest \"cdp.Security: setIgnoreCertificateErrors\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n\n    try ctx.processMessage(.{\n        .id = 8,\n        .method = \"Security.setIgnoreCertificateErrors\",\n        .params = .{ .ignore = true },\n    });\n    try ctx.expectSentResult(null, .{ .id = 8 });\n\n    try ctx.processMessage(.{\n        .id = 9,\n        .method = \"Security.setIgnoreCertificateErrors\",\n        .params = .{ .ignore = false },\n    });\n    try ctx.expectSentResult(null, .{ .id = 9 });\n}\n"
  },
  {
    "path": "src/cdp/domains/storage.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\n\nconst log = @import(\"../../log.zig\");\nconst URL = @import(\"../../browser/URL.zig\");\nconst Cookie = @import(\"../../browser/webapi/storage/storage.zig\").Cookie;\nconst CookieJar = Cookie.Jar;\npub const PreparedUri = Cookie.PreparedUri;\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        clearCookies,\n        setCookies,\n        getCookies,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .clearCookies => return clearCookies(cmd),\n        .getCookies => return getCookies(cmd),\n        .setCookies => return setCookies(cmd),\n    }\n}\n\nconst BrowserContextParam = struct { browserContextId: ?[]const u8 = null };\n\nfn clearCookies(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};\n\n    if (params.browserContextId) |browser_context_id| {\n        if (std.mem.eql(u8, browser_context_id, bc.id) == false) {\n            return error.UnknownBrowserContextId;\n        }\n    }\n\n    bc.session.cookie_jar.clearRetainingCapacity();\n\n    return cmd.sendResult(null, .{});\n}\n\nfn getCookies(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};\n\n    if (params.browserContextId) |browser_context_id| {\n        if (std.mem.eql(u8, browser_context_id, bc.id) == false) {\n            return error.UnknownBrowserContextId;\n        }\n    }\n    bc.session.cookie_jar.removeExpired(null);\n    const writer = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };\n    try cmd.sendResult(.{ .cookies = writer }, .{});\n}\n\nfn setCookies(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const params = (try cmd.params(struct {\n        cookies: []const CdpCookie,\n        browserContextId: ?[]const u8 = null,\n    })) orelse return error.InvalidParams;\n\n    if (params.browserContextId) |browser_context_id| {\n        if (std.mem.eql(u8, browser_context_id, bc.id) == false) {\n            return error.UnknownBrowserContextId;\n        }\n    }\n\n    for (params.cookies) |param| {\n        try setCdpCookie(&bc.session.cookie_jar, param);\n    }\n\n    try cmd.sendResult(null, .{});\n}\n\npub const SameSite = enum {\n    Strict,\n    Lax,\n    None,\n};\npub const CookiePriority = enum {\n    Low,\n    Medium,\n    High,\n};\npub const CookieSourceScheme = enum {\n    Unset,\n    NonSecure,\n    Secure,\n};\n\npub const CookiePartitionKey = struct {\n    topLevelSite: []const u8,\n    hasCrossSiteAncestor: bool,\n};\n\npub const CdpCookie = struct {\n    name: []const u8,\n    value: []const u8,\n    url: ?[:0]const u8 = null,\n    domain: ?[]const u8 = null,\n    path: ?[:0]const u8 = null,\n    secure: ?bool = null, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3\n    httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3\n    sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies\n    expires: ?f64 = null, // -1? says google\n    priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00\n    sameParty: ?bool = null,\n    sourceScheme: ?CookieSourceScheme = null,\n    // sourcePort: Temporary ability and it will be removed from CDP\n    partitionKey: ?CookiePartitionKey = null,\n};\n\npub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {\n    // Silently ignore partitionKey since we don't support partitioned cookies (CHIPS).\n    // This allows Puppeteer's page.setCookie() to work, which may send cookies with\n    // partitionKey as part of its cookie-setting workflow.\n    if (param.partitionKey != null) {\n        log.warn(.not_implemented, \"partition key\", .{ .src = \"setCdpCookie\" });\n    }\n    // Still reject unsupported features\n    if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null) {\n        return error.NotImplemented;\n    }\n\n    var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);\n    errdefer arena.deinit();\n    const a = arena.allocator();\n\n    // NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.\n    const domain = try Cookie.parseDomain(a, param.url, param.domain);\n    const path = if (param.path == null) \"/\" else try Cookie.parsePath(a, null, param.path);\n\n    const secure = if (param.secure) |s| s else if (param.url) |url| URL.isHTTPS(url) else false;\n\n    const cookie = Cookie{\n        .arena = arena,\n        .name = try a.dupe(u8, param.name),\n        .value = try a.dupe(u8, param.value),\n        .path = path,\n        .domain = domain,\n        .expires = param.expires,\n        .secure = secure,\n        .http_only = param.httpOnly,\n        .same_site = switch (param.sameSite) {\n            .Strict => .strict,\n            .Lax => .lax,\n            .None => .none,\n        },\n    };\n    try cookie_jar.add(cookie, std.time.timestamp());\n}\n\npub const CookieWriter = struct {\n    cookies: []const Cookie,\n    urls: ?[]const PreparedUri = null,\n\n    pub fn jsonStringify(self: *const CookieWriter, w: anytype) !void {\n        self.writeCookies(w) catch |err| {\n            // The only error our jsonStringify method can return is @TypeOf(w).Error.\n            log.err(.cdp, \"json stringify\", .{ .err = err });\n            return error.WriteFailed;\n        };\n    }\n\n    fn writeCookies(self: CookieWriter, w: anytype) !void {\n        try w.beginArray();\n        if (self.urls) |urls| {\n            for (self.cookies) |*cookie| {\n                for (urls) |*url| {\n                    if (cookie.appliesTo(url, true, true, true)) { // TBD same_site, should we compare to the pages url?\n                        try writeCookie(cookie, w);\n                        break;\n                    }\n                }\n            }\n        } else {\n            for (self.cookies) |*cookie| {\n                try writeCookie(cookie, w);\n            }\n        }\n        try w.endArray();\n    }\n};\npub fn writeCookie(cookie: *const Cookie, w: anytype) !void {\n    try w.beginObject();\n    {\n        try w.objectField(\"name\");\n        try w.write(cookie.name);\n\n        try w.objectField(\"value\");\n        try w.write(cookie.value);\n\n        try w.objectField(\"domain\");\n        try w.write(cookie.domain); // Should we hide a leading dot?\n\n        try w.objectField(\"path\");\n        try w.write(cookie.path);\n\n        try w.objectField(\"expires\");\n        try w.write(cookie.expires orelse -1);\n\n        try w.objectField(\"size\");\n        try w.write(cookie.name.len + cookie.value.len);\n\n        try w.objectField(\"httpOnly\");\n        try w.write(cookie.http_only);\n\n        try w.objectField(\"secure\");\n        try w.write(cookie.secure);\n\n        try w.objectField(\"session\");\n        try w.write(cookie.expires == null);\n\n        try w.objectField(\"sameSite\");\n        switch (cookie.same_site) {\n            .none => try w.write(\"None\"),\n            .lax => try w.write(\"Lax\"),\n            .strict => try w.write(\"Strict\"),\n        }\n\n        // TODO experimentals\n    }\n    try w.endObject();\n}\n\nconst testing = @import(\"../testing.zig\");\n\ntest \"cdp.Storage: cookies\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n    _ = try ctx.loadBrowserContext(.{ .id = \"BID-S\" });\n\n    // Initially empty\n    try ctx.processMessage(.{\n        .id = 3,\n        .method = \"Storage.getCookies\",\n        .params = .{ .browserContextId = \"BID-S\" },\n    });\n    try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });\n\n    // Has cookies after setting them\n    try ctx.processMessage(.{\n        .id = 4,\n        .method = \"Storage.setCookies\",\n        .params = .{\n            .cookies = &[_]CdpCookie{\n                .{ .name = \"test\", .value = \"value\", .domain = \"example.com\", .path = \"/mango\" },\n                .{ .name = \"test2\", .value = \"value2\", .url = \"https://car.example.com/pancakes\" },\n            },\n            .browserContextId = \"BID-S\",\n        },\n    });\n    try ctx.expectSentResult(null, .{ .id = 4 });\n    try ctx.processMessage(.{\n        .id = 5,\n        .method = \"Storage.getCookies\",\n        .params = .{ .browserContextId = \"BID-S\" },\n    });\n    try ctx.expectSentResult(.{\n        .cookies = &[_]ResCookie{\n            .{ .name = \"test\", .value = \"value\", .domain = \".example.com\", .path = \"/mango\", .size = 9 },\n            .{ .name = \"test2\", .value = \"value2\", .domain = \"car.example.com\", .path = \"/\", .size = 11, .secure = true }, // No Pancakes!\n        },\n    }, .{ .id = 5 });\n\n    // Empty after clearing cookies\n    try ctx.processMessage(.{\n        .id = 6,\n        .method = \"Storage.clearCookies\",\n        .params = .{ .browserContextId = \"BID-S\" },\n    });\n    try ctx.expectSentResult(null, .{ .id = 6 });\n    try ctx.processMessage(.{\n        .id = 7,\n        .method = \"Storage.getCookies\",\n        .params = .{ .browserContextId = \"BID-S\" },\n    });\n    try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 7 });\n}\n\npub const ResCookie = struct {\n    name: []const u8,\n    value: []const u8,\n    domain: []const u8,\n    path: []const u8 = \"/\",\n    expires: f64 = -1,\n    size: usize = 0,\n    httpOnly: bool = false,\n    secure: bool = false,\n    sameSite: []const u8 = \"None\",\n};\n"
  },
  {
    "path": "src/cdp/domains/target.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst id = @import(\"../id.zig\");\nconst log = @import(\"../../log.zig\");\nconst URL = @import(\"../../browser/URL.zig\");\nconst js = @import(\"../../browser/js/js.zig\");\n\n// TODO: hard coded IDs\nconst LOADER_ID = \"LOADERID42AA389647D702B4D805F49A\";\n\npub fn processMessage(cmd: anytype) !void {\n    const action = std.meta.stringToEnum(enum {\n        getTargets,\n        attachToTarget,\n        attachToBrowserTarget,\n        closeTarget,\n        createBrowserContext,\n        createTarget,\n        detachFromTarget,\n        disposeBrowserContext,\n        getBrowserContexts,\n        getTargetInfo,\n        sendMessageToTarget,\n        setAutoAttach,\n        setDiscoverTargets,\n        activateTarget,\n    }, cmd.input.action) orelse return error.UnknownMethod;\n\n    switch (action) {\n        .getTargets => return getTargets(cmd),\n        .attachToTarget => return attachToTarget(cmd),\n        .attachToBrowserTarget => return attachToBrowserTarget(cmd),\n        .closeTarget => return closeTarget(cmd),\n        .createBrowserContext => return createBrowserContext(cmd),\n        .createTarget => return createTarget(cmd),\n        .detachFromTarget => return detachFromTarget(cmd),\n        .disposeBrowserContext => return disposeBrowserContext(cmd),\n        .getBrowserContexts => return getBrowserContexts(cmd),\n        .getTargetInfo => return getTargetInfo(cmd),\n        .sendMessageToTarget => return sendMessageToTarget(cmd),\n        .setAutoAttach => return setAutoAttach(cmd),\n        .setDiscoverTargets => return setDiscoverTargets(cmd),\n        .activateTarget => return cmd.sendResult(null, .{}),\n    }\n}\n\nfn getTargets(cmd: anytype) !void {\n    // If no context available, return an empty array.\n    const bc = cmd.browser_context orelse {\n        return cmd.sendResult(.{\n            .targetInfos = [_]TargetInfo{},\n        }, .{ .include_session_id = false });\n    };\n\n    const target_id = &(bc.target_id orelse {\n        return cmd.sendResult(.{\n            .targetInfos = [_]TargetInfo{},\n        }, .{ .include_session_id = false });\n    });\n\n    return cmd.sendResult(.{\n        .targetInfos = [_]TargetInfo{.{\n            .targetId = target_id,\n            .type = \"page\",\n            .title = bc.getTitle() orelse \"\",\n            .url = bc.getURL() orelse \"about:blank\",\n            .attached = true,\n            .canAccessOpener = false,\n        }},\n    }, .{ .include_session_id = false });\n}\n\nfn getBrowserContexts(cmd: anytype) !void {\n    var browser_context_ids: []const []const u8 = undefined;\n    if (cmd.browser_context) |bc| {\n        browser_context_ids = &.{bc.id};\n    } else {\n        browser_context_ids = &.{};\n    }\n\n    return cmd.sendResult(.{\n        .browserContextIds = browser_context_ids,\n    }, .{ .include_session_id = false });\n}\n\nfn createBrowserContext(cmd: anytype) !void {\n    const params = try cmd.params(struct {\n        disposeOnDetach: bool = false,\n        proxyServer: ?[:0]const u8 = null,\n        proxyBypassList: ?[]const u8 = null,\n        originsWithUniversalNetworkAccess: ?[]const []const u8 = null,\n    });\n    if (params) |p| {\n        if (p.disposeOnDetach or p.proxyBypassList != null or p.originsWithUniversalNetworkAccess != null) {\n            log.warn(.not_implemented, \"Target.createBrowserContext\", .{ .disposeOnDetach = p.disposeOnDetach, .has_proxyBypassList = p.proxyBypassList != null, .has_originsWithUniversalNetworkAccess = p.originsWithUniversalNetworkAccess != null });\n        }\n    }\n\n    const bc = cmd.createBrowserContext() catch |err| switch (err) {\n        error.AlreadyExists => return cmd.sendError(-32000, \"Cannot have more than one browser context at a time\", .{}),\n        else => return err,\n    };\n\n    if (params) |p| {\n        if (p.proxyServer) |proxy| {\n            // For now the http client is not in the browser context so we assume there is just 1.\n            try cmd.cdp.browser.http_client.changeProxy(proxy);\n            bc.http_proxy_changed = true;\n        }\n    }\n\n    return cmd.sendResult(.{\n        .browserContextId = bc.id,\n    }, .{});\n}\n\nfn disposeBrowserContext(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        browserContextId: []const u8,\n    })) orelse return error.InvalidParams;\n\n    if (cmd.cdp.disposeBrowserContext(params.browserContextId) == false) {\n        return cmd.sendError(-32602, \"No browser context with the given id found\", .{});\n    }\n    try cmd.sendResult(null, .{});\n}\n\nfn createTarget(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        url: [:0]const u8 = \"about:blank\",\n        // width: ?u64 = null,\n        // height: ?u64 = null,\n        browserContextId: ?[]const u8 = null,\n        // enableBeginFrameControl: bool = false,\n        // newWindow: bool = false,\n        // background: bool = false,\n        // forTab: ?bool = null,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse cmd.createBrowserContext() catch |err| switch (err) {\n        error.AlreadyExists => unreachable,\n        else => return err,\n    };\n\n    if (bc.target_id != null) {\n        return error.TargetAlreadyLoaded;\n    }\n    if (params.browserContextId) |param_browser_context_id| {\n        if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {\n            return error.UnknownBrowserContextId;\n        }\n    }\n\n    // if target_id is null, we should never have a page\n    lp.assert(bc.session.page == null, \"CDP.target.createTarget not null page\", .{});\n\n    // if target_id is null, we should never have a session_id\n    lp.assert(bc.session_id == null, \"CDP.target.createTarget not null session_id\", .{});\n\n    const page = try bc.session.createPage();\n\n    // the target_id == the frame_id of the \"root\" page\n    const frame_id = id.toFrameId(page._frame_id);\n    bc.target_id = frame_id;\n    const target_id = &bc.target_id.?;\n    {\n        var ls: js.Local.Scope = undefined;\n        page.js.localScope(&ls);\n        defer ls.deinit();\n\n        const aux_data = try std.fmt.allocPrint(cmd.arena, \"{{\\\"isDefault\\\":true,\\\"type\\\":\\\"default\\\",\\\"frameId\\\":\\\"{s}\\\"}}\", .{target_id});\n        bc.inspector_session.inspector.contextCreated(\n            &ls.local,\n            \"\",\n            \"\", // @ZIGDOM\n            // try page.origin(arena),\n            aux_data,\n            true,\n        );\n    }\n\n    // change CDP state\n    bc.security_origin = \"://\";\n    bc.secure_context_type = \"InsecureScheme\";\n\n    // send targetCreated event\n    // TODO: should this only be sent when Target.setDiscoverTargets\n    // has been enabled?\n    try cmd.sendEvent(\"Target.targetCreated\", .{\n        .targetInfo = TargetInfo{\n            .attached = false,\n            .targetId = target_id,\n            .title = \"\",\n            .browserContextId = bc.id,\n            .url = \"about:blank\",\n        },\n    }, .{});\n\n    // attach to the target only if auto attach is set.\n    if (cmd.cdp.target_auto_attach) {\n        try doAttachtoTarget(cmd, target_id);\n    }\n\n    if (!std.mem.eql(u8, \"about:blank\", params.url)) {\n        const encoded_url = try URL.ensureEncoded(page.call_arena, params.url);\n        try page.navigate(\n            encoded_url,\n            .{ .reason = .address_bar, .kind = .{ .push = null } },\n        );\n    }\n\n    try cmd.sendResult(.{\n        .targetId = target_id,\n    }, .{});\n}\n\nfn attachToTarget(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        targetId: []const u8,\n        flatten: bool = true,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const target_id = &(bc.target_id orelse return error.TargetNotLoaded);\n    if (std.mem.eql(u8, target_id, params.targetId) == false) {\n        return error.UnknownTargetId;\n    }\n\n    try doAttachtoTarget(cmd, target_id);\n\n    return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});\n}\n\nfn attachToBrowserTarget(cmd: anytype) !void {\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n\n    const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();\n\n    try cmd.sendEvent(\"Target.attachedToTarget\", AttachToTarget{\n        .sessionId = session_id,\n        .targetInfo = TargetInfo{\n            .targetId = bc.id, // We use the browser context is as browser's target id.\n            .title = \"\",\n            .url = \"\",\n            .type = \"browser\",\n            // Chrome doesn't send a browserContextId in this case.\n            .browserContextId = null,\n        },\n    }, .{});\n\n    bc.session_id = session_id;\n\n    return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});\n}\n\nfn closeTarget(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        targetId: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    const target_id = &(bc.target_id orelse return error.TargetNotLoaded);\n    if (std.mem.eql(u8, target_id, params.targetId) == false) {\n        return error.UnknownTargetId;\n    }\n\n    // can't be null if we have a target_id\n    lp.assert(bc.session.page != null, \"CDP.target.closeTarget null page\", .{});\n\n    try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });\n\n    // could be null, created but never attached\n    if (bc.session_id) |session_id| {\n        // Inspector.detached event\n        try cmd.sendEvent(\"Inspector.detached\", .{\n            .reason = \"Render process gone.\",\n        }, .{ .session_id = session_id });\n\n        // detachedFromTarget event\n        try cmd.sendEvent(\"Target.detachedFromTarget\", .{\n            .targetId = target_id,\n            .sessionId = session_id,\n            .reason = \"Render process gone.\",\n        }, .{});\n\n        bc.session_id = null;\n    }\n\n    bc.session.removePage();\n    for (bc.isolated_worlds.items) |world| {\n        world.deinit();\n    }\n    bc.isolated_worlds.clearRetainingCapacity();\n    bc.target_id = null;\n}\n\nfn getTargetInfo(cmd: anytype) !void {\n    const Params = struct {\n        targetId: ?[]const u8 = null,\n    };\n    const params = (try cmd.params(Params)) orelse Params{};\n\n    if (params.targetId) |param_target_id| {\n        const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n        const target_id = &(bc.target_id orelse return error.TargetNotLoaded);\n        if (std.mem.eql(u8, target_id, param_target_id) == false) {\n            return error.UnknownTargetId;\n        }\n\n        return cmd.sendResult(.{\n            .targetInfo = TargetInfo{\n                .targetId = target_id,\n                .type = \"page\",\n                .title = bc.getTitle() orelse \"\",\n                .url = bc.getURL() orelse \"about:blank\",\n                .attached = true,\n                .canAccessOpener = false,\n            },\n        }, .{ .include_session_id = false });\n    }\n\n    return cmd.sendResult(.{\n        .targetInfo = TargetInfo{\n            .targetId = \"TID-STARTUP\",\n            .type = \"browser\",\n            .title = \"\",\n            .url = \"about:blank\",\n            .attached = true,\n            .canAccessOpener = false,\n        },\n    }, .{ .include_session_id = false });\n}\n\nfn sendMessageToTarget(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        message: []const u8,\n        sessionId: []const u8,\n    })) orelse return error.InvalidParams;\n\n    const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;\n    if (bc.target_id == null) {\n        return error.TargetNotLoaded;\n    }\n\n    lp.assert(bc.session_id != null, \"CDP.target.sendMessageToTarget null session_id\", .{});\n    if (std.mem.eql(u8, bc.session_id.?, params.sessionId) == false) {\n        // Is this right? Is the params.sessionId meant to be the active\n        // sessionId? What else could it be? We have no other session_id.\n        return error.UnknownSessionId;\n    }\n\n    const Capture = struct {\n        aw: std.Io.Writer.Allocating,\n\n        pub fn sendJSON(self: *@This(), message: anytype) !void {\n            return std.json.Stringify.value(message, .{\n                .emit_null_optional_fields = false,\n            }, &self.aw.writer);\n        }\n    };\n\n    var capture = Capture{\n        .aw = .init(cmd.arena),\n    };\n\n    cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| {\n        log.err(.cdp, \"internal dispatch error\", .{ .err = err, .id = cmd.input.id, .message = params.message });\n        return err;\n    };\n\n    try cmd.sendEvent(\"Target.receivedMessageFromTarget\", .{\n        .message = capture.aw.written(),\n        .sessionId = params.sessionId,\n    }, .{});\n}\n\nfn detachFromTarget(cmd: anytype) !void {\n    // TODO check if sessionId/targetId match.\n    // const params = (try cmd.params(struct {\n    //     sessionId: ?[]const u8,\n    //     targetId: ?[]const u8,\n    // })) orelse return error.InvalidParams;\n\n    if (cmd.browser_context) |bc| {\n        bc.session_id = null;\n        // TODO should we send a Target.detachedFromTarget event?\n    }\n\n    return cmd.sendResult(null, .{});\n}\n\n// TODO: noop method\nfn setDiscoverTargets(cmd: anytype) !void {\n    return cmd.sendResult(null, .{});\n}\n\nfn setAutoAttach(cmd: anytype) !void {\n    const params = (try cmd.params(struct {\n        autoAttach: bool,\n        waitForDebuggerOnStart: bool,\n        flatten: bool = true,\n        // filter: ?[]TargetFilter = null,\n    })) orelse return error.InvalidParams;\n\n    // set a flag to send Target.attachedToTarget events\n    cmd.cdp.target_auto_attach = params.autoAttach;\n\n    if (cmd.cdp.target_auto_attach == false) {\n        // detach from all currently attached targets.\n        if (cmd.browser_context) |bc| {\n            bc.session_id = null;\n            // TODO should we send a Target.detachedFromTarget event?\n        }\n        try cmd.sendResult(null, .{});\n        return;\n    }\n\n    // autoAttach is set to true, we must attach to all existing targets.\n    if (cmd.browser_context) |bc| {\n        if (bc.target_id == null) {\n            if (bc.session.currentPage()) |page| {\n                // the target_id == the frame_id of the \"root\" page\n                bc.target_id = id.toFrameId(page._frame_id);\n                try doAttachtoTarget(cmd, &bc.target_id.?);\n            }\n        }\n        try cmd.sendResult(null, .{});\n        return;\n    }\n\n    // This is a hack. Puppeteer, and probably others, expect the Browser to\n    // automatically started creating targets. Things like an empty tab, or\n    // a blank page. And they block until this happens. So we send an event\n    // telling them that they've been attached to our Broswer. Hopefully, the\n    // first thing they'll do is create a real BrowserContext and progress from\n    // there.\n    // This hack requires the main cdp dispatch handler to special case\n    // messages from this \"STARTUP\" session.\n    try cmd.sendEvent(\"Target.attachedToTarget\", AttachToTarget{\n        .sessionId = \"STARTUP\",\n        .targetInfo = TargetInfo{\n            .type = \"page\",\n            .targetId = \"TID-STARTUP\",\n            .title = \"\",\n            .url = \"about:blank\",\n            .browserContextId = \"BID-STARTUP\",\n        },\n    }, .{});\n\n    try cmd.sendResult(null, .{});\n}\n\nfn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {\n    const bc = cmd.browser_context.?;\n    const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();\n\n    if (bc.session_id == null) {\n        // extra_headers should not be kept on a new page or tab,\n        // currently we have only 1 page, we clear it just in case\n        bc.extra_headers.clearRetainingCapacity();\n    }\n\n    try cmd.sendEvent(\"Target.attachedToTarget\", AttachToTarget{\n        .sessionId = session_id,\n        .targetInfo = TargetInfo{\n            .targetId = target_id,\n            .title = bc.getTitle() orelse \"\",\n            .url = bc.getURL() orelse \"about:blank\",\n            .browserContextId = bc.id,\n        },\n    }, .{ .session_id = bc.session_id });\n\n    bc.session_id = session_id;\n}\n\nconst AttachToTarget = struct {\n    sessionId: []const u8,\n    targetInfo: TargetInfo,\n    waitingForDebugger: bool = false,\n};\n\nconst TargetInfo = struct {\n    url: []const u8,\n    title: []const u8,\n    targetId: []const u8,\n    attached: bool = true,\n    type: []const u8 = \"page\",\n    canAccessOpener: bool = false,\n    browserContextId: ?[]const u8 = null,\n};\n\nconst testing = @import(\"../testing.zig\");\ntest \"cdp.target: getBrowserContexts\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    // {\n    //     // no browser context\n    //     try ctx.processMessage(.{.id = 4, .method = \"Target.getBrowserContexts\"});\n\n    //     try ctx.expectSentResult(.{\n    //         .browserContextIds = &.{},\n    //     }, .{ .id = 4, .session_id = null });\n    // }\n\n    {\n        // with a browser context\n        _ = try ctx.loadBrowserContext(.{ .id = \"BID-X\" });\n        try ctx.processMessage(.{ .id = 5, .method = \"Target.getBrowserContexts\" });\n\n        try ctx.expectSentResult(.{\n            .browserContextIds = &.{\"BID-X\"},\n        }, .{ .id = 5, .session_id = null });\n    }\n}\n\ntest \"cdp.target: createBrowserContext\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        try ctx.processMessage(.{ .id = 4, .method = \"Target.createBrowserContext\" });\n        try ctx.expectSentResult(.{\n            .browserContextId = ctx.cdp().browser_context.?.id,\n        }, .{ .id = 4, .session_id = null });\n    }\n\n    {\n        // we already have one now\n        try ctx.processMessage(.{ .id = 5, .method = \"Target.createBrowserContext\" });\n        try ctx.expectSentError(-32000, \"Cannot have more than one browser context at a time\", .{ .id = 5 });\n    }\n}\n\ntest \"cdp.target: disposeBrowserContext\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        try ctx.processMessage(.{ .id = 7, .method = \"Target.disposeBrowserContext\" });\n        try ctx.expectSentError(-31998, \"InvalidParams\", .{ .id = 7 });\n    }\n\n    {\n        try ctx.processMessage(.{\n            .id = 8,\n            .method = \"Target.disposeBrowserContext\",\n            .params = .{ .browserContextId = \"BID-10\" },\n        });\n        try ctx.expectSentError(-32602, \"No browser context with the given id found\", .{ .id = 8 });\n    }\n\n    {\n        _ = try ctx.loadBrowserContext(.{ .id = \"BID-20\" });\n        try ctx.processMessage(.{\n            .id = 9,\n            .method = \"Target.disposeBrowserContext\",\n            .params = .{ .browserContextId = \"BID-20\" },\n        });\n        try ctx.expectSentResult(null, .{ .id = 9 });\n        try testing.expectEqual(null, ctx.cdp().browser_context);\n    }\n}\n\ntest \"cdp.target: createTarget\" {\n    {\n        var ctx = testing.context();\n        defer ctx.deinit();\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .url = \"about:blank\" } });\n\n        // should create a browser context\n        const bc = ctx.cdp().browser_context.?;\n        try ctx.expectSentEvent(\"Target.targetCreated\", .{ .targetInfo = .{ .url = \"about:blank\", .title = \"\", .attached = false, .type = \"page\", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});\n    }\n\n    {\n        var ctx = testing.context();\n        defer ctx.deinit();\n        // active auto attach to get the Target.attachedToTarget event.\n        try ctx.processMessage(.{ .id = 9, .method = \"Target.setAutoAttach\", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .url = \"about:blank\" } });\n\n        // should create a browser context\n        const bc = ctx.cdp().browser_context.?;\n        try ctx.expectSentEvent(\"Target.targetCreated\", .{ .targetInfo = .{ .url = \"about:blank\", .title = \"\", .attached = false, .type = \"page\", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});\n        try ctx.expectSentEvent(\"Target.attachedToTarget\", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = \"about:blank\", .title = \"\", .attached = true, .type = \"page\", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});\n    }\n\n    var ctx = testing.context();\n    defer ctx.deinit();\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .browserContextId = \"BID-8\" } });\n        try ctx.expectSentError(-31998, \"UnknownBrowserContextId\", .{ .id = 10 });\n    }\n\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .browserContextId = \"BID-9\" } });\n        try testing.expectEqual(true, bc.target_id != null);\n        try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });\n        try ctx.expectSentEvent(\"Target.targetCreated\", .{ .targetInfo = .{ .url = \"about:blank\", .title = \"\", .attached = false, .type = \"page\", .canAccessOpener = false, .browserContextId = \"BID-9\", .targetId = bc.target_id.? } }, .{});\n    }\n}\n\ntest \"cdp.target: closeTarget\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.closeTarget\", .params = .{ .targetId = \"X\" } });\n        try ctx.expectSentError(-31998, \"BrowserContextNotLoaded\", .{ .id = 10 });\n    }\n\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.closeTarget\", .params = .{ .targetId = \"TID-8\" } });\n        try ctx.expectSentError(-31998, \"TargetNotLoaded\", .{ .id = 10 });\n    }\n\n    // pretend we createdTarget first\n    _ = try bc.session.createPage();\n    bc.target_id = \"TID-000000000A\".*;\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.closeTarget\", .params = .{ .targetId = \"TID-8\" } });\n        try ctx.expectSentError(-31998, \"UnknownTargetId\", .{ .id = 10 });\n    }\n\n    {\n        try ctx.processMessage(.{ .id = 11, .method = \"Target.closeTarget\", .params = .{ .targetId = \"TID-000000000A\" } });\n        try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });\n        try testing.expectEqual(null, bc.session.page);\n        try testing.expectEqual(null, bc.target_id);\n    }\n}\n\ntest \"cdp.target: attachToTarget\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.attachToTarget\", .params = .{ .targetId = \"X\" } });\n        try ctx.expectSentError(-31998, \"BrowserContextNotLoaded\", .{ .id = 10 });\n    }\n\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.attachToTarget\", .params = .{ .targetId = \"TID-8\" } });\n        try ctx.expectSentError(-31998, \"TargetNotLoaded\", .{ .id = 10 });\n    }\n\n    // pretend we createdTarget first\n    _ = try bc.session.createPage();\n    bc.target_id = \"TID-000000000B\".*;\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.attachToTarget\", .params = .{ .targetId = \"TID-8\" } });\n        try ctx.expectSentError(-31998, \"UnknownTargetId\", .{ .id = 10 });\n    }\n\n    {\n        try ctx.processMessage(.{ .id = 11, .method = \"Target.attachToTarget\", .params = .{ .targetId = \"TID-000000000B\" } });\n        const session_id = bc.session_id.?;\n        try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });\n        try ctx.expectSentEvent(\"Target.attachedToTarget\", .{ .sessionId = session_id, .targetInfo = .{ .url = \"about:blank\", .title = \"\", .attached = true, .type = \"page\", .canAccessOpener = false, .browserContextId = \"BID-9\", .targetId = bc.target_id.? } }, .{});\n    }\n}\n\ntest \"cdp.target: getTargetInfo\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n\n    {\n        try ctx.processMessage(.{ .id = 9, .method = \"Target.getTargetInfo\" });\n        try ctx.expectSentResult(.{\n            .targetInfo = .{\n                .type = \"browser\",\n                .title = \"\",\n                .url = \"about:blank\",\n                .attached = true,\n                .canAccessOpener = false,\n            },\n        }, .{ .id = 9 });\n    }\n\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.getTargetInfo\", .params = .{ .targetId = \"X\" } });\n        try ctx.expectSentError(-31998, \"BrowserContextNotLoaded\", .{ .id = 10 });\n    }\n\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.getTargetInfo\", .params = .{ .targetId = \"TID-8\" } });\n        try ctx.expectSentError(-31998, \"TargetNotLoaded\", .{ .id = 10 });\n    }\n\n    // pretend we createdTarget first\n    _ = try bc.session.createPage();\n    bc.target_id = \"TID-000000000C\".*;\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.getTargetInfo\", .params = .{ .targetId = \"TID-8\" } });\n        try ctx.expectSentError(-31998, \"UnknownTargetId\", .{ .id = 10 });\n    }\n\n    {\n        try ctx.processMessage(.{ .id = 11, .method = \"Target.getTargetInfo\", .params = .{ .targetId = \"TID-000000000C\" } });\n        try ctx.expectSentResult(.{\n            .targetInfo = .{\n                .targetId = \"TID-000000000C\",\n                .type = \"page\",\n                .title = \"\",\n                .url = \"about:blank\",\n                .attached = true,\n                .canAccessOpener = false,\n            },\n        }, .{ .id = 11 });\n    }\n}\n\ntest \"cdp.target: issue#474: attach to just created target\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .browserContextId = \"BID-9\" } });\n        try testing.expectEqual(true, bc.target_id != null);\n        try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });\n\n        try ctx.processMessage(.{ .id = 11, .method = \"Target.attachToTarget\", .params = .{ .targetId = bc.target_id.? } });\n        const session_id = bc.session_id.?;\n        try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });\n    }\n}\n\ntest \"cdp.target: detachFromTarget\" {\n    var ctx = testing.context();\n    defer ctx.deinit();\n    const bc = try ctx.loadBrowserContext(.{ .id = \"BID-9\" });\n    {\n        try ctx.processMessage(.{ .id = 10, .method = \"Target.createTarget\", .params = .{ .browserContextId = \"BID-9\" } });\n        try testing.expectEqual(true, bc.target_id != null);\n        try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });\n\n        try ctx.processMessage(.{ .id = 11, .method = \"Target.attachToTarget\", .params = .{ .targetId = bc.target_id.? } });\n        try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 11 });\n\n        try ctx.processMessage(.{ .id = 12, .method = \"Target.detachFromTarget\", .params = .{ .targetId = bc.target_id.? } });\n        try testing.expectEqual(null, bc.session_id);\n        try ctx.expectSentResult(null, .{ .id = 12 });\n\n        try ctx.processMessage(.{ .id = 13, .method = \"Target.attachToTarget\", .params = .{ .targetId = bc.target_id.? } });\n        try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 13 });\n    }\n}\n"
  },
  {
    "path": "src/cdp/id.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub fn toPageId(comptime id_type: enum { frame_id, loader_id }, input: []const u8) !u32 {\n    const err = switch (comptime id_type) {\n        .frame_id => error.InvalidFrameId,\n        .loader_id => error.InvalidLoaderId,\n    };\n\n    if (input.len < 4) {\n        return err;\n    }\n\n    return std.fmt.parseInt(u32, input[4..], 10) catch err;\n}\n\npub fn toFrameId(page_id: u32) [14]u8 {\n    var buf: [14]u8 = undefined;\n    _ = std.fmt.bufPrint(&buf, \"FID-{d:0>10}\", .{page_id}) catch unreachable;\n    return buf;\n}\n\npub fn toLoaderId(page_id: u32) [14]u8 {\n    var buf: [14]u8 = undefined;\n    _ = std.fmt.bufPrint(&buf, \"LID-{d:0>10}\", .{page_id}) catch unreachable;\n    return buf;\n}\n\npub fn toRequestId(page_id: u32) [14]u8 {\n    var buf: [14]u8 = undefined;\n    _ = std.fmt.bufPrint(&buf, \"REQ-{d:0>10}\", .{page_id}) catch unreachable;\n    return buf;\n}\n\npub fn toInterceptId(page_id: u32) [14]u8 {\n    var buf: [14]u8 = undefined;\n    _ = std.fmt.bufPrint(&buf, \"INT-{d:0>10}\", .{page_id}) catch unreachable;\n    return buf;\n}\n\n// Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3.\n// Wraps to 0 on overflow.\n// Many caveats for using this:\n// - Not thread-safe.\n// - Information leaking\n// - The slice returned by next() is only valid:\n//   - while incrementor is valid\n//   - until the next call to next()\n// On the positive, it's zero allocation\npub fn Incrementing(comptime T: type, comptime prefix: []const u8) type {\n    // +1 for the '-' separator\n    const NUMERIC_START = prefix.len + 1;\n    const MAX_BYTES = NUMERIC_START + switch (T) {\n        u8 => 3,\n        u16 => 5,\n        u32 => 10,\n        u64 => 20,\n        else => @compileError(\"Incrementing must be given an unsigned int type, got: \" ++ @typeName(T)),\n    };\n\n    const buffer = blk: {\n        var b = [_]u8{0} ** MAX_BYTES;\n        @memcpy(b[0..prefix.len], prefix);\n        b[prefix.len] = '-';\n        break :blk b;\n    };\n\n    const PrefixIntType = @Type(.{ .int = .{\n        .bits = NUMERIC_START * 8,\n        .signedness = .unsigned,\n    } });\n\n    const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*);\n\n    return struct {\n        counter: T = 0,\n        buffer: [MAX_BYTES]u8 = buffer,\n\n        const Self = @This();\n\n        pub fn next(self: *Self) []const u8 {\n            const counter = self.counter;\n            const n = counter +% 1;\n            defer self.counter = n;\n\n            const size = std.fmt.printInt(self.buffer[NUMERIC_START..], n, 10, .lower, .{});\n            return self.buffer[0 .. NUMERIC_START + size];\n        }\n\n        // extracts the numeric portion from an ID\n        pub fn parse(str: []const u8) !T {\n            if (str.len <= NUMERIC_START) {\n                return error.InvalidId;\n            }\n\n            if (@as(PrefixIntType, @bitCast(str[0..NUMERIC_START].*)) != PREFIX_INT_CODE) {\n                return error.InvalidId;\n            }\n\n            return std.fmt.parseInt(T, str[NUMERIC_START..], 10) catch {\n                return error.InvalidId;\n            };\n        }\n    };\n}\n\nconst testing = @import(\"../testing.zig\");\ntest \"id: Incrementing.next\" {\n    var id = Incrementing(u16, \"IDX\"){};\n    try testing.expectEqual(\"IDX-1\", id.next());\n    try testing.expectEqual(\"IDX-2\", id.next());\n    try testing.expectEqual(\"IDX-3\", id.next());\n\n    // force a wrap\n    id.counter = 65533;\n    try testing.expectEqual(\"IDX-65534\", id.next());\n    try testing.expectEqual(\"IDX-65535\", id.next());\n    try testing.expectEqual(\"IDX-0\", id.next());\n}\n\ntest \"id: Incrementing.parse\" {\n    const ReqId = Incrementing(u32, \"REQ\");\n    try testing.expectError(error.InvalidId, ReqId.parse(\"\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"R\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"RE\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"REQ\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"REQ-\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"REQ--1\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"REQ--\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"REQ-Nope\"));\n    try testing.expectError(error.InvalidId, ReqId.parse(\"REQ-4294967296\"));\n\n    try testing.expectEqual(0, try ReqId.parse(\"REQ-0\"));\n    try testing.expectEqual(99, try ReqId.parse(\"REQ-99\"));\n    try testing.expectEqual(4294967295, try ReqId.parse(\"REQ-4294967295\"));\n}\n\ntest \"id: toPageId\" {\n    try testing.expectEqual(0, toPageId(.frame_id, \"FID-0\"));\n    try testing.expectEqual(0, toPageId(.loader_id, \"LID-0\"));\n\n    try testing.expectEqual(4294967295, toPageId(.frame_id, \"FID-4294967295\"));\n    try testing.expectEqual(4294967295, toPageId(.loader_id, \"LID-4294967295\"));\n    try testing.expectError(error.InvalidFrameId, toPageId(.frame_id, \"\"));\n    try testing.expectError(error.InvalidLoaderId, toPageId(.loader_id, \"LID-NOPE\"));\n}\n\ntest \"id: toFrameId\" {\n    try testing.expectEqual(\"FID-0000000000\", toFrameId(0));\n    try testing.expectEqual(\"FID-4294967295\", toFrameId(4294967295));\n}\n\ntest \"id: toLoaderId\" {\n    try testing.expectEqual(\"LID-0000000000\", toLoaderId(0));\n    try testing.expectEqual(\"LID-4294967295\", toLoaderId(4294967295));\n}\n\ntest \"id: toRequestId\" {\n    try testing.expectEqual(\"REQ-0000000000\", toRequestId(0));\n    try testing.expectEqual(\"REQ-4294967295\", toRequestId(4294967295));\n}\n\ntest \"id: toInterceptId\" {\n    try testing.expectEqual(\"INT-0000000000\", toInterceptId(0));\n    try testing.expectEqual(\"INT-4294967295\", toInterceptId(4294967295));\n}\n"
  },
  {
    "path": "src/cdp/testing.zig",
    "content": "// Copyright (C) 2023-2024  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst json = std.json;\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst Testing = @This();\n\nconst main = @import(\"cdp.zig\");\n\nconst base = @import(\"../testing.zig\");\npub const allocator = base.allocator;\npub const expectJson = base.expectJson;\npub const expect = std.testing.expect;\npub const expectEqual = base.expectEqual;\npub const expectError = base.expectError;\npub const expectEqualSlices = base.expectEqualSlices;\npub const pageTest = base.pageTest;\npub const newString = base.newString;\n\nconst Client = struct {\n    allocator: Allocator,\n    send_arena: ArenaAllocator,\n    sent: std.ArrayList(json.Value) = .{},\n    serialized: std.ArrayList([]const u8) = .{},\n\n    fn init(alloc: Allocator) Client {\n        return .{\n            .allocator = alloc,\n            .send_arena = ArenaAllocator.init(alloc),\n        };\n    }\n\n    pub fn sendAllocator(self: *Client) Allocator {\n        return self.send_arena.allocator();\n    }\n\n    pub fn sendJSON(self: *Client, message: anytype, opts: json.Stringify.Options) !void {\n        var opts_copy = opts;\n        opts_copy.whitespace = .indent_2;\n        const serialized = try json.Stringify.valueAlloc(self.allocator, message, opts_copy);\n        try self.serialized.append(self.allocator, serialized);\n\n        const value = try json.parseFromSliceLeaky(json.Value, self.allocator, serialized, .{});\n        try self.sent.append(self.allocator, value);\n    }\n\n    pub fn sendJSONRaw(self: *Client, buf: std.ArrayList(u8)) !void {\n        const value = try json.parseFromSliceLeaky(json.Value, self.allocator, buf.items, .{});\n        try self.sent.append(self.allocator, value);\n    }\n};\n\nconst TestCDP = main.CDPT(struct {\n    pub const Client = *Testing.Client;\n});\n\nconst TestContext = struct {\n    client: ?Client = null,\n    cdp_: ?TestCDP = null,\n    arena: ArenaAllocator,\n\n    pub fn deinit(self: *TestContext) void {\n        if (self.cdp_) |*c| {\n            c.deinit();\n        }\n        self.arena.deinit();\n    }\n\n    pub fn cdp(self: *TestContext) *TestCDP {\n        if (self.cdp_ == null) {\n            self.client = Client.init(self.arena.allocator());\n            // Don't use the arena here. We want to detect leaks in CDP.\n            // The arena is only for test-specific stuff\n            self.cdp_ = TestCDP.init(base.test_app, base.test_http, &self.client.?) catch unreachable;\n        }\n        return &self.cdp_.?;\n    }\n\n    const BrowserContextOpts = struct {\n        id: ?[]const u8 = null,\n        target_id: ?[14]u8 = null,\n        session_id: ?[]const u8 = null,\n        url: ?[:0]const u8 = null,\n    };\n    pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) {\n        var c = self.cdp();\n        if (c.browser_context) |bc| {\n            _ = c.disposeBrowserContext(bc.id);\n        }\n\n        _ = try c.createBrowserContext();\n        var bc = &c.browser_context.?;\n\n        if (opts.id) |id| {\n            bc.id = id;\n        }\n\n        if (opts.target_id) |tid| {\n            bc.target_id = tid;\n        }\n\n        if (opts.session_id) |sid| {\n            bc.session_id = sid;\n        }\n\n        if (opts.url) |url| {\n            if (bc.session_id == null) {\n                bc.session_id = \"SID-X\";\n            }\n            if (bc.target_id == null) {\n                bc.target_id = \"TID-000000000Z\".*;\n            }\n            const page = try bc.session.createPage();\n            const full_url = try std.fmt.allocPrintSentinel(\n                self.arena.allocator(),\n                \"http://127.0.0.1:9582/src/browser/tests/{s}\",\n                .{url},\n                0,\n            );\n            try page.navigate(full_url, .{});\n            _ = bc.session.wait(2000);\n        }\n        return bc;\n    }\n\n    pub fn processMessage(self: *TestContext, msg: anytype) !void {\n        var json_message: []const u8 = undefined;\n        if (@typeInfo(@TypeOf(msg)) != .pointer) {\n            json_message = try std.json.Stringify.valueAlloc(self.arena.allocator(), msg, .{});\n        } else {\n            // assume this is a string we want to send as-is, if it isn't, we'll\n            // get a compile error, so no big deal.\n            json_message = msg;\n        }\n        return self.cdp().processMessage(json_message);\n    }\n\n    pub fn expectSentCount(self: *TestContext, expected: usize) !void {\n        try expectEqual(expected, self.client.?.sent.items.len);\n    }\n\n    const ExpectResultOpts = struct {\n        id: ?usize = null,\n        index: ?usize = null,\n        session_id: ?[]const u8 = null,\n    };\n    pub fn expectSentResult(self: *TestContext, expected: anytype, opts: ExpectResultOpts) !void {\n        const expected_result = .{\n            .id = opts.id,\n            .result = if (comptime @typeInfo(@TypeOf(expected)) == .null) struct {}{} else expected,\n            .sessionId = opts.session_id,\n        };\n\n        try self.expectSent(expected_result, .{ .index = opts.index });\n    }\n\n    const ExpectEventOpts = struct {\n        index: ?usize = null,\n        session_id: ?[]const u8 = null,\n    };\n    pub fn expectSentEvent(self: *TestContext, method: []const u8, params: anytype, opts: ExpectEventOpts) !void {\n        const expected_event = .{\n            .method = method,\n            .params = if (comptime @typeInfo(@TypeOf(params)) == .null) struct {}{} else params,\n            .sessionId = opts.session_id,\n        };\n\n        try self.expectSent(expected_event, .{ .index = opts.index });\n    }\n\n    const ExpectErrorOpts = struct {\n        id: ?usize = null,\n        index: ?usize = null,\n    };\n    pub fn expectSentError(self: *TestContext, code: i32, message: []const u8, opts: ExpectErrorOpts) !void {\n        const expected_message = .{\n            .id = opts.id,\n            .@\"error\" = .{ .code = code, .message = message },\n        };\n        try self.expectSent(expected_message, .{ .index = opts.index });\n    }\n\n    const SentOpts = struct {\n        index: ?usize = null,\n    };\n    pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {\n        const serialized = try json.Stringify.valueAlloc(self.arena.allocator(), expected, .{\n            .whitespace = .indent_2,\n            .emit_null_optional_fields = false,\n        });\n\n        for (self.client.?.sent.items, 0..) |sent, i| {\n            if (try compareExpectedToSent(serialized, sent) == false) {\n                continue;\n            }\n\n            if (opts.index) |expected_index| {\n                if (expected_index != i) {\n                    return error.ErrorAtWrongIndex;\n                }\n            }\n            _ = self.client.?.sent.orderedRemove(i);\n            _ = self.client.?.serialized.orderedRemove(i);\n            return;\n        }\n\n        std.debug.print(\"Error not found. Expecting:\\n{s}\\n\\nGot:\\n\", .{serialized});\n        for (self.client.?.serialized.items, 0..) |sent, i| {\n            std.debug.print(\"#{d}\\n{s}\\n\\n\", .{ i, sent });\n        }\n        return error.ErrorNotFound;\n    }\n};\n\npub fn context() TestContext {\n    return .{\n        .arena = ArenaAllocator.init(std.testing.allocator),\n    };\n}\n\n// Zig makes this hard. When sendJSON is called, we're sending an anytype.\n// We can't record that in an ArrayList(???), so we serialize it to JSON.\n// Now, ideally, we could just take our expected structure, serialize it to\n// json and check if the two are equal.\n// Except serializing to JSON isn't deterministic.\n// So we serialize the JSON then we deserialize to json.Value. And then we can\n// compare our anytype expectation with the json.Value that we captured\n\nfn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {\n    const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});\n    defer expected_value.deinit();\n    return base.isEqualJson(expected_value.value, actual);\n}\n"
  },
  {
    "path": "src/crash_handler.zig",
    "content": "const std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst builtin = @import(\"builtin\");\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\nconst abort = std.posix.abort;\n\n// tracks how deep within a panic we're panicling\nvar panic_level: usize = 0;\n\n// Locked to avoid interleaving panic messages from multiple threads.\nvar panic_mutex = std.Thread.Mutex{};\n\n// overwrite's Zig default panic handler\npub fn panic(msg: []const u8, _: ?*std.builtin.StackTrace, begin_addr: ?usize) noreturn {\n    @branchHint(.cold);\n    crash(msg, .{ .source = \"global\" }, begin_addr orelse @returnAddress());\n}\n\npub noinline fn crash(\n    reason: []const u8,\n    args: anytype,\n    begin_addr: usize,\n) noreturn {\n    @branchHint(.cold);\n\n    nosuspend switch (panic_level) {\n        0 => {\n            panic_level = panic_level + 1;\n\n            {\n                panic_mutex.lock();\n                defer panic_mutex.unlock();\n\n                var writer_w = std.fs.File.stderr().writerStreaming(&.{});\n                const writer = &writer_w.interface;\n\n                writer.writeAll(\n                    \\\\\n                    \\\\Lightpanda has crashed. Please report the issue:\n                    \\\\https://github.com/lightpanda-io/browser/issues\n                    \\\\or let us know on discord: https://discord.gg/g24PtgD6\n                    \\\\\n                ) catch abort();\n\n                writer.print(\"\\nreason: {s}\\n\", .{reason}) catch abort();\n                writer.print(\"OS: {s}\\n\", .{@tagName(builtin.os.tag)}) catch abort();\n                writer.print(\"mode: {s}\\n\", .{@tagName(builtin.mode)}) catch abort();\n                writer.print(\"version: {s}\\n\", .{lp.build_config.git_commit}) catch abort();\n                inline for (@typeInfo(@TypeOf(args)).@\"struct\".fields) |f| {\n                    writer.writeAll(f.name ++ \": \") catch break;\n                    @import(\"log.zig\").writeValue(.pretty, @field(args, f.name), writer) catch abort();\n                    writer.writeByte('\\n') catch abort();\n                }\n\n                std.debug.dumpCurrentStackTraceToWriter(begin_addr, writer) catch abort();\n            }\n\n            report(reason, begin_addr, args) catch {};\n        },\n        1 => {\n            panic_level = 2;\n            var stderr_w = std.fs.File.stderr().writerStreaming(&.{});\n            const stderr = &stderr_w.interface;\n            stderr.writeAll(\"panicked during a panic. Aborting.\\n\") catch abort();\n        },\n        else => {},\n    };\n\n    abort();\n}\n\nfn report(reason: []const u8, begin_addr: usize, args: anytype) !void {\n    if (comptime IS_DEBUG) {\n        return;\n    }\n\n    if (@import(\"telemetry/telemetry.zig\").isDisabled()) {\n        return;\n    }\n\n    var curl_path: [2048]u8 = undefined;\n    const curl_path_len = curlPath(&curl_path) orelse return;\n\n    var url_buffer: [4096]u8 = undefined;\n    const url = blk: {\n        var writer: std.Io.Writer = .fixed(&url_buffer);\n        try writer.print(\"https://crash.lightpanda.io/c?v={s}&r=\", .{lp.build_config.git_commit});\n        for (reason) |b| {\n            switch (b) {\n                'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => try writer.writeByte(b),\n                ' ' => try writer.writeByte('+'),\n                else => try writer.writeByte('!'), // some weird character, that we shouldn't have, but that'll we'll replace with a weird (bur url-safe) character\n            }\n        }\n\n        try writer.writeByte(0);\n        break :blk writer.buffered();\n    };\n\n    var body_buffer: [8192]u8 = undefined;\n    const body = blk: {\n        var writer: std.Io.Writer = .fixed(body_buffer[0..8191]); // reserve 1 space\n        inline for (@typeInfo(@TypeOf(args)).@\"struct\".fields) |f| {\n            writer.writeAll(f.name ++ \": \") catch break;\n            @import(\"log.zig\").writeValue(.pretty, @field(args, f.name), &writer) catch {};\n            writer.writeByte('\\n') catch {};\n        }\n\n        std.debug.dumpCurrentStackTraceToWriter(begin_addr, &writer) catch {};\n        const written = writer.buffered();\n        if (written.len == 0) {\n            break :blk \"???\";\n        }\n        // Overwrite the last character with our null terminator\n        // body_buffer always has to be > written\n        body_buffer[written.len] = 0;\n        break :blk body_buffer[0 .. written.len + 1];\n    };\n\n    var argv = [_:null]?[*:0]const u8{\n        curl_path[0..curl_path_len :0],\n        \"-fsSL\",\n        \"-H\",\n        \"Content-Type: application/octet-stream\",\n        \"--data-binary\",\n        body[0 .. body.len - 1 :0],\n        url[0 .. url.len - 1 :0],\n    };\n\n    const result = std.c.fork();\n    switch (result) {\n        0 => {\n            _ = std.c.close(0);\n            _ = std.c.close(1);\n            _ = std.c.close(2);\n            _ = std.c.execve(argv[0].?, &argv, std.c.environ);\n            std.c.exit(0);\n        },\n        else => return,\n    }\n}\n\nfn curlPath(buf: []u8) ?usize {\n    const path = std.posix.getenv(\"PATH\") orelse return null;\n    var it = std.mem.tokenizeScalar(u8, path, std.fs.path.delimiter);\n\n    var fba = std.heap.FixedBufferAllocator.init(buf);\n    const allocator = fba.allocator();\n\n    const cwd = std.fs.cwd();\n    while (it.next()) |p| {\n        defer fba.reset();\n        const full_path = std.fs.path.joinZ(allocator, &.{ p, \"curl\" }) catch continue;\n        cwd.accessZ(full_path, .{}) catch continue;\n        return full_path.len;\n    }\n    return null;\n}\n"
  },
  {
    "path": "src/crypto.zig",
    "content": "//! libcrypto utilities we use throughout browser.\n\nconst std = @import(\"std\");\n\nconst pthread_rwlock_t = std.c.pthread_rwlock_t;\n\npub const struct_env_md_st = opaque {};\npub const EVP_MD = struct_env_md_st;\npub const evp_pkey_alg_st = opaque {};\npub const EVP_PKEY_ALG = evp_pkey_alg_st;\npub const struct_engine_st = opaque {};\npub const ENGINE = struct_engine_st;\npub const CRYPTO_THREADID = c_int;\npub const struct_asn1_null_st = opaque {};\npub const ASN1_NULL = struct_asn1_null_st;\npub const ASN1_BOOLEAN = c_int;\npub const struct_ASN1_ITEM_st = opaque {};\npub const ASN1_ITEM = struct_ASN1_ITEM_st;\npub const struct_asn1_object_st = opaque {};\npub const ASN1_OBJECT = struct_asn1_object_st;\npub const struct_asn1_pctx_st = opaque {};\npub const ASN1_PCTX = struct_asn1_pctx_st;\npub const struct_asn1_string_st = extern struct {\n    length: c_int,\n    type: c_int,\n    data: [*c]u8,\n    flags: c_long,\n};\npub const ASN1_BIT_STRING = struct_asn1_string_st;\npub const ASN1_BMPSTRING = struct_asn1_string_st;\npub const ASN1_ENUMERATED = struct_asn1_string_st;\npub const ASN1_GENERALIZEDTIME = struct_asn1_string_st;\npub const ASN1_GENERALSTRING = struct_asn1_string_st;\npub const ASN1_IA5STRING = struct_asn1_string_st;\npub const ASN1_INTEGER = struct_asn1_string_st;\npub const ASN1_OCTET_STRING = struct_asn1_string_st;\npub const ASN1_PRINTABLESTRING = struct_asn1_string_st;\npub const ASN1_STRING = struct_asn1_string_st;\npub const ASN1_T61STRING = struct_asn1_string_st;\npub const ASN1_TIME = struct_asn1_string_st;\npub const ASN1_UNIVERSALSTRING = struct_asn1_string_st;\npub const ASN1_UTCTIME = struct_asn1_string_st;\npub const ASN1_UTF8STRING = struct_asn1_string_st;\npub const ASN1_VISIBLESTRING = struct_asn1_string_st;\npub const struct_ASN1_VALUE_st = opaque {};\npub const ASN1_VALUE = struct_ASN1_VALUE_st;\nconst union_unnamed_1 = extern union {\n    ptr: [*c]u8,\n    boolean: ASN1_BOOLEAN,\n    asn1_string: [*c]ASN1_STRING,\n    object: ?*ASN1_OBJECT,\n    integer: [*c]ASN1_INTEGER,\n    enumerated: [*c]ASN1_ENUMERATED,\n    bit_string: [*c]ASN1_BIT_STRING,\n    octet_string: [*c]ASN1_OCTET_STRING,\n    printablestring: [*c]ASN1_PRINTABLESTRING,\n    t61string: [*c]ASN1_T61STRING,\n    ia5string: [*c]ASN1_IA5STRING,\n    generalstring: [*c]ASN1_GENERALSTRING,\n    bmpstring: [*c]ASN1_BMPSTRING,\n    universalstring: [*c]ASN1_UNIVERSALSTRING,\n    utctime: [*c]ASN1_UTCTIME,\n    generalizedtime: [*c]ASN1_GENERALIZEDTIME,\n    visiblestring: [*c]ASN1_VISIBLESTRING,\n    utf8string: [*c]ASN1_UTF8STRING,\n    set: [*c]ASN1_STRING,\n    sequence: [*c]ASN1_STRING,\n    asn1_value: ?*ASN1_VALUE,\n};\npub const struct_asn1_type_st = extern struct {\n    type: c_int,\n    value: union_unnamed_1,\n};\npub const ASN1_TYPE = struct_asn1_type_st;\npub const struct_AUTHORITY_KEYID_st = opaque {};\npub const AUTHORITY_KEYID = struct_AUTHORITY_KEYID_st;\npub const struct_BASIC_CONSTRAINTS_st = opaque {};\npub const BASIC_CONSTRAINTS = struct_BASIC_CONSTRAINTS_st;\npub const struct_DIST_POINT_st = opaque {};\npub const DIST_POINT = struct_DIST_POINT_st;\npub const BN_ULONG = u64;\npub const struct_bignum_st = extern struct {\n    d: [*c]BN_ULONG,\n    width: c_int,\n    dmax: c_int,\n    neg: c_int,\n    flags: c_int,\n};\npub const BIGNUM = struct_bignum_st;\npub const struct_DSA_SIG_st = extern struct {\n    r: [*c]BIGNUM,\n    s: [*c]BIGNUM,\n};\npub const DSA_SIG = struct_DSA_SIG_st;\npub const struct_ISSUING_DIST_POINT_st = opaque {};\npub const ISSUING_DIST_POINT = struct_ISSUING_DIST_POINT_st;\npub const struct_NAME_CONSTRAINTS_st = opaque {};\npub const NAME_CONSTRAINTS = struct_NAME_CONSTRAINTS_st;\npub const struct_X509_pubkey_st = opaque {};\npub const X509_PUBKEY = struct_X509_pubkey_st;\npub const struct_Netscape_spkac_st = extern struct {\n    pubkey: ?*X509_PUBKEY,\n    challenge: [*c]ASN1_IA5STRING,\n};\npub const NETSCAPE_SPKAC = struct_Netscape_spkac_st;\npub const struct_X509_algor_st = extern struct {\n    algorithm: ?*ASN1_OBJECT,\n    parameter: [*c]ASN1_TYPE,\n};\npub const X509_ALGOR = struct_X509_algor_st;\npub const struct_Netscape_spki_st = extern struct {\n    spkac: [*c]NETSCAPE_SPKAC,\n    sig_algor: [*c]X509_ALGOR,\n    signature: [*c]ASN1_BIT_STRING,\n};\npub const NETSCAPE_SPKI = struct_Netscape_spki_st;\npub const struct_RIPEMD160state_st = opaque {};\npub const RIPEMD160_CTX = struct_RIPEMD160state_st;\npub const struct_X509_VERIFY_PARAM_st = opaque {};\npub const X509_VERIFY_PARAM = struct_X509_VERIFY_PARAM_st;\npub const struct_X509_crl_st = opaque {};\npub const X509_CRL = struct_X509_crl_st;\npub const struct_X509_extension_st = opaque {};\npub const X509_EXTENSION = struct_X509_extension_st;\npub const struct_x509_st = opaque {};\npub const X509 = struct_x509_st;\npub const CRYPTO_refcount_t = u32;\npub const struct_openssl_method_common_st = extern struct {\n    references: c_int,\n    is_static: u8,\n};\npub const struct_rsa_meth_st = extern struct {\n    common: struct_openssl_method_common_st,\n    app_data: ?*anyopaque,\n    init: ?*const fn (?*RSA) callconv(.c) c_int,\n    finish: ?*const fn (?*RSA) callconv(.c) c_int,\n    size: ?*const fn (?*const RSA) callconv(.c) usize,\n    sign: ?*const fn (c_int, [*c]const u8, c_uint, [*c]u8, [*c]c_uint, ?*const RSA) callconv(.c) c_int,\n    sign_raw: ?*const fn (?*RSA, [*c]usize, [*c]u8, usize, [*c]const u8, usize, c_int) callconv(.c) c_int,\n    decrypt: ?*const fn (?*RSA, [*c]usize, [*c]u8, usize, [*c]const u8, usize, c_int) callconv(.c) c_int,\n    private_transform: ?*const fn (?*RSA, [*c]u8, [*c]const u8, usize) callconv(.c) c_int,\n    flags: c_int,\n};\npub const RSA_METHOD = struct_rsa_meth_st;\npub const struct_stack_st_void = opaque {};\npub const struct_crypto_ex_data_st = extern struct {\n    sk: ?*struct_stack_st_void,\n};\npub const CRYPTO_EX_DATA = struct_crypto_ex_data_st;\npub const CRYPTO_MUTEX = pthread_rwlock_t;\npub const struct_bn_mont_ctx_st = extern struct {\n    RR: BIGNUM,\n    N: BIGNUM,\n    n0: [2]BN_ULONG,\n};\npub const BN_MONT_CTX = struct_bn_mont_ctx_st;\npub const struct_bn_blinding_st = opaque {};\npub const BN_BLINDING = struct_bn_blinding_st; // boringssl/include/openssl/rsa.h:788:12: warning: struct demoted to opaque type - has bitfield\npub const struct_rsa_st = opaque {};\npub const RSA = struct_rsa_st;\npub const struct_dsa_st = extern struct {\n    version: c_long,\n    p: [*c]BIGNUM,\n    q: [*c]BIGNUM,\n    g: [*c]BIGNUM,\n    pub_key: [*c]BIGNUM,\n    priv_key: [*c]BIGNUM,\n    flags: c_int,\n    method_mont_lock: CRYPTO_MUTEX,\n    method_mont_p: [*c]BN_MONT_CTX,\n    method_mont_q: [*c]BN_MONT_CTX,\n    references: CRYPTO_refcount_t,\n    ex_data: CRYPTO_EX_DATA,\n};\npub const DSA = struct_dsa_st;\npub const struct_dh_st = opaque {};\npub const DH = struct_dh_st;\npub const struct_ec_key_st = opaque {};\npub const EC_KEY = struct_ec_key_st;\nconst union_unnamed_2 = extern union {\n    ptr: ?*anyopaque,\n    rsa: ?*RSA,\n    dsa: [*c]DSA,\n    dh: ?*DH,\n    ec: ?*EC_KEY,\n};\npub const struct_evp_pkey_asn1_method_st = opaque {};\npub const EVP_PKEY_ASN1_METHOD = struct_evp_pkey_asn1_method_st;\npub const struct_evp_pkey_st = extern struct {\n    references: CRYPTO_refcount_t,\n    type: c_int,\n    pkey: union_unnamed_2,\n    ameth: ?*const EVP_PKEY_ASN1_METHOD,\n};\npub const EVP_PKEY = struct_evp_pkey_st;\npub const struct_evp_pkey_ctx_st = opaque {};\npub const EVP_PKEY_CTX = struct_evp_pkey_ctx_st;\n\npub extern fn RAND_bytes(buf: [*]u8, len: usize) c_int;\n\npub extern fn EVP_sha1() *const EVP_MD;\npub extern fn EVP_sha256() *const EVP_MD;\npub extern fn EVP_sha384() *const EVP_MD;\npub extern fn EVP_sha512() *const EVP_MD;\n\npub const EVP_MAX_MD_BLOCK_SIZE = 128;\n\npub extern fn EVP_MD_size(md: ?*const EVP_MD) usize;\npub extern fn EVP_MD_block_size(md: ?*const EVP_MD) usize;\n\npub extern fn CRYPTO_memcmp(a: ?*const anyopaque, b: ?*const anyopaque, len: usize) c_int;\n\npub extern fn HMAC(\n    evp_md: *const EVP_MD,\n    key: *const anyopaque,\n    key_len: usize,\n    data: [*]const u8,\n    data_len: usize,\n    out: [*]u8,\n    out_len: *c_uint,\n) ?[*]u8;\n\npub const X25519_PRIVATE_KEY_LEN = 32;\npub const X25519_PUBLIC_VALUE_LEN = 32;\npub const X25519_SHARED_KEY_LEN = 32;\n\npub extern fn X25519_keypair(out_public_value: *[32]u8, out_private_key: *[32]u8) void;\n\npub const NID_X25519 = @as(c_int, 948);\npub const EVP_PKEY_X25519 = NID_X25519;\npub const NID_ED25519 = 949;\npub const EVP_PKEY_ED25519 = NID_ED25519;\n\npub extern fn EVP_PKEY_new_raw_private_key(@\"type\": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) [*c]EVP_PKEY;\npub extern fn EVP_PKEY_new_raw_public_key(@\"type\": c_int, unused: ?*ENGINE, in: [*c]const u8, len: usize) [*c]EVP_PKEY;\npub extern fn EVP_PKEY_CTX_new(pkey: [*c]EVP_PKEY, e: ?*ENGINE) ?*EVP_PKEY_CTX;\npub extern fn EVP_PKEY_CTX_free(ctx: ?*EVP_PKEY_CTX) void;\npub extern fn EVP_PKEY_derive_init(ctx: ?*EVP_PKEY_CTX) c_int;\npub extern fn EVP_PKEY_derive(ctx: ?*EVP_PKEY_CTX, key: [*c]u8, out_key_len: [*c]usize) c_int;\npub extern fn EVP_PKEY_derive_set_peer(ctx: ?*EVP_PKEY_CTX, peer: [*c]EVP_PKEY) c_int;\npub extern fn EVP_PKEY_free(pkey: ?*EVP_PKEY) void;\n\npub extern fn EVP_DigestSignInit(ctx: ?*EVP_MD_CTX, pctx: ?*?*EVP_PKEY_CTX, typ: ?*const EVP_MD, e: ?*ENGINE, pkey: ?*EVP_PKEY) c_int;\npub extern fn EVP_DigestSign(ctx: ?*EVP_MD_CTX, sig: [*c]u8, sig_len: *usize, data: [*c]const u8, data_len: usize) c_int;\npub extern fn EVP_MD_CTX_new() ?*EVP_MD_CTX;\npub extern fn EVP_MD_CTX_free(ctx: ?*EVP_MD_CTX) void;\npub const struct_evp_md_ctx_st = opaque {};\npub const EVP_MD_CTX = struct_evp_md_ctx_st;\n"
  },
  {
    "path": "src/data/public_suffix_list.zig",
    "content": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\npub fn lookup(value: []const u8) bool {\n    return public_suffix_list.has(value);\n}\n\nconst public_suffix_list = std.StaticStringMap(void).initComptime(entries);\n\nconst entries: []const struct { []const u8, void } =\n    if (builtin.is_test) &.{\n        .{ \"api.gov.uk\", {} },\n        .{ \"gov.uk\", {} },\n    } else &.{\n        .{ \"ac\", {} },\n        .{ \"com.ac\", {} },\n        .{ \"edu.ac\", {} },\n        .{ \"gov.ac\", {} },\n        .{ \"mil.ac\", {} },\n        .{ \"net.ac\", {} },\n        .{ \"org.ac\", {} },\n        .{ \"ad\", {} },\n        .{ \"ae\", {} },\n        .{ \"ac.ae\", {} },\n        .{ \"co.ae\", {} },\n        .{ \"gov.ae\", {} },\n        .{ \"mil.ae\", {} },\n        .{ \"net.ae\", {} },\n        .{ \"org.ae\", {} },\n        .{ \"sch.ae\", {} },\n        .{ \"aero\", {} },\n        .{ \"airline.aero\", {} },\n        .{ \"airport.aero\", {} },\n        .{ \"accident-investigation.aero\", {} },\n        .{ \"accident-prevention.aero\", {} },\n        .{ \"aerobatic.aero\", {} },\n        .{ \"aeroclub.aero\", {} },\n        .{ \"aerodrome.aero\", {} },\n        .{ \"agents.aero\", {} },\n        .{ \"air-surveillance.aero\", {} },\n        .{ \"air-traffic-control.aero\", {} },\n        .{ \"aircraft.aero\", {} },\n        .{ \"airtraffic.aero\", {} },\n        .{ \"ambulance.aero\", {} },\n        .{ \"association.aero\", {} },\n        .{ \"author.aero\", {} },\n        .{ \"ballooning.aero\", {} },\n        .{ \"broker.aero\", {} },\n        .{ \"caa.aero\", {} },\n        .{ \"cargo.aero\", {} },\n        .{ \"catering.aero\", {} },\n        .{ \"certification.aero\", {} },\n        .{ \"championship.aero\", {} },\n        .{ \"charter.aero\", {} },\n        .{ \"civilaviation.aero\", {} },\n        .{ \"club.aero\", {} },\n        .{ \"conference.aero\", {} },\n        .{ \"consultant.aero\", {} },\n        .{ \"consulting.aero\", {} },\n        .{ \"control.aero\", {} },\n        .{ \"council.aero\", {} },\n        .{ \"crew.aero\", {} },\n        .{ \"design.aero\", {} },\n        .{ \"dgca.aero\", {} },\n        .{ \"educator.aero\", {} },\n        .{ \"emergency.aero\", {} },\n        .{ \"engine.aero\", {} },\n        .{ \"engineer.aero\", {} },\n        .{ \"entertainment.aero\", {} },\n        .{ \"equipment.aero\", {} },\n        .{ \"exchange.aero\", {} },\n        .{ \"express.aero\", {} },\n        .{ \"federation.aero\", {} },\n        .{ \"flight.aero\", {} },\n        .{ \"freight.aero\", {} },\n        .{ \"fuel.aero\", {} },\n        .{ \"gliding.aero\", {} },\n        .{ \"government.aero\", {} },\n        .{ \"groundhandling.aero\", {} },\n        .{ \"group.aero\", {} },\n        .{ \"hanggliding.aero\", {} },\n        .{ \"homebuilt.aero\", {} },\n        .{ \"insurance.aero\", {} },\n        .{ \"journal.aero\", {} },\n        .{ \"journalist.aero\", {} },\n        .{ \"leasing.aero\", {} },\n        .{ \"logistics.aero\", {} },\n        .{ \"magazine.aero\", {} },\n        .{ \"maintenance.aero\", {} },\n        .{ \"marketplace.aero\", {} },\n        .{ \"media.aero\", {} },\n        .{ \"microlight.aero\", {} },\n        .{ \"modelling.aero\", {} },\n        .{ \"navigation.aero\", {} },\n        .{ \"parachuting.aero\", {} },\n        .{ \"paragliding.aero\", {} },\n        .{ \"passenger-association.aero\", {} },\n        .{ \"pilot.aero\", {} },\n        .{ \"press.aero\", {} },\n        .{ \"production.aero\", {} },\n        .{ \"recreation.aero\", {} },\n        .{ \"repbody.aero\", {} },\n        .{ \"res.aero\", {} },\n        .{ \"research.aero\", {} },\n        .{ \"rotorcraft.aero\", {} },\n        .{ \"safety.aero\", {} },\n        .{ \"scientist.aero\", {} },\n        .{ \"services.aero\", {} },\n        .{ \"show.aero\", {} },\n        .{ \"skydiving.aero\", {} },\n        .{ \"software.aero\", {} },\n        .{ \"student.aero\", {} },\n        .{ \"taxi.aero\", {} },\n        .{ \"trader.aero\", {} },\n        .{ \"trading.aero\", {} },\n        .{ \"trainer.aero\", {} },\n        .{ \"union.aero\", {} },\n        .{ \"workinggroup.aero\", {} },\n        .{ \"works.aero\", {} },\n        .{ \"af\", {} },\n        .{ \"com.af\", {} },\n        .{ \"edu.af\", {} },\n        .{ \"gov.af\", {} },\n        .{ \"net.af\", {} },\n        .{ \"org.af\", {} },\n        .{ \"ag\", {} },\n        .{ \"co.ag\", {} },\n        .{ \"com.ag\", {} },\n        .{ \"net.ag\", {} },\n        .{ \"nom.ag\", {} },\n        .{ \"org.ag\", {} },\n        .{ \"ai\", {} },\n        .{ \"com.ai\", {} },\n        .{ \"net.ai\", {} },\n        .{ \"off.ai\", {} },\n        .{ \"org.ai\", {} },\n        .{ \"al\", {} },\n        .{ \"com.al\", {} },\n        .{ \"edu.al\", {} },\n        .{ \"gov.al\", {} },\n        .{ \"mil.al\", {} },\n        .{ \"net.al\", {} },\n        .{ \"org.al\", {} },\n        .{ \"am\", {} },\n        .{ \"co.am\", {} },\n        .{ \"com.am\", {} },\n        .{ \"commune.am\", {} },\n        .{ \"net.am\", {} },\n        .{ \"org.am\", {} },\n        .{ \"ao\", {} },\n        .{ \"co.ao\", {} },\n        .{ \"ed.ao\", {} },\n        .{ \"edu.ao\", {} },\n        .{ \"gov.ao\", {} },\n        .{ \"gv.ao\", {} },\n        .{ \"it.ao\", {} },\n        .{ \"og.ao\", {} },\n        .{ \"org.ao\", {} },\n        .{ \"pb.ao\", {} },\n        .{ \"aq\", {} },\n        .{ \"ar\", {} },\n        .{ \"bet.ar\", {} },\n        .{ \"com.ar\", {} },\n        .{ \"coop.ar\", {} },\n        .{ \"edu.ar\", {} },\n        .{ \"gob.ar\", {} },\n        .{ \"gov.ar\", {} },\n        .{ \"int.ar\", {} },\n        .{ \"mil.ar\", {} },\n        .{ \"musica.ar\", {} },\n        .{ \"mutual.ar\", {} },\n        .{ \"net.ar\", {} },\n        .{ \"org.ar\", {} },\n        .{ \"seg.ar\", {} },\n        .{ \"senasa.ar\", {} },\n        .{ \"tur.ar\", {} },\n        .{ \"arpa\", {} },\n        .{ \"e164.arpa\", {} },\n        .{ \"home.arpa\", {} },\n        .{ \"in-addr.arpa\", {} },\n        .{ \"ip6.arpa\", {} },\n        .{ \"iris.arpa\", {} },\n        .{ \"uri.arpa\", {} },\n        .{ \"urn.arpa\", {} },\n        .{ \"as\", {} },\n        .{ \"gov.as\", {} },\n        .{ \"asia\", {} },\n        .{ \"at\", {} },\n        .{ \"ac.at\", {} },\n        .{ \"sth.ac.at\", {} },\n        .{ \"co.at\", {} },\n        .{ \"gv.at\", {} },\n        .{ \"or.at\", {} },\n        .{ \"au\", {} },\n        .{ \"asn.au\", {} },\n        .{ \"com.au\", {} },\n        .{ \"edu.au\", {} },\n        .{ \"gov.au\", {} },\n        .{ \"id.au\", {} },\n        .{ \"net.au\", {} },\n        .{ \"org.au\", {} },\n        .{ \"conf.au\", {} },\n        .{ \"oz.au\", {} },\n        .{ \"act.au\", {} },\n        .{ \"nsw.au\", {} },\n        .{ \"nt.au\", {} },\n        .{ \"qld.au\", {} },\n        .{ \"sa.au\", {} },\n        .{ \"tas.au\", {} },\n        .{ \"vic.au\", {} },\n        .{ \"wa.au\", {} },\n        .{ \"act.edu.au\", {} },\n        .{ \"catholic.edu.au\", {} },\n        .{ \"nsw.edu.au\", {} },\n        .{ \"nt.edu.au\", {} },\n        .{ \"qld.edu.au\", {} },\n        .{ \"sa.edu.au\", {} },\n        .{ \"tas.edu.au\", {} },\n        .{ \"vic.edu.au\", {} },\n        .{ \"wa.edu.au\", {} },\n        .{ \"qld.gov.au\", {} },\n        .{ \"sa.gov.au\", {} },\n        .{ \"tas.gov.au\", {} },\n        .{ \"vic.gov.au\", {} },\n        .{ \"wa.gov.au\", {} },\n        .{ \"aw\", {} },\n        .{ \"com.aw\", {} },\n        .{ \"ax\", {} },\n        .{ \"az\", {} },\n        .{ \"biz.az\", {} },\n        .{ \"co.az\", {} },\n        .{ \"com.az\", {} },\n        .{ \"edu.az\", {} },\n        .{ \"gov.az\", {} },\n        .{ \"info.az\", {} },\n        .{ \"int.az\", {} },\n        .{ \"mil.az\", {} },\n        .{ \"name.az\", {} },\n        .{ \"net.az\", {} },\n        .{ \"org.az\", {} },\n        .{ \"pp.az\", {} },\n        .{ \"pro.az\", {} },\n        .{ \"ba\", {} },\n        .{ \"com.ba\", {} },\n        .{ \"edu.ba\", {} },\n        .{ \"gov.ba\", {} },\n        .{ \"mil.ba\", {} },\n        .{ \"net.ba\", {} },\n        .{ \"org.ba\", {} },\n        .{ \"bb\", {} },\n        .{ \"biz.bb\", {} },\n        .{ \"co.bb\", {} },\n        .{ \"com.bb\", {} },\n        .{ \"edu.bb\", {} },\n        .{ \"gov.bb\", {} },\n        .{ \"info.bb\", {} },\n        .{ \"net.bb\", {} },\n        .{ \"org.bb\", {} },\n        .{ \"store.bb\", {} },\n        .{ \"tv.bb\", {} },\n        .{ \"bd\", {} },\n        .{ \"ac.bd\", {} },\n        .{ \"ai.bd\", {} },\n        .{ \"co.bd\", {} },\n        .{ \"com.bd\", {} },\n        .{ \"edu.bd\", {} },\n        .{ \"gov.bd\", {} },\n        .{ \"id.bd\", {} },\n        .{ \"info.bd\", {} },\n        .{ \"it.bd\", {} },\n        .{ \"mil.bd\", {} },\n        .{ \"net.bd\", {} },\n        .{ \"org.bd\", {} },\n        .{ \"sch.bd\", {} },\n        .{ \"tv.bd\", {} },\n        .{ \"be\", {} },\n        .{ \"ac.be\", {} },\n        .{ \"bf\", {} },\n        .{ \"gov.bf\", {} },\n        .{ \"bg\", {} },\n        .{ \"0.bg\", {} },\n        .{ \"1.bg\", {} },\n        .{ \"2.bg\", {} },\n        .{ \"3.bg\", {} },\n        .{ \"4.bg\", {} },\n        .{ \"5.bg\", {} },\n        .{ \"6.bg\", {} },\n        .{ \"7.bg\", {} },\n        .{ \"8.bg\", {} },\n        .{ \"9.bg\", {} },\n        .{ \"a.bg\", {} },\n        .{ \"b.bg\", {} },\n        .{ \"c.bg\", {} },\n        .{ \"d.bg\", {} },\n        .{ \"e.bg\", {} },\n        .{ \"f.bg\", {} },\n        .{ \"g.bg\", {} },\n        .{ \"h.bg\", {} },\n        .{ \"i.bg\", {} },\n        .{ \"j.bg\", {} },\n        .{ \"k.bg\", {} },\n        .{ \"l.bg\", {} },\n        .{ \"m.bg\", {} },\n        .{ \"n.bg\", {} },\n        .{ \"o.bg\", {} },\n        .{ \"p.bg\", {} },\n        .{ \"q.bg\", {} },\n        .{ \"r.bg\", {} },\n        .{ \"s.bg\", {} },\n        .{ \"t.bg\", {} },\n        .{ \"u.bg\", {} },\n        .{ \"v.bg\", {} },\n        .{ \"w.bg\", {} },\n        .{ \"x.bg\", {} },\n        .{ \"y.bg\", {} },\n        .{ \"z.bg\", {} },\n        .{ \"bh\", {} },\n        .{ \"com.bh\", {} },\n        .{ \"edu.bh\", {} },\n        .{ \"gov.bh\", {} },\n        .{ \"net.bh\", {} },\n        .{ \"org.bh\", {} },\n        .{ \"bi\", {} },\n        .{ \"co.bi\", {} },\n        .{ \"com.bi\", {} },\n        .{ \"edu.bi\", {} },\n        .{ \"or.bi\", {} },\n        .{ \"org.bi\", {} },\n        .{ \"biz\", {} },\n        .{ \"bj\", {} },\n        .{ \"africa.bj\", {} },\n        .{ \"agro.bj\", {} },\n        .{ \"architectes.bj\", {} },\n        .{ \"assur.bj\", {} },\n        .{ \"avocats.bj\", {} },\n        .{ \"co.bj\", {} },\n        .{ \"com.bj\", {} },\n        .{ \"eco.bj\", {} },\n        .{ \"econo.bj\", {} },\n        .{ \"edu.bj\", {} },\n        .{ \"info.bj\", {} },\n        .{ \"loisirs.bj\", {} },\n        .{ \"money.bj\", {} },\n        .{ \"net.bj\", {} },\n        .{ \"org.bj\", {} },\n        .{ \"ote.bj\", {} },\n        .{ \"restaurant.bj\", {} },\n        .{ \"resto.bj\", {} },\n        .{ \"tourism.bj\", {} },\n        .{ \"univ.bj\", {} },\n        .{ \"bm\", {} },\n        .{ \"com.bm\", {} },\n        .{ \"edu.bm\", {} },\n        .{ \"gov.bm\", {} },\n        .{ \"net.bm\", {} },\n        .{ \"org.bm\", {} },\n        .{ \"bn\", {} },\n        .{ \"com.bn\", {} },\n        .{ \"edu.bn\", {} },\n        .{ \"gov.bn\", {} },\n        .{ \"net.bn\", {} },\n        .{ \"org.bn\", {} },\n        .{ \"bo\", {} },\n        .{ \"com.bo\", {} },\n        .{ \"edu.bo\", {} },\n        .{ \"gob.bo\", {} },\n        .{ \"int.bo\", {} },\n        .{ \"mil.bo\", {} },\n        .{ \"net.bo\", {} },\n        .{ \"org.bo\", {} },\n        .{ \"tv.bo\", {} },\n        .{ \"web.bo\", {} },\n        .{ \"academia.bo\", {} },\n        .{ \"agro.bo\", {} },\n        .{ \"arte.bo\", {} },\n        .{ \"blog.bo\", {} },\n        .{ \"bolivia.bo\", {} },\n        .{ \"ciencia.bo\", {} },\n        .{ \"cooperativa.bo\", {} },\n        .{ \"democracia.bo\", {} },\n        .{ \"deporte.bo\", {} },\n        .{ \"ecologia.bo\", {} },\n        .{ \"economia.bo\", {} },\n        .{ \"empresa.bo\", {} },\n        .{ \"indigena.bo\", {} },\n        .{ \"industria.bo\", {} },\n        .{ \"info.bo\", {} },\n        .{ \"medicina.bo\", {} },\n        .{ \"movimiento.bo\", {} },\n        .{ \"musica.bo\", {} },\n        .{ \"natural.bo\", {} },\n        .{ \"nombre.bo\", {} },\n        .{ \"noticias.bo\", {} },\n        .{ \"patria.bo\", {} },\n        .{ \"plurinacional.bo\", {} },\n        .{ \"politica.bo\", {} },\n        .{ \"profesional.bo\", {} },\n        .{ \"pueblo.bo\", {} },\n        .{ \"revista.bo\", {} },\n        .{ \"salud.bo\", {} },\n        .{ \"tecnologia.bo\", {} },\n        .{ \"tksat.bo\", {} },\n        .{ \"transporte.bo\", {} },\n        .{ \"wiki.bo\", {} },\n        .{ \"br\", {} },\n        .{ \"9guacu.br\", {} },\n        .{ \"abc.br\", {} },\n        .{ \"adm.br\", {} },\n        .{ \"adv.br\", {} },\n        .{ \"agr.br\", {} },\n        .{ \"aju.br\", {} },\n        .{ \"am.br\", {} },\n        .{ \"anani.br\", {} },\n        .{ \"aparecida.br\", {} },\n        .{ \"api.br\", {} },\n        .{ \"app.br\", {} },\n        .{ \"arq.br\", {} },\n        .{ \"art.br\", {} },\n        .{ \"ato.br\", {} },\n        .{ \"b.br\", {} },\n        .{ \"barueri.br\", {} },\n        .{ \"belem.br\", {} },\n        .{ \"bet.br\", {} },\n        .{ \"bhz.br\", {} },\n        .{ \"bib.br\", {} },\n        .{ \"bio.br\", {} },\n        .{ \"blog.br\", {} },\n        .{ \"bmd.br\", {} },\n        .{ \"boavista.br\", {} },\n        .{ \"bsb.br\", {} },\n        .{ \"campinagrande.br\", {} },\n        .{ \"campinas.br\", {} },\n        .{ \"caxias.br\", {} },\n        .{ \"cim.br\", {} },\n        .{ \"cng.br\", {} },\n        .{ \"cnt.br\", {} },\n        .{ \"com.br\", {} },\n        .{ \"contagem.br\", {} },\n        .{ \"coop.br\", {} },\n        .{ \"coz.br\", {} },\n        .{ \"cri.br\", {} },\n        .{ \"cuiaba.br\", {} },\n        .{ \"curitiba.br\", {} },\n        .{ \"def.br\", {} },\n        .{ \"des.br\", {} },\n        .{ \"det.br\", {} },\n        .{ \"dev.br\", {} },\n        .{ \"ecn.br\", {} },\n        .{ \"eco.br\", {} },\n        .{ \"edu.br\", {} },\n        .{ \"emp.br\", {} },\n        .{ \"enf.br\", {} },\n        .{ \"eng.br\", {} },\n        .{ \"esp.br\", {} },\n        .{ \"etc.br\", {} },\n        .{ \"eti.br\", {} },\n        .{ \"far.br\", {} },\n        .{ \"feira.br\", {} },\n        .{ \"flog.br\", {} },\n        .{ \"floripa.br\", {} },\n        .{ \"fm.br\", {} },\n        .{ \"fnd.br\", {} },\n        .{ \"fortal.br\", {} },\n        .{ \"fot.br\", {} },\n        .{ \"foz.br\", {} },\n        .{ \"fst.br\", {} },\n        .{ \"g12.br\", {} },\n        .{ \"geo.br\", {} },\n        .{ \"ggf.br\", {} },\n        .{ \"goiania.br\", {} },\n        .{ \"gov.br\", {} },\n        .{ \"ac.gov.br\", {} },\n        .{ \"al.gov.br\", {} },\n        .{ \"am.gov.br\", {} },\n        .{ \"ap.gov.br\", {} },\n        .{ \"ba.gov.br\", {} },\n        .{ \"ce.gov.br\", {} },\n        .{ \"df.gov.br\", {} },\n        .{ \"es.gov.br\", {} },\n        .{ \"go.gov.br\", {} },\n        .{ \"ma.gov.br\", {} },\n        .{ \"mg.gov.br\", {} },\n        .{ \"ms.gov.br\", {} },\n        .{ \"mt.gov.br\", {} },\n        .{ \"pa.gov.br\", {} },\n        .{ \"pb.gov.br\", {} },\n        .{ \"pe.gov.br\", {} },\n        .{ \"pi.gov.br\", {} },\n        .{ \"pr.gov.br\", {} },\n        .{ \"rj.gov.br\", {} },\n        .{ \"rn.gov.br\", {} },\n        .{ \"ro.gov.br\", {} },\n        .{ \"rr.gov.br\", {} },\n        .{ \"rs.gov.br\", {} },\n        .{ \"sc.gov.br\", {} },\n        .{ \"se.gov.br\", {} },\n        .{ \"sp.gov.br\", {} },\n        .{ \"to.gov.br\", {} },\n        .{ \"gru.br\", {} },\n        .{ \"ia.br\", {} },\n        .{ \"imb.br\", {} },\n        .{ \"ind.br\", {} },\n        .{ \"inf.br\", {} },\n        .{ \"jab.br\", {} },\n        .{ \"jampa.br\", {} },\n        .{ \"jdf.br\", {} },\n        .{ \"joinville.br\", {} },\n        .{ \"jor.br\", {} },\n        .{ \"jus.br\", {} },\n        .{ \"leg.br\", {} },\n        .{ \"leilao.br\", {} },\n        .{ \"lel.br\", {} },\n        .{ \"log.br\", {} },\n        .{ \"londrina.br\", {} },\n        .{ \"macapa.br\", {} },\n        .{ \"maceio.br\", {} },\n        .{ \"manaus.br\", {} },\n        .{ \"maringa.br\", {} },\n        .{ \"mat.br\", {} },\n        .{ \"med.br\", {} },\n        .{ \"mil.br\", {} },\n        .{ \"morena.br\", {} },\n        .{ \"mp.br\", {} },\n        .{ \"mus.br\", {} },\n        .{ \"natal.br\", {} },\n        .{ \"net.br\", {} },\n        .{ \"niteroi.br\", {} },\n        .{ \"*.nom.br\", {} },\n        .{ \"not.br\", {} },\n        .{ \"ntr.br\", {} },\n        .{ \"odo.br\", {} },\n        .{ \"ong.br\", {} },\n        .{ \"org.br\", {} },\n        .{ \"osasco.br\", {} },\n        .{ \"palmas.br\", {} },\n        .{ \"poa.br\", {} },\n        .{ \"ppg.br\", {} },\n        .{ \"pro.br\", {} },\n        .{ \"psc.br\", {} },\n        .{ \"psi.br\", {} },\n        .{ \"pvh.br\", {} },\n        .{ \"qsl.br\", {} },\n        .{ \"radio.br\", {} },\n        .{ \"rec.br\", {} },\n        .{ \"recife.br\", {} },\n        .{ \"rep.br\", {} },\n        .{ \"ribeirao.br\", {} },\n        .{ \"rio.br\", {} },\n        .{ \"riobranco.br\", {} },\n        .{ \"riopreto.br\", {} },\n        .{ \"salvador.br\", {} },\n        .{ \"sampa.br\", {} },\n        .{ \"santamaria.br\", {} },\n        .{ \"santoandre.br\", {} },\n        .{ \"saobernardo.br\", {} },\n        .{ \"saogonca.br\", {} },\n        .{ \"seg.br\", {} },\n        .{ \"sjc.br\", {} },\n        .{ \"slg.br\", {} },\n        .{ \"slz.br\", {} },\n        .{ \"social.br\", {} },\n        .{ \"sorocaba.br\", {} },\n        .{ \"srv.br\", {} },\n        .{ \"taxi.br\", {} },\n        .{ \"tc.br\", {} },\n        .{ \"tec.br\", {} },\n        .{ \"teo.br\", {} },\n        .{ \"the.br\", {} },\n        .{ \"tmp.br\", {} },\n        .{ \"trd.br\", {} },\n        .{ \"tur.br\", {} },\n        .{ \"tv.br\", {} },\n        .{ \"udi.br\", {} },\n        .{ \"vet.br\", {} },\n        .{ \"vix.br\", {} },\n        .{ \"vlog.br\", {} },\n        .{ \"wiki.br\", {} },\n        .{ \"xyz.br\", {} },\n        .{ \"zlg.br\", {} },\n        .{ \"bs\", {} },\n        .{ \"com.bs\", {} },\n        .{ \"edu.bs\", {} },\n        .{ \"gov.bs\", {} },\n        .{ \"net.bs\", {} },\n        .{ \"org.bs\", {} },\n        .{ \"bt\", {} },\n        .{ \"com.bt\", {} },\n        .{ \"edu.bt\", {} },\n        .{ \"gov.bt\", {} },\n        .{ \"net.bt\", {} },\n        .{ \"org.bt\", {} },\n        .{ \"bv\", {} },\n        .{ \"bw\", {} },\n        .{ \"ac.bw\", {} },\n        .{ \"co.bw\", {} },\n        .{ \"gov.bw\", {} },\n        .{ \"net.bw\", {} },\n        .{ \"org.bw\", {} },\n        .{ \"by\", {} },\n        .{ \"gov.by\", {} },\n        .{ \"mil.by\", {} },\n        .{ \"com.by\", {} },\n        .{ \"of.by\", {} },\n        .{ \"bz\", {} },\n        .{ \"co.bz\", {} },\n        .{ \"com.bz\", {} },\n        .{ \"edu.bz\", {} },\n        .{ \"gov.bz\", {} },\n        .{ \"net.bz\", {} },\n        .{ \"org.bz\", {} },\n        .{ \"ca\", {} },\n        .{ \"ab.ca\", {} },\n        .{ \"bc.ca\", {} },\n        .{ \"mb.ca\", {} },\n        .{ \"nb.ca\", {} },\n        .{ \"nf.ca\", {} },\n        .{ \"nl.ca\", {} },\n        .{ \"ns.ca\", {} },\n        .{ \"nt.ca\", {} },\n        .{ \"nu.ca\", {} },\n        .{ \"on.ca\", {} },\n        .{ \"pe.ca\", {} },\n        .{ \"qc.ca\", {} },\n        .{ \"sk.ca\", {} },\n        .{ \"yk.ca\", {} },\n        .{ \"gc.ca\", {} },\n        .{ \"cat\", {} },\n        .{ \"cc\", {} },\n        .{ \"cd\", {} },\n        .{ \"gov.cd\", {} },\n        .{ \"cf\", {} },\n        .{ \"cg\", {} },\n        .{ \"ch\", {} },\n        .{ \"ci\", {} },\n        .{ \"ac.ci\", {} },\n        .{ \"aéroport.ci\", {} },\n        .{ \"asso.ci\", {} },\n        .{ \"co.ci\", {} },\n        .{ \"com.ci\", {} },\n        .{ \"ed.ci\", {} },\n        .{ \"edu.ci\", {} },\n        .{ \"go.ci\", {} },\n        .{ \"gouv.ci\", {} },\n        .{ \"int.ci\", {} },\n        .{ \"net.ci\", {} },\n        .{ \"or.ci\", {} },\n        .{ \"org.ci\", {} },\n        .{ \"*.ck\", {} },\n        .{ \"!www.ck\", {} },\n        .{ \"cl\", {} },\n        .{ \"co.cl\", {} },\n        .{ \"gob.cl\", {} },\n        .{ \"gov.cl\", {} },\n        .{ \"mil.cl\", {} },\n        .{ \"cm\", {} },\n        .{ \"co.cm\", {} },\n        .{ \"com.cm\", {} },\n        .{ \"gov.cm\", {} },\n        .{ \"net.cm\", {} },\n        .{ \"cn\", {} },\n        .{ \"ac.cn\", {} },\n        .{ \"com.cn\", {} },\n        .{ \"edu.cn\", {} },\n        .{ \"gov.cn\", {} },\n        .{ \"mil.cn\", {} },\n        .{ \"net.cn\", {} },\n        .{ \"org.cn\", {} },\n        .{ \"公司.cn\", {} },\n        .{ \"網絡.cn\", {} },\n        .{ \"网络.cn\", {} },\n        .{ \"ah.cn\", {} },\n        .{ \"bj.cn\", {} },\n        .{ \"cq.cn\", {} },\n        .{ \"fj.cn\", {} },\n        .{ \"gd.cn\", {} },\n        .{ \"gs.cn\", {} },\n        .{ \"gx.cn\", {} },\n        .{ \"gz.cn\", {} },\n        .{ \"ha.cn\", {} },\n        .{ \"hb.cn\", {} },\n        .{ \"he.cn\", {} },\n        .{ \"hi.cn\", {} },\n        .{ \"hk.cn\", {} },\n        .{ \"hl.cn\", {} },\n        .{ \"hn.cn\", {} },\n        .{ \"jl.cn\", {} },\n        .{ \"js.cn\", {} },\n        .{ \"jx.cn\", {} },\n        .{ \"ln.cn\", {} },\n        .{ \"mo.cn\", {} },\n        .{ \"nm.cn\", {} },\n        .{ \"nx.cn\", {} },\n        .{ \"qh.cn\", {} },\n        .{ \"sc.cn\", {} },\n        .{ \"sd.cn\", {} },\n        .{ \"sh.cn\", {} },\n        .{ \"sn.cn\", {} },\n        .{ \"sx.cn\", {} },\n        .{ \"tj.cn\", {} },\n        .{ \"tw.cn\", {} },\n        .{ \"xj.cn\", {} },\n        .{ \"xz.cn\", {} },\n        .{ \"yn.cn\", {} },\n        .{ \"zj.cn\", {} },\n        .{ \"co\", {} },\n        .{ \"com.co\", {} },\n        .{ \"edu.co\", {} },\n        .{ \"gov.co\", {} },\n        .{ \"mil.co\", {} },\n        .{ \"net.co\", {} },\n        .{ \"nom.co\", {} },\n        .{ \"org.co\", {} },\n        .{ \"com\", {} },\n        .{ \"coop\", {} },\n        .{ \"cr\", {} },\n        .{ \"ac.cr\", {} },\n        .{ \"co.cr\", {} },\n        .{ \"ed.cr\", {} },\n        .{ \"fi.cr\", {} },\n        .{ \"go.cr\", {} },\n        .{ \"or.cr\", {} },\n        .{ \"sa.cr\", {} },\n        .{ \"cu\", {} },\n        .{ \"com.cu\", {} },\n        .{ \"edu.cu\", {} },\n        .{ \"gob.cu\", {} },\n        .{ \"inf.cu\", {} },\n        .{ \"nat.cu\", {} },\n        .{ \"net.cu\", {} },\n        .{ \"org.cu\", {} },\n        .{ \"cv\", {} },\n        .{ \"com.cv\", {} },\n        .{ \"edu.cv\", {} },\n        .{ \"id.cv\", {} },\n        .{ \"int.cv\", {} },\n        .{ \"net.cv\", {} },\n        .{ \"nome.cv\", {} },\n        .{ \"org.cv\", {} },\n        .{ \"publ.cv\", {} },\n        .{ \"cw\", {} },\n        .{ \"com.cw\", {} },\n        .{ \"edu.cw\", {} },\n        .{ \"net.cw\", {} },\n        .{ \"org.cw\", {} },\n        .{ \"cx\", {} },\n        .{ \"gov.cx\", {} },\n        .{ \"cy\", {} },\n        .{ \"ac.cy\", {} },\n        .{ \"biz.cy\", {} },\n        .{ \"com.cy\", {} },\n        .{ \"ekloges.cy\", {} },\n        .{ \"gov.cy\", {} },\n        .{ \"ltd.cy\", {} },\n        .{ \"mil.cy\", {} },\n        .{ \"net.cy\", {} },\n        .{ \"org.cy\", {} },\n        .{ \"press.cy\", {} },\n        .{ \"pro.cy\", {} },\n        .{ \"tm.cy\", {} },\n        .{ \"cz\", {} },\n        .{ \"gov.cz\", {} },\n        .{ \"de\", {} },\n        .{ \"dj\", {} },\n        .{ \"dk\", {} },\n        .{ \"dm\", {} },\n        .{ \"co.dm\", {} },\n        .{ \"com.dm\", {} },\n        .{ \"edu.dm\", {} },\n        .{ \"gov.dm\", {} },\n        .{ \"net.dm\", {} },\n        .{ \"org.dm\", {} },\n        .{ \"do\", {} },\n        .{ \"art.do\", {} },\n        .{ \"com.do\", {} },\n        .{ \"edu.do\", {} },\n        .{ \"gob.do\", {} },\n        .{ \"gov.do\", {} },\n        .{ \"mil.do\", {} },\n        .{ \"net.do\", {} },\n        .{ \"org.do\", {} },\n        .{ \"sld.do\", {} },\n        .{ \"web.do\", {} },\n        .{ \"dz\", {} },\n        .{ \"art.dz\", {} },\n        .{ \"asso.dz\", {} },\n        .{ \"com.dz\", {} },\n        .{ \"edu.dz\", {} },\n        .{ \"gov.dz\", {} },\n        .{ \"net.dz\", {} },\n        .{ \"org.dz\", {} },\n        .{ \"pol.dz\", {} },\n        .{ \"soc.dz\", {} },\n        .{ \"tm.dz\", {} },\n        .{ \"ec\", {} },\n        .{ \"abg.ec\", {} },\n        .{ \"adm.ec\", {} },\n        .{ \"agron.ec\", {} },\n        .{ \"arqt.ec\", {} },\n        .{ \"art.ec\", {} },\n        .{ \"bar.ec\", {} },\n        .{ \"chef.ec\", {} },\n        .{ \"com.ec\", {} },\n        .{ \"cont.ec\", {} },\n        .{ \"cpa.ec\", {} },\n        .{ \"cue.ec\", {} },\n        .{ \"dent.ec\", {} },\n        .{ \"dgn.ec\", {} },\n        .{ \"disco.ec\", {} },\n        .{ \"doc.ec\", {} },\n        .{ \"edu.ec\", {} },\n        .{ \"eng.ec\", {} },\n        .{ \"esm.ec\", {} },\n        .{ \"fin.ec\", {} },\n        .{ \"fot.ec\", {} },\n        .{ \"gal.ec\", {} },\n        .{ \"gob.ec\", {} },\n        .{ \"gov.ec\", {} },\n        .{ \"gye.ec\", {} },\n        .{ \"ibr.ec\", {} },\n        .{ \"info.ec\", {} },\n        .{ \"k12.ec\", {} },\n        .{ \"lat.ec\", {} },\n        .{ \"loj.ec\", {} },\n        .{ \"med.ec\", {} },\n        .{ \"mil.ec\", {} },\n        .{ \"mktg.ec\", {} },\n        .{ \"mon.ec\", {} },\n        .{ \"net.ec\", {} },\n        .{ \"ntr.ec\", {} },\n        .{ \"odont.ec\", {} },\n        .{ \"org.ec\", {} },\n        .{ \"pro.ec\", {} },\n        .{ \"prof.ec\", {} },\n        .{ \"psic.ec\", {} },\n        .{ \"psiq.ec\", {} },\n        .{ \"pub.ec\", {} },\n        .{ \"rio.ec\", {} },\n        .{ \"rrpp.ec\", {} },\n        .{ \"sal.ec\", {} },\n        .{ \"tech.ec\", {} },\n        .{ \"tul.ec\", {} },\n        .{ \"tur.ec\", {} },\n        .{ \"uio.ec\", {} },\n        .{ \"vet.ec\", {} },\n        .{ \"xxx.ec\", {} },\n        .{ \"edu\", {} },\n        .{ \"ee\", {} },\n        .{ \"aip.ee\", {} },\n        .{ \"com.ee\", {} },\n        .{ \"edu.ee\", {} },\n        .{ \"fie.ee\", {} },\n        .{ \"gov.ee\", {} },\n        .{ \"lib.ee\", {} },\n        .{ \"med.ee\", {} },\n        .{ \"org.ee\", {} },\n        .{ \"pri.ee\", {} },\n        .{ \"riik.ee\", {} },\n        .{ \"eg\", {} },\n        .{ \"ac.eg\", {} },\n        .{ \"com.eg\", {} },\n        .{ \"edu.eg\", {} },\n        .{ \"eun.eg\", {} },\n        .{ \"gov.eg\", {} },\n        .{ \"info.eg\", {} },\n        .{ \"me.eg\", {} },\n        .{ \"mil.eg\", {} },\n        .{ \"name.eg\", {} },\n        .{ \"net.eg\", {} },\n        .{ \"org.eg\", {} },\n        .{ \"sci.eg\", {} },\n        .{ \"sport.eg\", {} },\n        .{ \"tv.eg\", {} },\n        .{ \"*.er\", {} },\n        .{ \"es\", {} },\n        .{ \"com.es\", {} },\n        .{ \"edu.es\", {} },\n        .{ \"gob.es\", {} },\n        .{ \"nom.es\", {} },\n        .{ \"org.es\", {} },\n        .{ \"et\", {} },\n        .{ \"biz.et\", {} },\n        .{ \"com.et\", {} },\n        .{ \"edu.et\", {} },\n        .{ \"gov.et\", {} },\n        .{ \"info.et\", {} },\n        .{ \"name.et\", {} },\n        .{ \"net.et\", {} },\n        .{ \"org.et\", {} },\n        .{ \"eu\", {} },\n        .{ \"fi\", {} },\n        .{ \"aland.fi\", {} },\n        .{ \"fj\", {} },\n        .{ \"ac.fj\", {} },\n        .{ \"biz.fj\", {} },\n        .{ \"com.fj\", {} },\n        .{ \"edu.fj\", {} },\n        .{ \"gov.fj\", {} },\n        .{ \"id.fj\", {} },\n        .{ \"info.fj\", {} },\n        .{ \"mil.fj\", {} },\n        .{ \"name.fj\", {} },\n        .{ \"net.fj\", {} },\n        .{ \"org.fj\", {} },\n        .{ \"pro.fj\", {} },\n        .{ \"*.fk\", {} },\n        .{ \"fm\", {} },\n        .{ \"com.fm\", {} },\n        .{ \"edu.fm\", {} },\n        .{ \"net.fm\", {} },\n        .{ \"org.fm\", {} },\n        .{ \"fo\", {} },\n        .{ \"fr\", {} },\n        .{ \"asso.fr\", {} },\n        .{ \"com.fr\", {} },\n        .{ \"gouv.fr\", {} },\n        .{ \"nom.fr\", {} },\n        .{ \"prd.fr\", {} },\n        .{ \"tm.fr\", {} },\n        .{ \"avoues.fr\", {} },\n        .{ \"cci.fr\", {} },\n        .{ \"greta.fr\", {} },\n        .{ \"huissier-justice.fr\", {} },\n        .{ \"ga\", {} },\n        .{ \"gb\", {} },\n        .{ \"gd\", {} },\n        .{ \"edu.gd\", {} },\n        .{ \"gov.gd\", {} },\n        .{ \"ge\", {} },\n        .{ \"com.ge\", {} },\n        .{ \"edu.ge\", {} },\n        .{ \"gov.ge\", {} },\n        .{ \"net.ge\", {} },\n        .{ \"org.ge\", {} },\n        .{ \"pvt.ge\", {} },\n        .{ \"school.ge\", {} },\n        .{ \"gf\", {} },\n        .{ \"gg\", {} },\n        .{ \"co.gg\", {} },\n        .{ \"net.gg\", {} },\n        .{ \"org.gg\", {} },\n        .{ \"gh\", {} },\n        .{ \"biz.gh\", {} },\n        .{ \"com.gh\", {} },\n        .{ \"edu.gh\", {} },\n        .{ \"gov.gh\", {} },\n        .{ \"mil.gh\", {} },\n        .{ \"net.gh\", {} },\n        .{ \"org.gh\", {} },\n        .{ \"gi\", {} },\n        .{ \"com.gi\", {} },\n        .{ \"edu.gi\", {} },\n        .{ \"gov.gi\", {} },\n        .{ \"ltd.gi\", {} },\n        .{ \"mod.gi\", {} },\n        .{ \"org.gi\", {} },\n        .{ \"gl\", {} },\n        .{ \"co.gl\", {} },\n        .{ \"com.gl\", {} },\n        .{ \"edu.gl\", {} },\n        .{ \"net.gl\", {} },\n        .{ \"org.gl\", {} },\n        .{ \"gm\", {} },\n        .{ \"gn\", {} },\n        .{ \"ac.gn\", {} },\n        .{ \"com.gn\", {} },\n        .{ \"edu.gn\", {} },\n        .{ \"gov.gn\", {} },\n        .{ \"net.gn\", {} },\n        .{ \"org.gn\", {} },\n        .{ \"gov\", {} },\n        .{ \"gp\", {} },\n        .{ \"asso.gp\", {} },\n        .{ \"com.gp\", {} },\n        .{ \"edu.gp\", {} },\n        .{ \"mobi.gp\", {} },\n        .{ \"net.gp\", {} },\n        .{ \"org.gp\", {} },\n        .{ \"gq\", {} },\n        .{ \"gr\", {} },\n        .{ \"com.gr\", {} },\n        .{ \"edu.gr\", {} },\n        .{ \"gov.gr\", {} },\n        .{ \"net.gr\", {} },\n        .{ \"org.gr\", {} },\n        .{ \"gs\", {} },\n        .{ \"gt\", {} },\n        .{ \"com.gt\", {} },\n        .{ \"edu.gt\", {} },\n        .{ \"gob.gt\", {} },\n        .{ \"ind.gt\", {} },\n        .{ \"mil.gt\", {} },\n        .{ \"net.gt\", {} },\n        .{ \"org.gt\", {} },\n        .{ \"gu\", {} },\n        .{ \"com.gu\", {} },\n        .{ \"edu.gu\", {} },\n        .{ \"gov.gu\", {} },\n        .{ \"guam.gu\", {} },\n        .{ \"info.gu\", {} },\n        .{ \"net.gu\", {} },\n        .{ \"org.gu\", {} },\n        .{ \"web.gu\", {} },\n        .{ \"gw\", {} },\n        .{ \"gy\", {} },\n        .{ \"co.gy\", {} },\n        .{ \"com.gy\", {} },\n        .{ \"edu.gy\", {} },\n        .{ \"gov.gy\", {} },\n        .{ \"net.gy\", {} },\n        .{ \"org.gy\", {} },\n        .{ \"hk\", {} },\n        .{ \"com.hk\", {} },\n        .{ \"edu.hk\", {} },\n        .{ \"gov.hk\", {} },\n        .{ \"idv.hk\", {} },\n        .{ \"net.hk\", {} },\n        .{ \"org.hk\", {} },\n        .{ \"个人.hk\", {} },\n        .{ \"個人.hk\", {} },\n        .{ \"公司.hk\", {} },\n        .{ \"政府.hk\", {} },\n        .{ \"敎育.hk\", {} },\n        .{ \"教育.hk\", {} },\n        .{ \"箇人.hk\", {} },\n        .{ \"組織.hk\", {} },\n        .{ \"組织.hk\", {} },\n        .{ \"網絡.hk\", {} },\n        .{ \"網络.hk\", {} },\n        .{ \"组織.hk\", {} },\n        .{ \"组织.hk\", {} },\n        .{ \"网絡.hk\", {} },\n        .{ \"网络.hk\", {} },\n        .{ \"hm\", {} },\n        .{ \"hn\", {} },\n        .{ \"com.hn\", {} },\n        .{ \"edu.hn\", {} },\n        .{ \"gob.hn\", {} },\n        .{ \"mil.hn\", {} },\n        .{ \"net.hn\", {} },\n        .{ \"org.hn\", {} },\n        .{ \"hr\", {} },\n        .{ \"com.hr\", {} },\n        .{ \"from.hr\", {} },\n        .{ \"iz.hr\", {} },\n        .{ \"name.hr\", {} },\n        .{ \"ht\", {} },\n        .{ \"adult.ht\", {} },\n        .{ \"art.ht\", {} },\n        .{ \"asso.ht\", {} },\n        .{ \"com.ht\", {} },\n        .{ \"coop.ht\", {} },\n        .{ \"edu.ht\", {} },\n        .{ \"firm.ht\", {} },\n        .{ \"gouv.ht\", {} },\n        .{ \"info.ht\", {} },\n        .{ \"med.ht\", {} },\n        .{ \"net.ht\", {} },\n        .{ \"org.ht\", {} },\n        .{ \"perso.ht\", {} },\n        .{ \"pol.ht\", {} },\n        .{ \"pro.ht\", {} },\n        .{ \"rel.ht\", {} },\n        .{ \"shop.ht\", {} },\n        .{ \"hu\", {} },\n        .{ \"2000.hu\", {} },\n        .{ \"agrar.hu\", {} },\n        .{ \"bolt.hu\", {} },\n        .{ \"casino.hu\", {} },\n        .{ \"city.hu\", {} },\n        .{ \"co.hu\", {} },\n        .{ \"erotica.hu\", {} },\n        .{ \"erotika.hu\", {} },\n        .{ \"film.hu\", {} },\n        .{ \"forum.hu\", {} },\n        .{ \"games.hu\", {} },\n        .{ \"hotel.hu\", {} },\n        .{ \"info.hu\", {} },\n        .{ \"ingatlan.hu\", {} },\n        .{ \"jogasz.hu\", {} },\n        .{ \"konyvelo.hu\", {} },\n        .{ \"lakas.hu\", {} },\n        .{ \"media.hu\", {} },\n        .{ \"news.hu\", {} },\n        .{ \"org.hu\", {} },\n        .{ \"priv.hu\", {} },\n        .{ \"reklam.hu\", {} },\n        .{ \"sex.hu\", {} },\n        .{ \"shop.hu\", {} },\n        .{ \"sport.hu\", {} },\n        .{ \"suli.hu\", {} },\n        .{ \"szex.hu\", {} },\n        .{ \"tm.hu\", {} },\n        .{ \"tozsde.hu\", {} },\n        .{ \"utazas.hu\", {} },\n        .{ \"video.hu\", {} },\n        .{ \"id\", {} },\n        .{ \"ac.id\", {} },\n        .{ \"biz.id\", {} },\n        .{ \"co.id\", {} },\n        .{ \"desa.id\", {} },\n        .{ \"go.id\", {} },\n        .{ \"kop.id\", {} },\n        .{ \"mil.id\", {} },\n        .{ \"my.id\", {} },\n        .{ \"net.id\", {} },\n        .{ \"or.id\", {} },\n        .{ \"ponpes.id\", {} },\n        .{ \"sch.id\", {} },\n        .{ \"web.id\", {} },\n        .{ \"ᬩᬮᬶ.id\", {} },\n        .{ \"ie\", {} },\n        .{ \"gov.ie\", {} },\n        .{ \"il\", {} },\n        .{ \"ac.il\", {} },\n        .{ \"co.il\", {} },\n        .{ \"gov.il\", {} },\n        .{ \"idf.il\", {} },\n        .{ \"k12.il\", {} },\n        .{ \"muni.il\", {} },\n        .{ \"net.il\", {} },\n        .{ \"org.il\", {} },\n        .{ \"ישראל\", {} },\n        .{ \"אקדמיה.ישראל\", {} },\n        .{ \"ישוב.ישראל\", {} },\n        .{ \"צהל.ישראל\", {} },\n        .{ \"ממשל.ישראל\", {} },\n        .{ \"im\", {} },\n        .{ \"ac.im\", {} },\n        .{ \"co.im\", {} },\n        .{ \"ltd.co.im\", {} },\n        .{ \"plc.co.im\", {} },\n        .{ \"com.im\", {} },\n        .{ \"net.im\", {} },\n        .{ \"org.im\", {} },\n        .{ \"tt.im\", {} },\n        .{ \"tv.im\", {} },\n        .{ \"in\", {} },\n        .{ \"5g.in\", {} },\n        .{ \"6g.in\", {} },\n        .{ \"ac.in\", {} },\n        .{ \"ai.in\", {} },\n        .{ \"am.in\", {} },\n        .{ \"bank.in\", {} },\n        .{ \"bihar.in\", {} },\n        .{ \"biz.in\", {} },\n        .{ \"business.in\", {} },\n        .{ \"ca.in\", {} },\n        .{ \"cn.in\", {} },\n        .{ \"co.in\", {} },\n        .{ \"com.in\", {} },\n        .{ \"coop.in\", {} },\n        .{ \"cs.in\", {} },\n        .{ \"delhi.in\", {} },\n        .{ \"dr.in\", {} },\n        .{ \"edu.in\", {} },\n        .{ \"er.in\", {} },\n        .{ \"fin.in\", {} },\n        .{ \"firm.in\", {} },\n        .{ \"gen.in\", {} },\n        .{ \"gov.in\", {} },\n        .{ \"gujarat.in\", {} },\n        .{ \"ind.in\", {} },\n        .{ \"info.in\", {} },\n        .{ \"int.in\", {} },\n        .{ \"internet.in\", {} },\n        .{ \"io.in\", {} },\n        .{ \"me.in\", {} },\n        .{ \"mil.in\", {} },\n        .{ \"net.in\", {} },\n        .{ \"nic.in\", {} },\n        .{ \"org.in\", {} },\n        .{ \"pg.in\", {} },\n        .{ \"post.in\", {} },\n        .{ \"pro.in\", {} },\n        .{ \"res.in\", {} },\n        .{ \"travel.in\", {} },\n        .{ \"tv.in\", {} },\n        .{ \"uk.in\", {} },\n        .{ \"up.in\", {} },\n        .{ \"us.in\", {} },\n        .{ \"info\", {} },\n        .{ \"int\", {} },\n        .{ \"eu.int\", {} },\n        .{ \"io\", {} },\n        .{ \"co.io\", {} },\n        .{ \"com.io\", {} },\n        .{ \"edu.io\", {} },\n        .{ \"gov.io\", {} },\n        .{ \"mil.io\", {} },\n        .{ \"net.io\", {} },\n        .{ \"nom.io\", {} },\n        .{ \"org.io\", {} },\n        .{ \"iq\", {} },\n        .{ \"com.iq\", {} },\n        .{ \"edu.iq\", {} },\n        .{ \"gov.iq\", {} },\n        .{ \"mil.iq\", {} },\n        .{ \"net.iq\", {} },\n        .{ \"org.iq\", {} },\n        .{ \"ir\", {} },\n        .{ \"ac.ir\", {} },\n        .{ \"co.ir\", {} },\n        .{ \"gov.ir\", {} },\n        .{ \"id.ir\", {} },\n        .{ \"net.ir\", {} },\n        .{ \"org.ir\", {} },\n        .{ \"sch.ir\", {} },\n        .{ \"ایران.ir\", {} },\n        .{ \"ايران.ir\", {} },\n        .{ \"is\", {} },\n        .{ \"it\", {} },\n        .{ \"edu.it\", {} },\n        .{ \"gov.it\", {} },\n        .{ \"abr.it\", {} },\n        .{ \"abruzzo.it\", {} },\n        .{ \"aosta-valley.it\", {} },\n        .{ \"aostavalley.it\", {} },\n        .{ \"bas.it\", {} },\n        .{ \"basilicata.it\", {} },\n        .{ \"cal.it\", {} },\n        .{ \"calabria.it\", {} },\n        .{ \"cam.it\", {} },\n        .{ \"campania.it\", {} },\n        .{ \"emilia-romagna.it\", {} },\n        .{ \"emiliaromagna.it\", {} },\n        .{ \"emr.it\", {} },\n        .{ \"friuli-v-giulia.it\", {} },\n        .{ \"friuli-ve-giulia.it\", {} },\n        .{ \"friuli-vegiulia.it\", {} },\n        .{ \"friuli-venezia-giulia.it\", {} },\n        .{ \"friuli-veneziagiulia.it\", {} },\n        .{ \"friuli-vgiulia.it\", {} },\n        .{ \"friuliv-giulia.it\", {} },\n        .{ \"friulive-giulia.it\", {} },\n        .{ \"friulivegiulia.it\", {} },\n        .{ \"friulivenezia-giulia.it\", {} },\n        .{ \"friuliveneziagiulia.it\", {} },\n        .{ \"friulivgiulia.it\", {} },\n        .{ \"fvg.it\", {} },\n        .{ \"laz.it\", {} },\n        .{ \"lazio.it\", {} },\n        .{ \"lig.it\", {} },\n        .{ \"liguria.it\", {} },\n        .{ \"lom.it\", {} },\n        .{ \"lombardia.it\", {} },\n        .{ \"lombardy.it\", {} },\n        .{ \"lucania.it\", {} },\n        .{ \"mar.it\", {} },\n        .{ \"marche.it\", {} },\n        .{ \"mol.it\", {} },\n        .{ \"molise.it\", {} },\n        .{ \"piedmont.it\", {} },\n        .{ \"piemonte.it\", {} },\n        .{ \"pmn.it\", {} },\n        .{ \"pug.it\", {} },\n        .{ \"puglia.it\", {} },\n        .{ \"sar.it\", {} },\n        .{ \"sardegna.it\", {} },\n        .{ \"sardinia.it\", {} },\n        .{ \"sic.it\", {} },\n        .{ \"sicilia.it\", {} },\n        .{ \"sicily.it\", {} },\n        .{ \"taa.it\", {} },\n        .{ \"tos.it\", {} },\n        .{ \"toscana.it\", {} },\n        .{ \"trentin-sud-tirol.it\", {} },\n        .{ \"trentin-süd-tirol.it\", {} },\n        .{ \"trentin-sudtirol.it\", {} },\n        .{ \"trentin-südtirol.it\", {} },\n        .{ \"trentin-sued-tirol.it\", {} },\n        .{ \"trentin-suedtirol.it\", {} },\n        .{ \"trentino.it\", {} },\n        .{ \"trentino-a-adige.it\", {} },\n        .{ \"trentino-aadige.it\", {} },\n        .{ \"trentino-alto-adige.it\", {} },\n        .{ \"trentino-altoadige.it\", {} },\n        .{ \"trentino-s-tirol.it\", {} },\n        .{ \"trentino-stirol.it\", {} },\n        .{ \"trentino-sud-tirol.it\", {} },\n        .{ \"trentino-süd-tirol.it\", {} },\n        .{ \"trentino-sudtirol.it\", {} },\n        .{ \"trentino-südtirol.it\", {} },\n        .{ \"trentino-sued-tirol.it\", {} },\n        .{ \"trentino-suedtirol.it\", {} },\n        .{ \"trentinoa-adige.it\", {} },\n        .{ \"trentinoaadige.it\", {} },\n        .{ \"trentinoalto-adige.it\", {} },\n        .{ \"trentinoaltoadige.it\", {} },\n        .{ \"trentinos-tirol.it\", {} },\n        .{ \"trentinostirol.it\", {} },\n        .{ \"trentinosud-tirol.it\", {} },\n        .{ \"trentinosüd-tirol.it\", {} },\n        .{ \"trentinosudtirol.it\", {} },\n        .{ \"trentinosüdtirol.it\", {} },\n        .{ \"trentinosued-tirol.it\", {} },\n        .{ \"trentinosuedtirol.it\", {} },\n        .{ \"trentinsud-tirol.it\", {} },\n        .{ \"trentinsüd-tirol.it\", {} },\n        .{ \"trentinsudtirol.it\", {} },\n        .{ \"trentinsüdtirol.it\", {} },\n        .{ \"trentinsued-tirol.it\", {} },\n        .{ \"trentinsuedtirol.it\", {} },\n        .{ \"tuscany.it\", {} },\n        .{ \"umb.it\", {} },\n        .{ \"umbria.it\", {} },\n        .{ \"val-d-aosta.it\", {} },\n        .{ \"val-daosta.it\", {} },\n        .{ \"vald-aosta.it\", {} },\n        .{ \"valdaosta.it\", {} },\n        .{ \"valle-aosta.it\", {} },\n        .{ \"valle-d-aosta.it\", {} },\n        .{ \"valle-daosta.it\", {} },\n        .{ \"valleaosta.it\", {} },\n        .{ \"valled-aosta.it\", {} },\n        .{ \"valledaosta.it\", {} },\n        .{ \"vallee-aoste.it\", {} },\n        .{ \"vallée-aoste.it\", {} },\n        .{ \"vallee-d-aoste.it\", {} },\n        .{ \"vallée-d-aoste.it\", {} },\n        .{ \"valleeaoste.it\", {} },\n        .{ \"valléeaoste.it\", {} },\n        .{ \"valleedaoste.it\", {} },\n        .{ \"valléedaoste.it\", {} },\n        .{ \"vao.it\", {} },\n        .{ \"vda.it\", {} },\n        .{ \"ven.it\", {} },\n        .{ \"veneto.it\", {} },\n        .{ \"ag.it\", {} },\n        .{ \"agrigento.it\", {} },\n        .{ \"al.it\", {} },\n        .{ \"alessandria.it\", {} },\n        .{ \"alto-adige.it\", {} },\n        .{ \"altoadige.it\", {} },\n        .{ \"an.it\", {} },\n        .{ \"ancona.it\", {} },\n        .{ \"andria-barletta-trani.it\", {} },\n        .{ \"andria-trani-barletta.it\", {} },\n        .{ \"andriabarlettatrani.it\", {} },\n        .{ \"andriatranibarletta.it\", {} },\n        .{ \"ao.it\", {} },\n        .{ \"aosta.it\", {} },\n        .{ \"aoste.it\", {} },\n        .{ \"ap.it\", {} },\n        .{ \"aq.it\", {} },\n        .{ \"aquila.it\", {} },\n        .{ \"ar.it\", {} },\n        .{ \"arezzo.it\", {} },\n        .{ \"ascoli-piceno.it\", {} },\n        .{ \"ascolipiceno.it\", {} },\n        .{ \"asti.it\", {} },\n        .{ \"at.it\", {} },\n        .{ \"av.it\", {} },\n        .{ \"avellino.it\", {} },\n        .{ \"ba.it\", {} },\n        .{ \"balsan.it\", {} },\n        .{ \"balsan-sudtirol.it\", {} },\n        .{ \"balsan-südtirol.it\", {} },\n        .{ \"balsan-suedtirol.it\", {} },\n        .{ \"bari.it\", {} },\n        .{ \"barletta-trani-andria.it\", {} },\n        .{ \"barlettatraniandria.it\", {} },\n        .{ \"belluno.it\", {} },\n        .{ \"benevento.it\", {} },\n        .{ \"bergamo.it\", {} },\n        .{ \"bg.it\", {} },\n        .{ \"bi.it\", {} },\n        .{ \"biella.it\", {} },\n        .{ \"bl.it\", {} },\n        .{ \"bn.it\", {} },\n        .{ \"bo.it\", {} },\n        .{ \"bologna.it\", {} },\n        .{ \"bolzano.it\", {} },\n        .{ \"bolzano-altoadige.it\", {} },\n        .{ \"bozen.it\", {} },\n        .{ \"bozen-sudtirol.it\", {} },\n        .{ \"bozen-südtirol.it\", {} },\n        .{ \"bozen-suedtirol.it\", {} },\n        .{ \"br.it\", {} },\n        .{ \"brescia.it\", {} },\n        .{ \"brindisi.it\", {} },\n        .{ \"bs.it\", {} },\n        .{ \"bt.it\", {} },\n        .{ \"bulsan.it\", {} },\n        .{ \"bulsan-sudtirol.it\", {} },\n        .{ \"bulsan-südtirol.it\", {} },\n        .{ \"bulsan-suedtirol.it\", {} },\n        .{ \"bz.it\", {} },\n        .{ \"ca.it\", {} },\n        .{ \"cagliari.it\", {} },\n        .{ \"caltanissetta.it\", {} },\n        .{ \"campidano-medio.it\", {} },\n        .{ \"campidanomedio.it\", {} },\n        .{ \"campobasso.it\", {} },\n        .{ \"carbonia-iglesias.it\", {} },\n        .{ \"carboniaiglesias.it\", {} },\n        .{ \"carrara-massa.it\", {} },\n        .{ \"carraramassa.it\", {} },\n        .{ \"caserta.it\", {} },\n        .{ \"catania.it\", {} },\n        .{ \"catanzaro.it\", {} },\n        .{ \"cb.it\", {} },\n        .{ \"ce.it\", {} },\n        .{ \"cesena-forli.it\", {} },\n        .{ \"cesena-forlì.it\", {} },\n        .{ \"cesenaforli.it\", {} },\n        .{ \"cesenaforlì.it\", {} },\n        .{ \"ch.it\", {} },\n        .{ \"chieti.it\", {} },\n        .{ \"ci.it\", {} },\n        .{ \"cl.it\", {} },\n        .{ \"cn.it\", {} },\n        .{ \"co.it\", {} },\n        .{ \"como.it\", {} },\n        .{ \"cosenza.it\", {} },\n        .{ \"cr.it\", {} },\n        .{ \"cremona.it\", {} },\n        .{ \"crotone.it\", {} },\n        .{ \"cs.it\", {} },\n        .{ \"ct.it\", {} },\n        .{ \"cuneo.it\", {} },\n        .{ \"cz.it\", {} },\n        .{ \"dell-ogliastra.it\", {} },\n        .{ \"dellogliastra.it\", {} },\n        .{ \"en.it\", {} },\n        .{ \"enna.it\", {} },\n        .{ \"fc.it\", {} },\n        .{ \"fe.it\", {} },\n        .{ \"fermo.it\", {} },\n        .{ \"ferrara.it\", {} },\n        .{ \"fg.it\", {} },\n        .{ \"fi.it\", {} },\n        .{ \"firenze.it\", {} },\n        .{ \"florence.it\", {} },\n        .{ \"fm.it\", {} },\n        .{ \"foggia.it\", {} },\n        .{ \"forli-cesena.it\", {} },\n        .{ \"forlì-cesena.it\", {} },\n        .{ \"forlicesena.it\", {} },\n        .{ \"forlìcesena.it\", {} },\n        .{ \"fr.it\", {} },\n        .{ \"frosinone.it\", {} },\n        .{ \"ge.it\", {} },\n        .{ \"genoa.it\", {} },\n        .{ \"genova.it\", {} },\n        .{ \"go.it\", {} },\n        .{ \"gorizia.it\", {} },\n        .{ \"gr.it\", {} },\n        .{ \"grosseto.it\", {} },\n        .{ \"iglesias-carbonia.it\", {} },\n        .{ \"iglesiascarbonia.it\", {} },\n        .{ \"im.it\", {} },\n        .{ \"imperia.it\", {} },\n        .{ \"is.it\", {} },\n        .{ \"isernia.it\", {} },\n        .{ \"kr.it\", {} },\n        .{ \"la-spezia.it\", {} },\n        .{ \"laquila.it\", {} },\n        .{ \"laspezia.it\", {} },\n        .{ \"latina.it\", {} },\n        .{ \"lc.it\", {} },\n        .{ \"le.it\", {} },\n        .{ \"lecce.it\", {} },\n        .{ \"lecco.it\", {} },\n        .{ \"li.it\", {} },\n        .{ \"livorno.it\", {} },\n        .{ \"lo.it\", {} },\n        .{ \"lodi.it\", {} },\n        .{ \"lt.it\", {} },\n        .{ \"lu.it\", {} },\n        .{ \"lucca.it\", {} },\n        .{ \"macerata.it\", {} },\n        .{ \"mantova.it\", {} },\n        .{ \"massa-carrara.it\", {} },\n        .{ \"massacarrara.it\", {} },\n        .{ \"matera.it\", {} },\n        .{ \"mb.it\", {} },\n        .{ \"mc.it\", {} },\n        .{ \"me.it\", {} },\n        .{ \"medio-campidano.it\", {} },\n        .{ \"mediocampidano.it\", {} },\n        .{ \"messina.it\", {} },\n        .{ \"mi.it\", {} },\n        .{ \"milan.it\", {} },\n        .{ \"milano.it\", {} },\n        .{ \"mn.it\", {} },\n        .{ \"mo.it\", {} },\n        .{ \"modena.it\", {} },\n        .{ \"monza.it\", {} },\n        .{ \"monza-brianza.it\", {} },\n        .{ \"monza-e-della-brianza.it\", {} },\n        .{ \"monzabrianza.it\", {} },\n        .{ \"monzaebrianza.it\", {} },\n        .{ \"monzaedellabrianza.it\", {} },\n        .{ \"ms.it\", {} },\n        .{ \"mt.it\", {} },\n        .{ \"na.it\", {} },\n        .{ \"naples.it\", {} },\n        .{ \"napoli.it\", {} },\n        .{ \"no.it\", {} },\n        .{ \"novara.it\", {} },\n        .{ \"nu.it\", {} },\n        .{ \"nuoro.it\", {} },\n        .{ \"og.it\", {} },\n        .{ \"ogliastra.it\", {} },\n        .{ \"olbia-tempio.it\", {} },\n        .{ \"olbiatempio.it\", {} },\n        .{ \"or.it\", {} },\n        .{ \"oristano.it\", {} },\n        .{ \"ot.it\", {} },\n        .{ \"pa.it\", {} },\n        .{ \"padova.it\", {} },\n        .{ \"padua.it\", {} },\n        .{ \"palermo.it\", {} },\n        .{ \"parma.it\", {} },\n        .{ \"pavia.it\", {} },\n        .{ \"pc.it\", {} },\n        .{ \"pd.it\", {} },\n        .{ \"pe.it\", {} },\n        .{ \"perugia.it\", {} },\n        .{ \"pesaro-urbino.it\", {} },\n        .{ \"pesarourbino.it\", {} },\n        .{ \"pescara.it\", {} },\n        .{ \"pg.it\", {} },\n        .{ \"pi.it\", {} },\n        .{ \"piacenza.it\", {} },\n        .{ \"pisa.it\", {} },\n        .{ \"pistoia.it\", {} },\n        .{ \"pn.it\", {} },\n        .{ \"po.it\", {} },\n        .{ \"pordenone.it\", {} },\n        .{ \"potenza.it\", {} },\n        .{ \"pr.it\", {} },\n        .{ \"prato.it\", {} },\n        .{ \"pt.it\", {} },\n        .{ \"pu.it\", {} },\n        .{ \"pv.it\", {} },\n        .{ \"pz.it\", {} },\n        .{ \"ra.it\", {} },\n        .{ \"ragusa.it\", {} },\n        .{ \"ravenna.it\", {} },\n        .{ \"rc.it\", {} },\n        .{ \"re.it\", {} },\n        .{ \"reggio-calabria.it\", {} },\n        .{ \"reggio-emilia.it\", {} },\n        .{ \"reggiocalabria.it\", {} },\n        .{ \"reggioemilia.it\", {} },\n        .{ \"rg.it\", {} },\n        .{ \"ri.it\", {} },\n        .{ \"rieti.it\", {} },\n        .{ \"rimini.it\", {} },\n        .{ \"rm.it\", {} },\n        .{ \"rn.it\", {} },\n        .{ \"ro.it\", {} },\n        .{ \"roma.it\", {} },\n        .{ \"rome.it\", {} },\n        .{ \"rovigo.it\", {} },\n        .{ \"sa.it\", {} },\n        .{ \"salerno.it\", {} },\n        .{ \"sassari.it\", {} },\n        .{ \"savona.it\", {} },\n        .{ \"si.it\", {} },\n        .{ \"siena.it\", {} },\n        .{ \"siracusa.it\", {} },\n        .{ \"so.it\", {} },\n        .{ \"sondrio.it\", {} },\n        .{ \"sp.it\", {} },\n        .{ \"sr.it\", {} },\n        .{ \"ss.it\", {} },\n        .{ \"südtirol.it\", {} },\n        .{ \"suedtirol.it\", {} },\n        .{ \"sv.it\", {} },\n        .{ \"ta.it\", {} },\n        .{ \"taranto.it\", {} },\n        .{ \"te.it\", {} },\n        .{ \"tempio-olbia.it\", {} },\n        .{ \"tempioolbia.it\", {} },\n        .{ \"teramo.it\", {} },\n        .{ \"terni.it\", {} },\n        .{ \"tn.it\", {} },\n        .{ \"to.it\", {} },\n        .{ \"torino.it\", {} },\n        .{ \"tp.it\", {} },\n        .{ \"tr.it\", {} },\n        .{ \"trani-andria-barletta.it\", {} },\n        .{ \"trani-barletta-andria.it\", {} },\n        .{ \"traniandriabarletta.it\", {} },\n        .{ \"tranibarlettaandria.it\", {} },\n        .{ \"trapani.it\", {} },\n        .{ \"trento.it\", {} },\n        .{ \"treviso.it\", {} },\n        .{ \"trieste.it\", {} },\n        .{ \"ts.it\", {} },\n        .{ \"turin.it\", {} },\n        .{ \"tv.it\", {} },\n        .{ \"ud.it\", {} },\n        .{ \"udine.it\", {} },\n        .{ \"urbino-pesaro.it\", {} },\n        .{ \"urbinopesaro.it\", {} },\n        .{ \"va.it\", {} },\n        .{ \"varese.it\", {} },\n        .{ \"vb.it\", {} },\n        .{ \"vc.it\", {} },\n        .{ \"ve.it\", {} },\n        .{ \"venezia.it\", {} },\n        .{ \"venice.it\", {} },\n        .{ \"verbania.it\", {} },\n        .{ \"vercelli.it\", {} },\n        .{ \"verona.it\", {} },\n        .{ \"vi.it\", {} },\n        .{ \"vibo-valentia.it\", {} },\n        .{ \"vibovalentia.it\", {} },\n        .{ \"vicenza.it\", {} },\n        .{ \"viterbo.it\", {} },\n        .{ \"vr.it\", {} },\n        .{ \"vs.it\", {} },\n        .{ \"vt.it\", {} },\n        .{ \"vv.it\", {} },\n        .{ \"je\", {} },\n        .{ \"co.je\", {} },\n        .{ \"net.je\", {} },\n        .{ \"org.je\", {} },\n        .{ \"*.jm\", {} },\n        .{ \"jo\", {} },\n        .{ \"agri.jo\", {} },\n        .{ \"ai.jo\", {} },\n        .{ \"com.jo\", {} },\n        .{ \"edu.jo\", {} },\n        .{ \"eng.jo\", {} },\n        .{ \"fm.jo\", {} },\n        .{ \"gov.jo\", {} },\n        .{ \"mil.jo\", {} },\n        .{ \"net.jo\", {} },\n        .{ \"org.jo\", {} },\n        .{ \"per.jo\", {} },\n        .{ \"phd.jo\", {} },\n        .{ \"sch.jo\", {} },\n        .{ \"tv.jo\", {} },\n        .{ \"jobs\", {} },\n        .{ \"jp\", {} },\n        .{ \"ac.jp\", {} },\n        .{ \"ad.jp\", {} },\n        .{ \"co.jp\", {} },\n        .{ \"ed.jp\", {} },\n        .{ \"go.jp\", {} },\n        .{ \"gr.jp\", {} },\n        .{ \"lg.jp\", {} },\n        .{ \"ne.jp\", {} },\n        .{ \"or.jp\", {} },\n        .{ \"aichi.jp\", {} },\n        .{ \"akita.jp\", {} },\n        .{ \"aomori.jp\", {} },\n        .{ \"chiba.jp\", {} },\n        .{ \"ehime.jp\", {} },\n        .{ \"fukui.jp\", {} },\n        .{ \"fukuoka.jp\", {} },\n        .{ \"fukushima.jp\", {} },\n        .{ \"gifu.jp\", {} },\n        .{ \"gunma.jp\", {} },\n        .{ \"hiroshima.jp\", {} },\n        .{ \"hokkaido.jp\", {} },\n        .{ \"hyogo.jp\", {} },\n        .{ \"ibaraki.jp\", {} },\n        .{ \"ishikawa.jp\", {} },\n        .{ \"iwate.jp\", {} },\n        .{ \"kagawa.jp\", {} },\n        .{ \"kagoshima.jp\", {} },\n        .{ \"kanagawa.jp\", {} },\n        .{ \"kochi.jp\", {} },\n        .{ \"kumamoto.jp\", {} },\n        .{ \"kyoto.jp\", {} },\n        .{ \"mie.jp\", {} },\n        .{ \"miyagi.jp\", {} },\n        .{ \"miyazaki.jp\", {} },\n        .{ \"nagano.jp\", {} },\n        .{ \"nagasaki.jp\", {} },\n        .{ \"nara.jp\", {} },\n        .{ \"niigata.jp\", {} },\n        .{ \"oita.jp\", {} },\n        .{ \"okayama.jp\", {} },\n        .{ \"okinawa.jp\", {} },\n        .{ \"osaka.jp\", {} },\n        .{ \"saga.jp\", {} },\n        .{ \"saitama.jp\", {} },\n        .{ \"shiga.jp\", {} },\n        .{ \"shimane.jp\", {} },\n        .{ \"shizuoka.jp\", {} },\n        .{ \"tochigi.jp\", {} },\n        .{ \"tokushima.jp\", {} },\n        .{ \"tokyo.jp\", {} },\n        .{ \"tottori.jp\", {} },\n        .{ \"toyama.jp\", {} },\n        .{ \"wakayama.jp\", {} },\n        .{ \"yamagata.jp\", {} },\n        .{ \"yamaguchi.jp\", {} },\n        .{ \"yamanashi.jp\", {} },\n        .{ \"三重.jp\", {} },\n        .{ \"京都.jp\", {} },\n        .{ \"佐賀.jp\", {} },\n        .{ \"兵庫.jp\", {} },\n        .{ \"北海道.jp\", {} },\n        .{ \"千葉.jp\", {} },\n        .{ \"和歌山.jp\", {} },\n        .{ \"埼玉.jp\", {} },\n        .{ \"大分.jp\", {} },\n        .{ \"大阪.jp\", {} },\n        .{ \"奈良.jp\", {} },\n        .{ \"宮城.jp\", {} },\n        .{ \"宮崎.jp\", {} },\n        .{ \"富山.jp\", {} },\n        .{ \"山口.jp\", {} },\n        .{ \"山形.jp\", {} },\n        .{ \"山梨.jp\", {} },\n        .{ \"岐阜.jp\", {} },\n        .{ \"岡山.jp\", {} },\n        .{ \"岩手.jp\", {} },\n        .{ \"島根.jp\", {} },\n        .{ \"広島.jp\", {} },\n        .{ \"徳島.jp\", {} },\n        .{ \"愛媛.jp\", {} },\n        .{ \"愛知.jp\", {} },\n        .{ \"新潟.jp\", {} },\n        .{ \"東京.jp\", {} },\n        .{ \"栃木.jp\", {} },\n        .{ \"沖縄.jp\", {} },\n        .{ \"滋賀.jp\", {} },\n        .{ \"熊本.jp\", {} },\n        .{ \"石川.jp\", {} },\n        .{ \"神奈川.jp\", {} },\n        .{ \"福井.jp\", {} },\n        .{ \"福岡.jp\", {} },\n        .{ \"福島.jp\", {} },\n        .{ \"秋田.jp\", {} },\n        .{ \"群馬.jp\", {} },\n        .{ \"茨城.jp\", {} },\n        .{ \"長崎.jp\", {} },\n        .{ \"長野.jp\", {} },\n        .{ \"青森.jp\", {} },\n        .{ \"静岡.jp\", {} },\n        .{ \"香川.jp\", {} },\n        .{ \"高知.jp\", {} },\n        .{ \"鳥取.jp\", {} },\n        .{ \"鹿児島.jp\", {} },\n        .{ \"*.kawasaki.jp\", {} },\n        .{ \"!city.kawasaki.jp\", {} },\n        .{ \"*.kitakyushu.jp\", {} },\n        .{ \"!city.kitakyushu.jp\", {} },\n        .{ \"*.kobe.jp\", {} },\n        .{ \"!city.kobe.jp\", {} },\n        .{ \"*.nagoya.jp\", {} },\n        .{ \"!city.nagoya.jp\", {} },\n        .{ \"*.sapporo.jp\", {} },\n        .{ \"!city.sapporo.jp\", {} },\n        .{ \"*.sendai.jp\", {} },\n        .{ \"!city.sendai.jp\", {} },\n        .{ \"*.yokohama.jp\", {} },\n        .{ \"!city.yokohama.jp\", {} },\n        .{ \"aisai.aichi.jp\", {} },\n        .{ \"ama.aichi.jp\", {} },\n        .{ \"anjo.aichi.jp\", {} },\n        .{ \"asuke.aichi.jp\", {} },\n        .{ \"chiryu.aichi.jp\", {} },\n        .{ \"chita.aichi.jp\", {} },\n        .{ \"fuso.aichi.jp\", {} },\n        .{ \"gamagori.aichi.jp\", {} },\n        .{ \"handa.aichi.jp\", {} },\n        .{ \"hazu.aichi.jp\", {} },\n        .{ \"hekinan.aichi.jp\", {} },\n        .{ \"higashiura.aichi.jp\", {} },\n        .{ \"ichinomiya.aichi.jp\", {} },\n        .{ \"inazawa.aichi.jp\", {} },\n        .{ \"inuyama.aichi.jp\", {} },\n        .{ \"isshiki.aichi.jp\", {} },\n        .{ \"iwakura.aichi.jp\", {} },\n        .{ \"kanie.aichi.jp\", {} },\n        .{ \"kariya.aichi.jp\", {} },\n        .{ \"kasugai.aichi.jp\", {} },\n        .{ \"kira.aichi.jp\", {} },\n        .{ \"kiyosu.aichi.jp\", {} },\n        .{ \"komaki.aichi.jp\", {} },\n        .{ \"konan.aichi.jp\", {} },\n        .{ \"kota.aichi.jp\", {} },\n        .{ \"mihama.aichi.jp\", {} },\n        .{ \"miyoshi.aichi.jp\", {} },\n        .{ \"nishio.aichi.jp\", {} },\n        .{ \"nisshin.aichi.jp\", {} },\n        .{ \"obu.aichi.jp\", {} },\n        .{ \"oguchi.aichi.jp\", {} },\n        .{ \"oharu.aichi.jp\", {} },\n        .{ \"okazaki.aichi.jp\", {} },\n        .{ \"owariasahi.aichi.jp\", {} },\n        .{ \"seto.aichi.jp\", {} },\n        .{ \"shikatsu.aichi.jp\", {} },\n        .{ \"shinshiro.aichi.jp\", {} },\n        .{ \"shitara.aichi.jp\", {} },\n        .{ \"tahara.aichi.jp\", {} },\n        .{ \"takahama.aichi.jp\", {} },\n        .{ \"tobishima.aichi.jp\", {} },\n        .{ \"toei.aichi.jp\", {} },\n        .{ \"togo.aichi.jp\", {} },\n        .{ \"tokai.aichi.jp\", {} },\n        .{ \"tokoname.aichi.jp\", {} },\n        .{ \"toyoake.aichi.jp\", {} },\n        .{ \"toyohashi.aichi.jp\", {} },\n        .{ \"toyokawa.aichi.jp\", {} },\n        .{ \"toyone.aichi.jp\", {} },\n        .{ \"toyota.aichi.jp\", {} },\n        .{ \"tsushima.aichi.jp\", {} },\n        .{ \"yatomi.aichi.jp\", {} },\n        .{ \"akita.akita.jp\", {} },\n        .{ \"daisen.akita.jp\", {} },\n        .{ \"fujisato.akita.jp\", {} },\n        .{ \"gojome.akita.jp\", {} },\n        .{ \"hachirogata.akita.jp\", {} },\n        .{ \"happou.akita.jp\", {} },\n        .{ \"higashinaruse.akita.jp\", {} },\n        .{ \"honjo.akita.jp\", {} },\n        .{ \"honjyo.akita.jp\", {} },\n        .{ \"ikawa.akita.jp\", {} },\n        .{ \"kamikoani.akita.jp\", {} },\n        .{ \"kamioka.akita.jp\", {} },\n        .{ \"katagami.akita.jp\", {} },\n        .{ \"kazuno.akita.jp\", {} },\n        .{ \"kitaakita.akita.jp\", {} },\n        .{ \"kosaka.akita.jp\", {} },\n        .{ \"kyowa.akita.jp\", {} },\n        .{ \"misato.akita.jp\", {} },\n        .{ \"mitane.akita.jp\", {} },\n        .{ \"moriyoshi.akita.jp\", {} },\n        .{ \"nikaho.akita.jp\", {} },\n        .{ \"noshiro.akita.jp\", {} },\n        .{ \"odate.akita.jp\", {} },\n        .{ \"oga.akita.jp\", {} },\n        .{ \"ogata.akita.jp\", {} },\n        .{ \"semboku.akita.jp\", {} },\n        .{ \"yokote.akita.jp\", {} },\n        .{ \"yurihonjo.akita.jp\", {} },\n        .{ \"aomori.aomori.jp\", {} },\n        .{ \"gonohe.aomori.jp\", {} },\n        .{ \"hachinohe.aomori.jp\", {} },\n        .{ \"hashikami.aomori.jp\", {} },\n        .{ \"hiranai.aomori.jp\", {} },\n        .{ \"hirosaki.aomori.jp\", {} },\n        .{ \"itayanagi.aomori.jp\", {} },\n        .{ \"kuroishi.aomori.jp\", {} },\n        .{ \"misawa.aomori.jp\", {} },\n        .{ \"mutsu.aomori.jp\", {} },\n        .{ \"nakadomari.aomori.jp\", {} },\n        .{ \"noheji.aomori.jp\", {} },\n        .{ \"oirase.aomori.jp\", {} },\n        .{ \"owani.aomori.jp\", {} },\n        .{ \"rokunohe.aomori.jp\", {} },\n        .{ \"sannohe.aomori.jp\", {} },\n        .{ \"shichinohe.aomori.jp\", {} },\n        .{ \"shingo.aomori.jp\", {} },\n        .{ \"takko.aomori.jp\", {} },\n        .{ \"towada.aomori.jp\", {} },\n        .{ \"tsugaru.aomori.jp\", {} },\n        .{ \"tsuruta.aomori.jp\", {} },\n        .{ \"abiko.chiba.jp\", {} },\n        .{ \"asahi.chiba.jp\", {} },\n        .{ \"chonan.chiba.jp\", {} },\n        .{ \"chosei.chiba.jp\", {} },\n        .{ \"choshi.chiba.jp\", {} },\n        .{ \"chuo.chiba.jp\", {} },\n        .{ \"funabashi.chiba.jp\", {} },\n        .{ \"futtsu.chiba.jp\", {} },\n        .{ \"hanamigawa.chiba.jp\", {} },\n        .{ \"ichihara.chiba.jp\", {} },\n        .{ \"ichikawa.chiba.jp\", {} },\n        .{ \"ichinomiya.chiba.jp\", {} },\n        .{ \"inzai.chiba.jp\", {} },\n        .{ \"isumi.chiba.jp\", {} },\n        .{ \"kamagaya.chiba.jp\", {} },\n        .{ \"kamogawa.chiba.jp\", {} },\n        .{ \"kashiwa.chiba.jp\", {} },\n        .{ \"katori.chiba.jp\", {} },\n        .{ \"katsuura.chiba.jp\", {} },\n        .{ \"kimitsu.chiba.jp\", {} },\n        .{ \"kisarazu.chiba.jp\", {} },\n        .{ \"kozaki.chiba.jp\", {} },\n        .{ \"kujukuri.chiba.jp\", {} },\n        .{ \"kyonan.chiba.jp\", {} },\n        .{ \"matsudo.chiba.jp\", {} },\n        .{ \"midori.chiba.jp\", {} },\n        .{ \"mihama.chiba.jp\", {} },\n        .{ \"minamiboso.chiba.jp\", {} },\n        .{ \"mobara.chiba.jp\", {} },\n        .{ \"mutsuzawa.chiba.jp\", {} },\n        .{ \"nagara.chiba.jp\", {} },\n        .{ \"nagareyama.chiba.jp\", {} },\n        .{ \"narashino.chiba.jp\", {} },\n        .{ \"narita.chiba.jp\", {} },\n        .{ \"noda.chiba.jp\", {} },\n        .{ \"oamishirasato.chiba.jp\", {} },\n        .{ \"omigawa.chiba.jp\", {} },\n        .{ \"onjuku.chiba.jp\", {} },\n        .{ \"otaki.chiba.jp\", {} },\n        .{ \"sakae.chiba.jp\", {} },\n        .{ \"sakura.chiba.jp\", {} },\n        .{ \"shimofusa.chiba.jp\", {} },\n        .{ \"shirako.chiba.jp\", {} },\n        .{ \"shiroi.chiba.jp\", {} },\n        .{ \"shisui.chiba.jp\", {} },\n        .{ \"sodegaura.chiba.jp\", {} },\n        .{ \"sosa.chiba.jp\", {} },\n        .{ \"tako.chiba.jp\", {} },\n        .{ \"tateyama.chiba.jp\", {} },\n        .{ \"togane.chiba.jp\", {} },\n        .{ \"tohnosho.chiba.jp\", {} },\n        .{ \"tomisato.chiba.jp\", {} },\n        .{ \"urayasu.chiba.jp\", {} },\n        .{ \"yachimata.chiba.jp\", {} },\n        .{ \"yachiyo.chiba.jp\", {} },\n        .{ \"yokaichiba.chiba.jp\", {} },\n        .{ \"yokoshibahikari.chiba.jp\", {} },\n        .{ \"yotsukaido.chiba.jp\", {} },\n        .{ \"ainan.ehime.jp\", {} },\n        .{ \"honai.ehime.jp\", {} },\n        .{ \"ikata.ehime.jp\", {} },\n        .{ \"imabari.ehime.jp\", {} },\n        .{ \"iyo.ehime.jp\", {} },\n        .{ \"kamijima.ehime.jp\", {} },\n        .{ \"kihoku.ehime.jp\", {} },\n        .{ \"kumakogen.ehime.jp\", {} },\n        .{ \"masaki.ehime.jp\", {} },\n        .{ \"matsuno.ehime.jp\", {} },\n        .{ \"matsuyama.ehime.jp\", {} },\n        .{ \"namikata.ehime.jp\", {} },\n        .{ \"niihama.ehime.jp\", {} },\n        .{ \"ozu.ehime.jp\", {} },\n        .{ \"saijo.ehime.jp\", {} },\n        .{ \"seiyo.ehime.jp\", {} },\n        .{ \"shikokuchuo.ehime.jp\", {} },\n        .{ \"tobe.ehime.jp\", {} },\n        .{ \"toon.ehime.jp\", {} },\n        .{ \"uchiko.ehime.jp\", {} },\n        .{ \"uwajima.ehime.jp\", {} },\n        .{ \"yawatahama.ehime.jp\", {} },\n        .{ \"echizen.fukui.jp\", {} },\n        .{ \"eiheiji.fukui.jp\", {} },\n        .{ \"fukui.fukui.jp\", {} },\n        .{ \"ikeda.fukui.jp\", {} },\n        .{ \"katsuyama.fukui.jp\", {} },\n        .{ \"mihama.fukui.jp\", {} },\n        .{ \"minamiechizen.fukui.jp\", {} },\n        .{ \"obama.fukui.jp\", {} },\n        .{ \"ohi.fukui.jp\", {} },\n        .{ \"ono.fukui.jp\", {} },\n        .{ \"sabae.fukui.jp\", {} },\n        .{ \"sakai.fukui.jp\", {} },\n        .{ \"takahama.fukui.jp\", {} },\n        .{ \"tsuruga.fukui.jp\", {} },\n        .{ \"wakasa.fukui.jp\", {} },\n        .{ \"ashiya.fukuoka.jp\", {} },\n        .{ \"buzen.fukuoka.jp\", {} },\n        .{ \"chikugo.fukuoka.jp\", {} },\n        .{ \"chikuho.fukuoka.jp\", {} },\n        .{ \"chikujo.fukuoka.jp\", {} },\n        .{ \"chikushino.fukuoka.jp\", {} },\n        .{ \"chikuzen.fukuoka.jp\", {} },\n        .{ \"chuo.fukuoka.jp\", {} },\n        .{ \"dazaifu.fukuoka.jp\", {} },\n        .{ \"fukuchi.fukuoka.jp\", {} },\n        .{ \"hakata.fukuoka.jp\", {} },\n        .{ \"higashi.fukuoka.jp\", {} },\n        .{ \"hirokawa.fukuoka.jp\", {} },\n        .{ \"hisayama.fukuoka.jp\", {} },\n        .{ \"iizuka.fukuoka.jp\", {} },\n        .{ \"inatsuki.fukuoka.jp\", {} },\n        .{ \"kaho.fukuoka.jp\", {} },\n        .{ \"kasuga.fukuoka.jp\", {} },\n        .{ \"kasuya.fukuoka.jp\", {} },\n        .{ \"kawara.fukuoka.jp\", {} },\n        .{ \"keisen.fukuoka.jp\", {} },\n        .{ \"koga.fukuoka.jp\", {} },\n        .{ \"kurate.fukuoka.jp\", {} },\n        .{ \"kurogi.fukuoka.jp\", {} },\n        .{ \"kurume.fukuoka.jp\", {} },\n        .{ \"minami.fukuoka.jp\", {} },\n        .{ \"miyako.fukuoka.jp\", {} },\n        .{ \"miyama.fukuoka.jp\", {} },\n        .{ \"miyawaka.fukuoka.jp\", {} },\n        .{ \"mizumaki.fukuoka.jp\", {} },\n        .{ \"munakata.fukuoka.jp\", {} },\n        .{ \"nakagawa.fukuoka.jp\", {} },\n        .{ \"nakama.fukuoka.jp\", {} },\n        .{ \"nishi.fukuoka.jp\", {} },\n        .{ \"nogata.fukuoka.jp\", {} },\n        .{ \"ogori.fukuoka.jp\", {} },\n        .{ \"okagaki.fukuoka.jp\", {} },\n        .{ \"okawa.fukuoka.jp\", {} },\n        .{ \"oki.fukuoka.jp\", {} },\n        .{ \"omuta.fukuoka.jp\", {} },\n        .{ \"onga.fukuoka.jp\", {} },\n        .{ \"onojo.fukuoka.jp\", {} },\n        .{ \"oto.fukuoka.jp\", {} },\n        .{ \"saigawa.fukuoka.jp\", {} },\n        .{ \"sasaguri.fukuoka.jp\", {} },\n        .{ \"shingu.fukuoka.jp\", {} },\n        .{ \"shinyoshitomi.fukuoka.jp\", {} },\n        .{ \"shonai.fukuoka.jp\", {} },\n        .{ \"soeda.fukuoka.jp\", {} },\n        .{ \"sue.fukuoka.jp\", {} },\n        .{ \"tachiarai.fukuoka.jp\", {} },\n        .{ \"tagawa.fukuoka.jp\", {} },\n        .{ \"takata.fukuoka.jp\", {} },\n        .{ \"toho.fukuoka.jp\", {} },\n        .{ \"toyotsu.fukuoka.jp\", {} },\n        .{ \"tsuiki.fukuoka.jp\", {} },\n        .{ \"ukiha.fukuoka.jp\", {} },\n        .{ \"umi.fukuoka.jp\", {} },\n        .{ \"usui.fukuoka.jp\", {} },\n        .{ \"yamada.fukuoka.jp\", {} },\n        .{ \"yame.fukuoka.jp\", {} },\n        .{ \"yanagawa.fukuoka.jp\", {} },\n        .{ \"yukuhashi.fukuoka.jp\", {} },\n        .{ \"aizubange.fukushima.jp\", {} },\n        .{ \"aizumisato.fukushima.jp\", {} },\n        .{ \"aizuwakamatsu.fukushima.jp\", {} },\n        .{ \"asakawa.fukushima.jp\", {} },\n        .{ \"bandai.fukushima.jp\", {} },\n        .{ \"date.fukushima.jp\", {} },\n        .{ \"fukushima.fukushima.jp\", {} },\n        .{ \"furudono.fukushima.jp\", {} },\n        .{ \"futaba.fukushima.jp\", {} },\n        .{ \"hanawa.fukushima.jp\", {} },\n        .{ \"higashi.fukushima.jp\", {} },\n        .{ \"hirata.fukushima.jp\", {} },\n        .{ \"hirono.fukushima.jp\", {} },\n        .{ \"iitate.fukushima.jp\", {} },\n        .{ \"inawashiro.fukushima.jp\", {} },\n        .{ \"ishikawa.fukushima.jp\", {} },\n        .{ \"iwaki.fukushima.jp\", {} },\n        .{ \"izumizaki.fukushima.jp\", {} },\n        .{ \"kagamiishi.fukushima.jp\", {} },\n        .{ \"kaneyama.fukushima.jp\", {} },\n        .{ \"kawamata.fukushima.jp\", {} },\n        .{ \"kitakata.fukushima.jp\", {} },\n        .{ \"kitashiobara.fukushima.jp\", {} },\n        .{ \"koori.fukushima.jp\", {} },\n        .{ \"koriyama.fukushima.jp\", {} },\n        .{ \"kunimi.fukushima.jp\", {} },\n        .{ \"miharu.fukushima.jp\", {} },\n        .{ \"mishima.fukushima.jp\", {} },\n        .{ \"namie.fukushima.jp\", {} },\n        .{ \"nango.fukushima.jp\", {} },\n        .{ \"nishiaizu.fukushima.jp\", {} },\n        .{ \"nishigo.fukushima.jp\", {} },\n        .{ \"okuma.fukushima.jp\", {} },\n        .{ \"omotego.fukushima.jp\", {} },\n        .{ \"ono.fukushima.jp\", {} },\n        .{ \"otama.fukushima.jp\", {} },\n        .{ \"samegawa.fukushima.jp\", {} },\n        .{ \"shimogo.fukushima.jp\", {} },\n        .{ \"shirakawa.fukushima.jp\", {} },\n        .{ \"showa.fukushima.jp\", {} },\n        .{ \"soma.fukushima.jp\", {} },\n        .{ \"sukagawa.fukushima.jp\", {} },\n        .{ \"taishin.fukushima.jp\", {} },\n        .{ \"tamakawa.fukushima.jp\", {} },\n        .{ \"tanagura.fukushima.jp\", {} },\n        .{ \"tenei.fukushima.jp\", {} },\n        .{ \"yabuki.fukushima.jp\", {} },\n        .{ \"yamato.fukushima.jp\", {} },\n        .{ \"yamatsuri.fukushima.jp\", {} },\n        .{ \"yanaizu.fukushima.jp\", {} },\n        .{ \"yugawa.fukushima.jp\", {} },\n        .{ \"anpachi.gifu.jp\", {} },\n        .{ \"ena.gifu.jp\", {} },\n        .{ \"gifu.gifu.jp\", {} },\n        .{ \"ginan.gifu.jp\", {} },\n        .{ \"godo.gifu.jp\", {} },\n        .{ \"gujo.gifu.jp\", {} },\n        .{ \"hashima.gifu.jp\", {} },\n        .{ \"hichiso.gifu.jp\", {} },\n        .{ \"hida.gifu.jp\", {} },\n        .{ \"higashishirakawa.gifu.jp\", {} },\n        .{ \"ibigawa.gifu.jp\", {} },\n        .{ \"ikeda.gifu.jp\", {} },\n        .{ \"kakamigahara.gifu.jp\", {} },\n        .{ \"kani.gifu.jp\", {} },\n        .{ \"kasahara.gifu.jp\", {} },\n        .{ \"kasamatsu.gifu.jp\", {} },\n        .{ \"kawaue.gifu.jp\", {} },\n        .{ \"kitagata.gifu.jp\", {} },\n        .{ \"mino.gifu.jp\", {} },\n        .{ \"minokamo.gifu.jp\", {} },\n        .{ \"mitake.gifu.jp\", {} },\n        .{ \"mizunami.gifu.jp\", {} },\n        .{ \"motosu.gifu.jp\", {} },\n        .{ \"nakatsugawa.gifu.jp\", {} },\n        .{ \"ogaki.gifu.jp\", {} },\n        .{ \"sakahogi.gifu.jp\", {} },\n        .{ \"seki.gifu.jp\", {} },\n        .{ \"sekigahara.gifu.jp\", {} },\n        .{ \"shirakawa.gifu.jp\", {} },\n        .{ \"tajimi.gifu.jp\", {} },\n        .{ \"takayama.gifu.jp\", {} },\n        .{ \"tarui.gifu.jp\", {} },\n        .{ \"toki.gifu.jp\", {} },\n        .{ \"tomika.gifu.jp\", {} },\n        .{ \"wanouchi.gifu.jp\", {} },\n        .{ \"yamagata.gifu.jp\", {} },\n        .{ \"yaotsu.gifu.jp\", {} },\n        .{ \"yoro.gifu.jp\", {} },\n        .{ \"annaka.gunma.jp\", {} },\n        .{ \"chiyoda.gunma.jp\", {} },\n        .{ \"fujioka.gunma.jp\", {} },\n        .{ \"higashiagatsuma.gunma.jp\", {} },\n        .{ \"isesaki.gunma.jp\", {} },\n        .{ \"itakura.gunma.jp\", {} },\n        .{ \"kanna.gunma.jp\", {} },\n        .{ \"kanra.gunma.jp\", {} },\n        .{ \"katashina.gunma.jp\", {} },\n        .{ \"kawaba.gunma.jp\", {} },\n        .{ \"kiryu.gunma.jp\", {} },\n        .{ \"kusatsu.gunma.jp\", {} },\n        .{ \"maebashi.gunma.jp\", {} },\n        .{ \"meiwa.gunma.jp\", {} },\n        .{ \"midori.gunma.jp\", {} },\n        .{ \"minakami.gunma.jp\", {} },\n        .{ \"naganohara.gunma.jp\", {} },\n        .{ \"nakanojo.gunma.jp\", {} },\n        .{ \"nanmoku.gunma.jp\", {} },\n        .{ \"numata.gunma.jp\", {} },\n        .{ \"oizumi.gunma.jp\", {} },\n        .{ \"ora.gunma.jp\", {} },\n        .{ \"ota.gunma.jp\", {} },\n        .{ \"shibukawa.gunma.jp\", {} },\n        .{ \"shimonita.gunma.jp\", {} },\n        .{ \"shinto.gunma.jp\", {} },\n        .{ \"showa.gunma.jp\", {} },\n        .{ \"takasaki.gunma.jp\", {} },\n        .{ \"takayama.gunma.jp\", {} },\n        .{ \"tamamura.gunma.jp\", {} },\n        .{ \"tatebayashi.gunma.jp\", {} },\n        .{ \"tomioka.gunma.jp\", {} },\n        .{ \"tsukiyono.gunma.jp\", {} },\n        .{ \"tsumagoi.gunma.jp\", {} },\n        .{ \"ueno.gunma.jp\", {} },\n        .{ \"yoshioka.gunma.jp\", {} },\n        .{ \"asaminami.hiroshima.jp\", {} },\n        .{ \"daiwa.hiroshima.jp\", {} },\n        .{ \"etajima.hiroshima.jp\", {} },\n        .{ \"fuchu.hiroshima.jp\", {} },\n        .{ \"fukuyama.hiroshima.jp\", {} },\n        .{ \"hatsukaichi.hiroshima.jp\", {} },\n        .{ \"higashihiroshima.hiroshima.jp\", {} },\n        .{ \"hongo.hiroshima.jp\", {} },\n        .{ \"jinsekikogen.hiroshima.jp\", {} },\n        .{ \"kaita.hiroshima.jp\", {} },\n        .{ \"kui.hiroshima.jp\", {} },\n        .{ \"kumano.hiroshima.jp\", {} },\n        .{ \"kure.hiroshima.jp\", {} },\n        .{ \"mihara.hiroshima.jp\", {} },\n        .{ \"miyoshi.hiroshima.jp\", {} },\n        .{ \"naka.hiroshima.jp\", {} },\n        .{ \"onomichi.hiroshima.jp\", {} },\n        .{ \"osakikamijima.hiroshima.jp\", {} },\n        .{ \"otake.hiroshima.jp\", {} },\n        .{ \"saka.hiroshima.jp\", {} },\n        .{ \"sera.hiroshima.jp\", {} },\n        .{ \"seranishi.hiroshima.jp\", {} },\n        .{ \"shinichi.hiroshima.jp\", {} },\n        .{ \"shobara.hiroshima.jp\", {} },\n        .{ \"takehara.hiroshima.jp\", {} },\n        .{ \"abashiri.hokkaido.jp\", {} },\n        .{ \"abira.hokkaido.jp\", {} },\n        .{ \"aibetsu.hokkaido.jp\", {} },\n        .{ \"akabira.hokkaido.jp\", {} },\n        .{ \"akkeshi.hokkaido.jp\", {} },\n        .{ \"asahikawa.hokkaido.jp\", {} },\n        .{ \"ashibetsu.hokkaido.jp\", {} },\n        .{ \"ashoro.hokkaido.jp\", {} },\n        .{ \"assabu.hokkaido.jp\", {} },\n        .{ \"atsuma.hokkaido.jp\", {} },\n        .{ \"bibai.hokkaido.jp\", {} },\n        .{ \"biei.hokkaido.jp\", {} },\n        .{ \"bifuka.hokkaido.jp\", {} },\n        .{ \"bihoro.hokkaido.jp\", {} },\n        .{ \"biratori.hokkaido.jp\", {} },\n        .{ \"chippubetsu.hokkaido.jp\", {} },\n        .{ \"chitose.hokkaido.jp\", {} },\n        .{ \"date.hokkaido.jp\", {} },\n        .{ \"ebetsu.hokkaido.jp\", {} },\n        .{ \"embetsu.hokkaido.jp\", {} },\n        .{ \"eniwa.hokkaido.jp\", {} },\n        .{ \"erimo.hokkaido.jp\", {} },\n        .{ \"esan.hokkaido.jp\", {} },\n        .{ \"esashi.hokkaido.jp\", {} },\n        .{ \"fukagawa.hokkaido.jp\", {} },\n        .{ \"fukushima.hokkaido.jp\", {} },\n        .{ \"furano.hokkaido.jp\", {} },\n        .{ \"furubira.hokkaido.jp\", {} },\n        .{ \"haboro.hokkaido.jp\", {} },\n        .{ \"hakodate.hokkaido.jp\", {} },\n        .{ \"hamatonbetsu.hokkaido.jp\", {} },\n        .{ \"hidaka.hokkaido.jp\", {} },\n        .{ \"higashikagura.hokkaido.jp\", {} },\n        .{ \"higashikawa.hokkaido.jp\", {} },\n        .{ \"hiroo.hokkaido.jp\", {} },\n        .{ \"hokuryu.hokkaido.jp\", {} },\n        .{ \"hokuto.hokkaido.jp\", {} },\n        .{ \"honbetsu.hokkaido.jp\", {} },\n        .{ \"horokanai.hokkaido.jp\", {} },\n        .{ \"horonobe.hokkaido.jp\", {} },\n        .{ \"ikeda.hokkaido.jp\", {} },\n        .{ \"imakane.hokkaido.jp\", {} },\n        .{ \"ishikari.hokkaido.jp\", {} },\n        .{ \"iwamizawa.hokkaido.jp\", {} },\n        .{ \"iwanai.hokkaido.jp\", {} },\n        .{ \"kamifurano.hokkaido.jp\", {} },\n        .{ \"kamikawa.hokkaido.jp\", {} },\n        .{ \"kamishihoro.hokkaido.jp\", {} },\n        .{ \"kamisunagawa.hokkaido.jp\", {} },\n        .{ \"kamoenai.hokkaido.jp\", {} },\n        .{ \"kayabe.hokkaido.jp\", {} },\n        .{ \"kembuchi.hokkaido.jp\", {} },\n        .{ \"kikonai.hokkaido.jp\", {} },\n        .{ \"kimobetsu.hokkaido.jp\", {} },\n        .{ \"kitahiroshima.hokkaido.jp\", {} },\n        .{ \"kitami.hokkaido.jp\", {} },\n        .{ \"kiyosato.hokkaido.jp\", {} },\n        .{ \"koshimizu.hokkaido.jp\", {} },\n        .{ \"kunneppu.hokkaido.jp\", {} },\n        .{ \"kuriyama.hokkaido.jp\", {} },\n        .{ \"kuromatsunai.hokkaido.jp\", {} },\n        .{ \"kushiro.hokkaido.jp\", {} },\n        .{ \"kutchan.hokkaido.jp\", {} },\n        .{ \"kyowa.hokkaido.jp\", {} },\n        .{ \"mashike.hokkaido.jp\", {} },\n        .{ \"matsumae.hokkaido.jp\", {} },\n        .{ \"mikasa.hokkaido.jp\", {} },\n        .{ \"minamifurano.hokkaido.jp\", {} },\n        .{ \"mombetsu.hokkaido.jp\", {} },\n        .{ \"moseushi.hokkaido.jp\", {} },\n        .{ \"mukawa.hokkaido.jp\", {} },\n        .{ \"muroran.hokkaido.jp\", {} },\n        .{ \"naie.hokkaido.jp\", {} },\n        .{ \"nakagawa.hokkaido.jp\", {} },\n        .{ \"nakasatsunai.hokkaido.jp\", {} },\n        .{ \"nakatombetsu.hokkaido.jp\", {} },\n        .{ \"nanae.hokkaido.jp\", {} },\n        .{ \"nanporo.hokkaido.jp\", {} },\n        .{ \"nayoro.hokkaido.jp\", {} },\n        .{ \"nemuro.hokkaido.jp\", {} },\n        .{ \"niikappu.hokkaido.jp\", {} },\n        .{ \"niki.hokkaido.jp\", {} },\n        .{ \"nishiokoppe.hokkaido.jp\", {} },\n        .{ \"noboribetsu.hokkaido.jp\", {} },\n        .{ \"numata.hokkaido.jp\", {} },\n        .{ \"obihiro.hokkaido.jp\", {} },\n        .{ \"obira.hokkaido.jp\", {} },\n        .{ \"oketo.hokkaido.jp\", {} },\n        .{ \"okoppe.hokkaido.jp\", {} },\n        .{ \"otaru.hokkaido.jp\", {} },\n        .{ \"otobe.hokkaido.jp\", {} },\n        .{ \"otofuke.hokkaido.jp\", {} },\n        .{ \"otoineppu.hokkaido.jp\", {} },\n        .{ \"oumu.hokkaido.jp\", {} },\n        .{ \"ozora.hokkaido.jp\", {} },\n        .{ \"pippu.hokkaido.jp\", {} },\n        .{ \"rankoshi.hokkaido.jp\", {} },\n        .{ \"rebun.hokkaido.jp\", {} },\n        .{ \"rikubetsu.hokkaido.jp\", {} },\n        .{ \"rishiri.hokkaido.jp\", {} },\n        .{ \"rishirifuji.hokkaido.jp\", {} },\n        .{ \"saroma.hokkaido.jp\", {} },\n        .{ \"sarufutsu.hokkaido.jp\", {} },\n        .{ \"shakotan.hokkaido.jp\", {} },\n        .{ \"shari.hokkaido.jp\", {} },\n        .{ \"shibecha.hokkaido.jp\", {} },\n        .{ \"shibetsu.hokkaido.jp\", {} },\n        .{ \"shikabe.hokkaido.jp\", {} },\n        .{ \"shikaoi.hokkaido.jp\", {} },\n        .{ \"shimamaki.hokkaido.jp\", {} },\n        .{ \"shimizu.hokkaido.jp\", {} },\n        .{ \"shimokawa.hokkaido.jp\", {} },\n        .{ \"shinshinotsu.hokkaido.jp\", {} },\n        .{ \"shintoku.hokkaido.jp\", {} },\n        .{ \"shiranuka.hokkaido.jp\", {} },\n        .{ \"shiraoi.hokkaido.jp\", {} },\n        .{ \"shiriuchi.hokkaido.jp\", {} },\n        .{ \"sobetsu.hokkaido.jp\", {} },\n        .{ \"sunagawa.hokkaido.jp\", {} },\n        .{ \"taiki.hokkaido.jp\", {} },\n        .{ \"takasu.hokkaido.jp\", {} },\n        .{ \"takikawa.hokkaido.jp\", {} },\n        .{ \"takinoue.hokkaido.jp\", {} },\n        .{ \"teshikaga.hokkaido.jp\", {} },\n        .{ \"tobetsu.hokkaido.jp\", {} },\n        .{ \"tohma.hokkaido.jp\", {} },\n        .{ \"tomakomai.hokkaido.jp\", {} },\n        .{ \"tomari.hokkaido.jp\", {} },\n        .{ \"toya.hokkaido.jp\", {} },\n        .{ \"toyako.hokkaido.jp\", {} },\n        .{ \"toyotomi.hokkaido.jp\", {} },\n        .{ \"toyoura.hokkaido.jp\", {} },\n        .{ \"tsubetsu.hokkaido.jp\", {} },\n        .{ \"tsukigata.hokkaido.jp\", {} },\n        .{ \"urakawa.hokkaido.jp\", {} },\n        .{ \"urausu.hokkaido.jp\", {} },\n        .{ \"uryu.hokkaido.jp\", {} },\n        .{ \"utashinai.hokkaido.jp\", {} },\n        .{ \"wakkanai.hokkaido.jp\", {} },\n        .{ \"wassamu.hokkaido.jp\", {} },\n        .{ \"yakumo.hokkaido.jp\", {} },\n        .{ \"yoichi.hokkaido.jp\", {} },\n        .{ \"aioi.hyogo.jp\", {} },\n        .{ \"akashi.hyogo.jp\", {} },\n        .{ \"ako.hyogo.jp\", {} },\n        .{ \"amagasaki.hyogo.jp\", {} },\n        .{ \"aogaki.hyogo.jp\", {} },\n        .{ \"asago.hyogo.jp\", {} },\n        .{ \"ashiya.hyogo.jp\", {} },\n        .{ \"awaji.hyogo.jp\", {} },\n        .{ \"fukusaki.hyogo.jp\", {} },\n        .{ \"goshiki.hyogo.jp\", {} },\n        .{ \"harima.hyogo.jp\", {} },\n        .{ \"himeji.hyogo.jp\", {} },\n        .{ \"ichikawa.hyogo.jp\", {} },\n        .{ \"inagawa.hyogo.jp\", {} },\n        .{ \"itami.hyogo.jp\", {} },\n        .{ \"kakogawa.hyogo.jp\", {} },\n        .{ \"kamigori.hyogo.jp\", {} },\n        .{ \"kamikawa.hyogo.jp\", {} },\n        .{ \"kasai.hyogo.jp\", {} },\n        .{ \"kasuga.hyogo.jp\", {} },\n        .{ \"kawanishi.hyogo.jp\", {} },\n        .{ \"miki.hyogo.jp\", {} },\n        .{ \"minamiawaji.hyogo.jp\", {} },\n        .{ \"nishinomiya.hyogo.jp\", {} },\n        .{ \"nishiwaki.hyogo.jp\", {} },\n        .{ \"ono.hyogo.jp\", {} },\n        .{ \"sanda.hyogo.jp\", {} },\n        .{ \"sannan.hyogo.jp\", {} },\n        .{ \"sasayama.hyogo.jp\", {} },\n        .{ \"sayo.hyogo.jp\", {} },\n        .{ \"shingu.hyogo.jp\", {} },\n        .{ \"shinonsen.hyogo.jp\", {} },\n        .{ \"shiso.hyogo.jp\", {} },\n        .{ \"sumoto.hyogo.jp\", {} },\n        .{ \"taishi.hyogo.jp\", {} },\n        .{ \"taka.hyogo.jp\", {} },\n        .{ \"takarazuka.hyogo.jp\", {} },\n        .{ \"takasago.hyogo.jp\", {} },\n        .{ \"takino.hyogo.jp\", {} },\n        .{ \"tamba.hyogo.jp\", {} },\n        .{ \"tatsuno.hyogo.jp\", {} },\n        .{ \"toyooka.hyogo.jp\", {} },\n        .{ \"yabu.hyogo.jp\", {} },\n        .{ \"yashiro.hyogo.jp\", {} },\n        .{ \"yoka.hyogo.jp\", {} },\n        .{ \"yokawa.hyogo.jp\", {} },\n        .{ \"ami.ibaraki.jp\", {} },\n        .{ \"asahi.ibaraki.jp\", {} },\n        .{ \"bando.ibaraki.jp\", {} },\n        .{ \"chikusei.ibaraki.jp\", {} },\n        .{ \"daigo.ibaraki.jp\", {} },\n        .{ \"fujishiro.ibaraki.jp\", {} },\n        .{ \"hitachi.ibaraki.jp\", {} },\n        .{ \"hitachinaka.ibaraki.jp\", {} },\n        .{ \"hitachiomiya.ibaraki.jp\", {} },\n        .{ \"hitachiota.ibaraki.jp\", {} },\n        .{ \"ibaraki.ibaraki.jp\", {} },\n        .{ \"ina.ibaraki.jp\", {} },\n        .{ \"inashiki.ibaraki.jp\", {} },\n        .{ \"itako.ibaraki.jp\", {} },\n        .{ \"iwama.ibaraki.jp\", {} },\n        .{ \"joso.ibaraki.jp\", {} },\n        .{ \"kamisu.ibaraki.jp\", {} },\n        .{ \"kasama.ibaraki.jp\", {} },\n        .{ \"kashima.ibaraki.jp\", {} },\n        .{ \"kasumigaura.ibaraki.jp\", {} },\n        .{ \"koga.ibaraki.jp\", {} },\n        .{ \"miho.ibaraki.jp\", {} },\n        .{ \"mito.ibaraki.jp\", {} },\n        .{ \"moriya.ibaraki.jp\", {} },\n        .{ \"naka.ibaraki.jp\", {} },\n        .{ \"namegata.ibaraki.jp\", {} },\n        .{ \"oarai.ibaraki.jp\", {} },\n        .{ \"ogawa.ibaraki.jp\", {} },\n        .{ \"omitama.ibaraki.jp\", {} },\n        .{ \"ryugasaki.ibaraki.jp\", {} },\n        .{ \"sakai.ibaraki.jp\", {} },\n        .{ \"sakuragawa.ibaraki.jp\", {} },\n        .{ \"shimodate.ibaraki.jp\", {} },\n        .{ \"shimotsuma.ibaraki.jp\", {} },\n        .{ \"shirosato.ibaraki.jp\", {} },\n        .{ \"sowa.ibaraki.jp\", {} },\n        .{ \"suifu.ibaraki.jp\", {} },\n        .{ \"takahagi.ibaraki.jp\", {} },\n        .{ \"tamatsukuri.ibaraki.jp\", {} },\n        .{ \"tokai.ibaraki.jp\", {} },\n        .{ \"tomobe.ibaraki.jp\", {} },\n        .{ \"tone.ibaraki.jp\", {} },\n        .{ \"toride.ibaraki.jp\", {} },\n        .{ \"tsuchiura.ibaraki.jp\", {} },\n        .{ \"tsukuba.ibaraki.jp\", {} },\n        .{ \"uchihara.ibaraki.jp\", {} },\n        .{ \"ushiku.ibaraki.jp\", {} },\n        .{ \"yachiyo.ibaraki.jp\", {} },\n        .{ \"yamagata.ibaraki.jp\", {} },\n        .{ \"yawara.ibaraki.jp\", {} },\n        .{ \"yuki.ibaraki.jp\", {} },\n        .{ \"anamizu.ishikawa.jp\", {} },\n        .{ \"hakui.ishikawa.jp\", {} },\n        .{ \"hakusan.ishikawa.jp\", {} },\n        .{ \"kaga.ishikawa.jp\", {} },\n        .{ \"kahoku.ishikawa.jp\", {} },\n        .{ \"kanazawa.ishikawa.jp\", {} },\n        .{ \"kawakita.ishikawa.jp\", {} },\n        .{ \"komatsu.ishikawa.jp\", {} },\n        .{ \"nakanoto.ishikawa.jp\", {} },\n        .{ \"nanao.ishikawa.jp\", {} },\n        .{ \"nomi.ishikawa.jp\", {} },\n        .{ \"nonoichi.ishikawa.jp\", {} },\n        .{ \"noto.ishikawa.jp\", {} },\n        .{ \"shika.ishikawa.jp\", {} },\n        .{ \"suzu.ishikawa.jp\", {} },\n        .{ \"tsubata.ishikawa.jp\", {} },\n        .{ \"tsurugi.ishikawa.jp\", {} },\n        .{ \"uchinada.ishikawa.jp\", {} },\n        .{ \"wajima.ishikawa.jp\", {} },\n        .{ \"fudai.iwate.jp\", {} },\n        .{ \"fujisawa.iwate.jp\", {} },\n        .{ \"hanamaki.iwate.jp\", {} },\n        .{ \"hiraizumi.iwate.jp\", {} },\n        .{ \"hirono.iwate.jp\", {} },\n        .{ \"ichinohe.iwate.jp\", {} },\n        .{ \"ichinoseki.iwate.jp\", {} },\n        .{ \"iwaizumi.iwate.jp\", {} },\n        .{ \"iwate.iwate.jp\", {} },\n        .{ \"joboji.iwate.jp\", {} },\n        .{ \"kamaishi.iwate.jp\", {} },\n        .{ \"kanegasaki.iwate.jp\", {} },\n        .{ \"karumai.iwate.jp\", {} },\n        .{ \"kawai.iwate.jp\", {} },\n        .{ \"kitakami.iwate.jp\", {} },\n        .{ \"kuji.iwate.jp\", {} },\n        .{ \"kunohe.iwate.jp\", {} },\n        .{ \"kuzumaki.iwate.jp\", {} },\n        .{ \"miyako.iwate.jp\", {} },\n        .{ \"mizusawa.iwate.jp\", {} },\n        .{ \"morioka.iwate.jp\", {} },\n        .{ \"ninohe.iwate.jp\", {} },\n        .{ \"noda.iwate.jp\", {} },\n        .{ \"ofunato.iwate.jp\", {} },\n        .{ \"oshu.iwate.jp\", {} },\n        .{ \"otsuchi.iwate.jp\", {} },\n        .{ \"rikuzentakata.iwate.jp\", {} },\n        .{ \"shiwa.iwate.jp\", {} },\n        .{ \"shizukuishi.iwate.jp\", {} },\n        .{ \"sumita.iwate.jp\", {} },\n        .{ \"tanohata.iwate.jp\", {} },\n        .{ \"tono.iwate.jp\", {} },\n        .{ \"yahaba.iwate.jp\", {} },\n        .{ \"yamada.iwate.jp\", {} },\n        .{ \"ayagawa.kagawa.jp\", {} },\n        .{ \"higashikagawa.kagawa.jp\", {} },\n        .{ \"kanonji.kagawa.jp\", {} },\n        .{ \"kotohira.kagawa.jp\", {} },\n        .{ \"manno.kagawa.jp\", {} },\n        .{ \"marugame.kagawa.jp\", {} },\n        .{ \"mitoyo.kagawa.jp\", {} },\n        .{ \"naoshima.kagawa.jp\", {} },\n        .{ \"sanuki.kagawa.jp\", {} },\n        .{ \"tadotsu.kagawa.jp\", {} },\n        .{ \"takamatsu.kagawa.jp\", {} },\n        .{ \"tonosho.kagawa.jp\", {} },\n        .{ \"uchinomi.kagawa.jp\", {} },\n        .{ \"utazu.kagawa.jp\", {} },\n        .{ \"zentsuji.kagawa.jp\", {} },\n        .{ \"akune.kagoshima.jp\", {} },\n        .{ \"amami.kagoshima.jp\", {} },\n        .{ \"hioki.kagoshima.jp\", {} },\n        .{ \"isa.kagoshima.jp\", {} },\n        .{ \"isen.kagoshima.jp\", {} },\n        .{ \"izumi.kagoshima.jp\", {} },\n        .{ \"kagoshima.kagoshima.jp\", {} },\n        .{ \"kanoya.kagoshima.jp\", {} },\n        .{ \"kawanabe.kagoshima.jp\", {} },\n        .{ \"kinko.kagoshima.jp\", {} },\n        .{ \"kouyama.kagoshima.jp\", {} },\n        .{ \"makurazaki.kagoshima.jp\", {} },\n        .{ \"matsumoto.kagoshima.jp\", {} },\n        .{ \"minamitane.kagoshima.jp\", {} },\n        .{ \"nakatane.kagoshima.jp\", {} },\n        .{ \"nishinoomote.kagoshima.jp\", {} },\n        .{ \"satsumasendai.kagoshima.jp\", {} },\n        .{ \"soo.kagoshima.jp\", {} },\n        .{ \"tarumizu.kagoshima.jp\", {} },\n        .{ \"yusui.kagoshima.jp\", {} },\n        .{ \"aikawa.kanagawa.jp\", {} },\n        .{ \"atsugi.kanagawa.jp\", {} },\n        .{ \"ayase.kanagawa.jp\", {} },\n        .{ \"chigasaki.kanagawa.jp\", {} },\n        .{ \"ebina.kanagawa.jp\", {} },\n        .{ \"fujisawa.kanagawa.jp\", {} },\n        .{ \"hadano.kanagawa.jp\", {} },\n        .{ \"hakone.kanagawa.jp\", {} },\n        .{ \"hiratsuka.kanagawa.jp\", {} },\n        .{ \"isehara.kanagawa.jp\", {} },\n        .{ \"kaisei.kanagawa.jp\", {} },\n        .{ \"kamakura.kanagawa.jp\", {} },\n        .{ \"kiyokawa.kanagawa.jp\", {} },\n        .{ \"matsuda.kanagawa.jp\", {} },\n        .{ \"minamiashigara.kanagawa.jp\", {} },\n        .{ \"miura.kanagawa.jp\", {} },\n        .{ \"nakai.kanagawa.jp\", {} },\n        .{ \"ninomiya.kanagawa.jp\", {} },\n        .{ \"odawara.kanagawa.jp\", {} },\n        .{ \"oi.kanagawa.jp\", {} },\n        .{ \"oiso.kanagawa.jp\", {} },\n        .{ \"sagamihara.kanagawa.jp\", {} },\n        .{ \"samukawa.kanagawa.jp\", {} },\n        .{ \"tsukui.kanagawa.jp\", {} },\n        .{ \"yamakita.kanagawa.jp\", {} },\n        .{ \"yamato.kanagawa.jp\", {} },\n        .{ \"yokosuka.kanagawa.jp\", {} },\n        .{ \"yugawara.kanagawa.jp\", {} },\n        .{ \"zama.kanagawa.jp\", {} },\n        .{ \"zushi.kanagawa.jp\", {} },\n        .{ \"aki.kochi.jp\", {} },\n        .{ \"geisei.kochi.jp\", {} },\n        .{ \"hidaka.kochi.jp\", {} },\n        .{ \"higashitsuno.kochi.jp\", {} },\n        .{ \"ino.kochi.jp\", {} },\n        .{ \"kagami.kochi.jp\", {} },\n        .{ \"kami.kochi.jp\", {} },\n        .{ \"kitagawa.kochi.jp\", {} },\n        .{ \"kochi.kochi.jp\", {} },\n        .{ \"mihara.kochi.jp\", {} },\n        .{ \"motoyama.kochi.jp\", {} },\n        .{ \"muroto.kochi.jp\", {} },\n        .{ \"nahari.kochi.jp\", {} },\n        .{ \"nakamura.kochi.jp\", {} },\n        .{ \"nankoku.kochi.jp\", {} },\n        .{ \"nishitosa.kochi.jp\", {} },\n        .{ \"niyodogawa.kochi.jp\", {} },\n        .{ \"ochi.kochi.jp\", {} },\n        .{ \"okawa.kochi.jp\", {} },\n        .{ \"otoyo.kochi.jp\", {} },\n        .{ \"otsuki.kochi.jp\", {} },\n        .{ \"sakawa.kochi.jp\", {} },\n        .{ \"sukumo.kochi.jp\", {} },\n        .{ \"susaki.kochi.jp\", {} },\n        .{ \"tosa.kochi.jp\", {} },\n        .{ \"tosashimizu.kochi.jp\", {} },\n        .{ \"toyo.kochi.jp\", {} },\n        .{ \"tsuno.kochi.jp\", {} },\n        .{ \"umaji.kochi.jp\", {} },\n        .{ \"yasuda.kochi.jp\", {} },\n        .{ \"yusuhara.kochi.jp\", {} },\n        .{ \"amakusa.kumamoto.jp\", {} },\n        .{ \"arao.kumamoto.jp\", {} },\n        .{ \"aso.kumamoto.jp\", {} },\n        .{ \"choyo.kumamoto.jp\", {} },\n        .{ \"gyokuto.kumamoto.jp\", {} },\n        .{ \"kamiamakusa.kumamoto.jp\", {} },\n        .{ \"kikuchi.kumamoto.jp\", {} },\n        .{ \"kumamoto.kumamoto.jp\", {} },\n        .{ \"mashiki.kumamoto.jp\", {} },\n        .{ \"mifune.kumamoto.jp\", {} },\n        .{ \"minamata.kumamoto.jp\", {} },\n        .{ \"minamioguni.kumamoto.jp\", {} },\n        .{ \"nagasu.kumamoto.jp\", {} },\n        .{ \"nishihara.kumamoto.jp\", {} },\n        .{ \"oguni.kumamoto.jp\", {} },\n        .{ \"ozu.kumamoto.jp\", {} },\n        .{ \"sumoto.kumamoto.jp\", {} },\n        .{ \"takamori.kumamoto.jp\", {} },\n        .{ \"uki.kumamoto.jp\", {} },\n        .{ \"uto.kumamoto.jp\", {} },\n        .{ \"yamaga.kumamoto.jp\", {} },\n        .{ \"yamato.kumamoto.jp\", {} },\n        .{ \"yatsushiro.kumamoto.jp\", {} },\n        .{ \"ayabe.kyoto.jp\", {} },\n        .{ \"fukuchiyama.kyoto.jp\", {} },\n        .{ \"higashiyama.kyoto.jp\", {} },\n        .{ \"ide.kyoto.jp\", {} },\n        .{ \"ine.kyoto.jp\", {} },\n        .{ \"joyo.kyoto.jp\", {} },\n        .{ \"kameoka.kyoto.jp\", {} },\n        .{ \"kamo.kyoto.jp\", {} },\n        .{ \"kita.kyoto.jp\", {} },\n        .{ \"kizu.kyoto.jp\", {} },\n        .{ \"kumiyama.kyoto.jp\", {} },\n        .{ \"kyotamba.kyoto.jp\", {} },\n        .{ \"kyotanabe.kyoto.jp\", {} },\n        .{ \"kyotango.kyoto.jp\", {} },\n        .{ \"maizuru.kyoto.jp\", {} },\n        .{ \"minami.kyoto.jp\", {} },\n        .{ \"minamiyamashiro.kyoto.jp\", {} },\n        .{ \"miyazu.kyoto.jp\", {} },\n        .{ \"muko.kyoto.jp\", {} },\n        .{ \"nagaokakyo.kyoto.jp\", {} },\n        .{ \"nakagyo.kyoto.jp\", {} },\n        .{ \"nantan.kyoto.jp\", {} },\n        .{ \"oyamazaki.kyoto.jp\", {} },\n        .{ \"sakyo.kyoto.jp\", {} },\n        .{ \"seika.kyoto.jp\", {} },\n        .{ \"tanabe.kyoto.jp\", {} },\n        .{ \"uji.kyoto.jp\", {} },\n        .{ \"ujitawara.kyoto.jp\", {} },\n        .{ \"wazuka.kyoto.jp\", {} },\n        .{ \"yamashina.kyoto.jp\", {} },\n        .{ \"yawata.kyoto.jp\", {} },\n        .{ \"asahi.mie.jp\", {} },\n        .{ \"inabe.mie.jp\", {} },\n        .{ \"ise.mie.jp\", {} },\n        .{ \"kameyama.mie.jp\", {} },\n        .{ \"kawagoe.mie.jp\", {} },\n        .{ \"kiho.mie.jp\", {} },\n        .{ \"kisosaki.mie.jp\", {} },\n        .{ \"kiwa.mie.jp\", {} },\n        .{ \"komono.mie.jp\", {} },\n        .{ \"kumano.mie.jp\", {} },\n        .{ \"kuwana.mie.jp\", {} },\n        .{ \"matsusaka.mie.jp\", {} },\n        .{ \"meiwa.mie.jp\", {} },\n        .{ \"mihama.mie.jp\", {} },\n        .{ \"minamiise.mie.jp\", {} },\n        .{ \"misugi.mie.jp\", {} },\n        .{ \"miyama.mie.jp\", {} },\n        .{ \"nabari.mie.jp\", {} },\n        .{ \"shima.mie.jp\", {} },\n        .{ \"suzuka.mie.jp\", {} },\n        .{ \"tado.mie.jp\", {} },\n        .{ \"taiki.mie.jp\", {} },\n        .{ \"taki.mie.jp\", {} },\n        .{ \"tamaki.mie.jp\", {} },\n        .{ \"toba.mie.jp\", {} },\n        .{ \"tsu.mie.jp\", {} },\n        .{ \"udono.mie.jp\", {} },\n        .{ \"ureshino.mie.jp\", {} },\n        .{ \"watarai.mie.jp\", {} },\n        .{ \"yokkaichi.mie.jp\", {} },\n        .{ \"furukawa.miyagi.jp\", {} },\n        .{ \"higashimatsushima.miyagi.jp\", {} },\n        .{ \"ishinomaki.miyagi.jp\", {} },\n        .{ \"iwanuma.miyagi.jp\", {} },\n        .{ \"kakuda.miyagi.jp\", {} },\n        .{ \"kami.miyagi.jp\", {} },\n        .{ \"kawasaki.miyagi.jp\", {} },\n        .{ \"marumori.miyagi.jp\", {} },\n        .{ \"matsushima.miyagi.jp\", {} },\n        .{ \"minamisanriku.miyagi.jp\", {} },\n        .{ \"misato.miyagi.jp\", {} },\n        .{ \"murata.miyagi.jp\", {} },\n        .{ \"natori.miyagi.jp\", {} },\n        .{ \"ogawara.miyagi.jp\", {} },\n        .{ \"ohira.miyagi.jp\", {} },\n        .{ \"onagawa.miyagi.jp\", {} },\n        .{ \"osaki.miyagi.jp\", {} },\n        .{ \"rifu.miyagi.jp\", {} },\n        .{ \"semine.miyagi.jp\", {} },\n        .{ \"shibata.miyagi.jp\", {} },\n        .{ \"shichikashuku.miyagi.jp\", {} },\n        .{ \"shikama.miyagi.jp\", {} },\n        .{ \"shiogama.miyagi.jp\", {} },\n        .{ \"shiroishi.miyagi.jp\", {} },\n        .{ \"tagajo.miyagi.jp\", {} },\n        .{ \"taiwa.miyagi.jp\", {} },\n        .{ \"tome.miyagi.jp\", {} },\n        .{ \"tomiya.miyagi.jp\", {} },\n        .{ \"wakuya.miyagi.jp\", {} },\n        .{ \"watari.miyagi.jp\", {} },\n        .{ \"yamamoto.miyagi.jp\", {} },\n        .{ \"zao.miyagi.jp\", {} },\n        .{ \"aya.miyazaki.jp\", {} },\n        .{ \"ebino.miyazaki.jp\", {} },\n        .{ \"gokase.miyazaki.jp\", {} },\n        .{ \"hyuga.miyazaki.jp\", {} },\n        .{ \"kadogawa.miyazaki.jp\", {} },\n        .{ \"kawaminami.miyazaki.jp\", {} },\n        .{ \"kijo.miyazaki.jp\", {} },\n        .{ \"kitagawa.miyazaki.jp\", {} },\n        .{ \"kitakata.miyazaki.jp\", {} },\n        .{ \"kitaura.miyazaki.jp\", {} },\n        .{ \"kobayashi.miyazaki.jp\", {} },\n        .{ \"kunitomi.miyazaki.jp\", {} },\n        .{ \"kushima.miyazaki.jp\", {} },\n        .{ \"mimata.miyazaki.jp\", {} },\n        .{ \"miyakonojo.miyazaki.jp\", {} },\n        .{ \"miyazaki.miyazaki.jp\", {} },\n        .{ \"morotsuka.miyazaki.jp\", {} },\n        .{ \"nichinan.miyazaki.jp\", {} },\n        .{ \"nishimera.miyazaki.jp\", {} },\n        .{ \"nobeoka.miyazaki.jp\", {} },\n        .{ \"saito.miyazaki.jp\", {} },\n        .{ \"shiiba.miyazaki.jp\", {} },\n        .{ \"shintomi.miyazaki.jp\", {} },\n        .{ \"takaharu.miyazaki.jp\", {} },\n        .{ \"takanabe.miyazaki.jp\", {} },\n        .{ \"takazaki.miyazaki.jp\", {} },\n        .{ \"tsuno.miyazaki.jp\", {} },\n        .{ \"achi.nagano.jp\", {} },\n        .{ \"agematsu.nagano.jp\", {} },\n        .{ \"anan.nagano.jp\", {} },\n        .{ \"aoki.nagano.jp\", {} },\n        .{ \"asahi.nagano.jp\", {} },\n        .{ \"azumino.nagano.jp\", {} },\n        .{ \"chikuhoku.nagano.jp\", {} },\n        .{ \"chikuma.nagano.jp\", {} },\n        .{ \"chino.nagano.jp\", {} },\n        .{ \"fujimi.nagano.jp\", {} },\n        .{ \"hakuba.nagano.jp\", {} },\n        .{ \"hara.nagano.jp\", {} },\n        .{ \"hiraya.nagano.jp\", {} },\n        .{ \"iida.nagano.jp\", {} },\n        .{ \"iijima.nagano.jp\", {} },\n        .{ \"iiyama.nagano.jp\", {} },\n        .{ \"iizuna.nagano.jp\", {} },\n        .{ \"ikeda.nagano.jp\", {} },\n        .{ \"ikusaka.nagano.jp\", {} },\n        .{ \"ina.nagano.jp\", {} },\n        .{ \"karuizawa.nagano.jp\", {} },\n        .{ \"kawakami.nagano.jp\", {} },\n        .{ \"kiso.nagano.jp\", {} },\n        .{ \"kisofukushima.nagano.jp\", {} },\n        .{ \"kitaaiki.nagano.jp\", {} },\n        .{ \"komagane.nagano.jp\", {} },\n        .{ \"komoro.nagano.jp\", {} },\n        .{ \"matsukawa.nagano.jp\", {} },\n        .{ \"matsumoto.nagano.jp\", {} },\n        .{ \"miasa.nagano.jp\", {} },\n        .{ \"minamiaiki.nagano.jp\", {} },\n        .{ \"minamimaki.nagano.jp\", {} },\n        .{ \"minamiminowa.nagano.jp\", {} },\n        .{ \"minowa.nagano.jp\", {} },\n        .{ \"miyada.nagano.jp\", {} },\n        .{ \"miyota.nagano.jp\", {} },\n        .{ \"mochizuki.nagano.jp\", {} },\n        .{ \"nagano.nagano.jp\", {} },\n        .{ \"nagawa.nagano.jp\", {} },\n        .{ \"nagiso.nagano.jp\", {} },\n        .{ \"nakagawa.nagano.jp\", {} },\n        .{ \"nakano.nagano.jp\", {} },\n        .{ \"nozawaonsen.nagano.jp\", {} },\n        .{ \"obuse.nagano.jp\", {} },\n        .{ \"ogawa.nagano.jp\", {} },\n        .{ \"okaya.nagano.jp\", {} },\n        .{ \"omachi.nagano.jp\", {} },\n        .{ \"omi.nagano.jp\", {} },\n        .{ \"ookuwa.nagano.jp\", {} },\n        .{ \"ooshika.nagano.jp\", {} },\n        .{ \"otaki.nagano.jp\", {} },\n        .{ \"otari.nagano.jp\", {} },\n        .{ \"sakae.nagano.jp\", {} },\n        .{ \"sakaki.nagano.jp\", {} },\n        .{ \"saku.nagano.jp\", {} },\n        .{ \"sakuho.nagano.jp\", {} },\n        .{ \"shimosuwa.nagano.jp\", {} },\n        .{ \"shinanomachi.nagano.jp\", {} },\n        .{ \"shiojiri.nagano.jp\", {} },\n        .{ \"suwa.nagano.jp\", {} },\n        .{ \"suzaka.nagano.jp\", {} },\n        .{ \"takagi.nagano.jp\", {} },\n        .{ \"takamori.nagano.jp\", {} },\n        .{ \"takayama.nagano.jp\", {} },\n        .{ \"tateshina.nagano.jp\", {} },\n        .{ \"tatsuno.nagano.jp\", {} },\n        .{ \"togakushi.nagano.jp\", {} },\n        .{ \"togura.nagano.jp\", {} },\n        .{ \"tomi.nagano.jp\", {} },\n        .{ \"ueda.nagano.jp\", {} },\n        .{ \"wada.nagano.jp\", {} },\n        .{ \"yamagata.nagano.jp\", {} },\n        .{ \"yamanouchi.nagano.jp\", {} },\n        .{ \"yasaka.nagano.jp\", {} },\n        .{ \"yasuoka.nagano.jp\", {} },\n        .{ \"chijiwa.nagasaki.jp\", {} },\n        .{ \"futsu.nagasaki.jp\", {} },\n        .{ \"goto.nagasaki.jp\", {} },\n        .{ \"hasami.nagasaki.jp\", {} },\n        .{ \"hirado.nagasaki.jp\", {} },\n        .{ \"iki.nagasaki.jp\", {} },\n        .{ \"isahaya.nagasaki.jp\", {} },\n        .{ \"kawatana.nagasaki.jp\", {} },\n        .{ \"kuchinotsu.nagasaki.jp\", {} },\n        .{ \"matsuura.nagasaki.jp\", {} },\n        .{ \"nagasaki.nagasaki.jp\", {} },\n        .{ \"obama.nagasaki.jp\", {} },\n        .{ \"omura.nagasaki.jp\", {} },\n        .{ \"oseto.nagasaki.jp\", {} },\n        .{ \"saikai.nagasaki.jp\", {} },\n        .{ \"sasebo.nagasaki.jp\", {} },\n        .{ \"seihi.nagasaki.jp\", {} },\n        .{ \"shimabara.nagasaki.jp\", {} },\n        .{ \"shinkamigoto.nagasaki.jp\", {} },\n        .{ \"togitsu.nagasaki.jp\", {} },\n        .{ \"tsushima.nagasaki.jp\", {} },\n        .{ \"unzen.nagasaki.jp\", {} },\n        .{ \"ando.nara.jp\", {} },\n        .{ \"gose.nara.jp\", {} },\n        .{ \"heguri.nara.jp\", {} },\n        .{ \"higashiyoshino.nara.jp\", {} },\n        .{ \"ikaruga.nara.jp\", {} },\n        .{ \"ikoma.nara.jp\", {} },\n        .{ \"kamikitayama.nara.jp\", {} },\n        .{ \"kanmaki.nara.jp\", {} },\n        .{ \"kashiba.nara.jp\", {} },\n        .{ \"kashihara.nara.jp\", {} },\n        .{ \"katsuragi.nara.jp\", {} },\n        .{ \"kawai.nara.jp\", {} },\n        .{ \"kawakami.nara.jp\", {} },\n        .{ \"kawanishi.nara.jp\", {} },\n        .{ \"koryo.nara.jp\", {} },\n        .{ \"kurotaki.nara.jp\", {} },\n        .{ \"mitsue.nara.jp\", {} },\n        .{ \"miyake.nara.jp\", {} },\n        .{ \"nara.nara.jp\", {} },\n        .{ \"nosegawa.nara.jp\", {} },\n        .{ \"oji.nara.jp\", {} },\n        .{ \"ouda.nara.jp\", {} },\n        .{ \"oyodo.nara.jp\", {} },\n        .{ \"sakurai.nara.jp\", {} },\n        .{ \"sango.nara.jp\", {} },\n        .{ \"shimoichi.nara.jp\", {} },\n        .{ \"shimokitayama.nara.jp\", {} },\n        .{ \"shinjo.nara.jp\", {} },\n        .{ \"soni.nara.jp\", {} },\n        .{ \"takatori.nara.jp\", {} },\n        .{ \"tawaramoto.nara.jp\", {} },\n        .{ \"tenkawa.nara.jp\", {} },\n        .{ \"tenri.nara.jp\", {} },\n        .{ \"uda.nara.jp\", {} },\n        .{ \"yamatokoriyama.nara.jp\", {} },\n        .{ \"yamatotakada.nara.jp\", {} },\n        .{ \"yamazoe.nara.jp\", {} },\n        .{ \"yoshino.nara.jp\", {} },\n        .{ \"aga.niigata.jp\", {} },\n        .{ \"agano.niigata.jp\", {} },\n        .{ \"gosen.niigata.jp\", {} },\n        .{ \"itoigawa.niigata.jp\", {} },\n        .{ \"izumozaki.niigata.jp\", {} },\n        .{ \"joetsu.niigata.jp\", {} },\n        .{ \"kamo.niigata.jp\", {} },\n        .{ \"kariwa.niigata.jp\", {} },\n        .{ \"kashiwazaki.niigata.jp\", {} },\n        .{ \"minamiuonuma.niigata.jp\", {} },\n        .{ \"mitsuke.niigata.jp\", {} },\n        .{ \"muika.niigata.jp\", {} },\n        .{ \"murakami.niigata.jp\", {} },\n        .{ \"myoko.niigata.jp\", {} },\n        .{ \"nagaoka.niigata.jp\", {} },\n        .{ \"niigata.niigata.jp\", {} },\n        .{ \"ojiya.niigata.jp\", {} },\n        .{ \"omi.niigata.jp\", {} },\n        .{ \"sado.niigata.jp\", {} },\n        .{ \"sanjo.niigata.jp\", {} },\n        .{ \"seiro.niigata.jp\", {} },\n        .{ \"seirou.niigata.jp\", {} },\n        .{ \"sekikawa.niigata.jp\", {} },\n        .{ \"shibata.niigata.jp\", {} },\n        .{ \"tagami.niigata.jp\", {} },\n        .{ \"tainai.niigata.jp\", {} },\n        .{ \"tochio.niigata.jp\", {} },\n        .{ \"tokamachi.niigata.jp\", {} },\n        .{ \"tsubame.niigata.jp\", {} },\n        .{ \"tsunan.niigata.jp\", {} },\n        .{ \"uonuma.niigata.jp\", {} },\n        .{ \"yahiko.niigata.jp\", {} },\n        .{ \"yoita.niigata.jp\", {} },\n        .{ \"yuzawa.niigata.jp\", {} },\n        .{ \"beppu.oita.jp\", {} },\n        .{ \"bungoono.oita.jp\", {} },\n        .{ \"bungotakada.oita.jp\", {} },\n        .{ \"hasama.oita.jp\", {} },\n        .{ \"hiji.oita.jp\", {} },\n        .{ \"himeshima.oita.jp\", {} },\n        .{ \"hita.oita.jp\", {} },\n        .{ \"kamitsue.oita.jp\", {} },\n        .{ \"kokonoe.oita.jp\", {} },\n        .{ \"kuju.oita.jp\", {} },\n        .{ \"kunisaki.oita.jp\", {} },\n        .{ \"kusu.oita.jp\", {} },\n        .{ \"oita.oita.jp\", {} },\n        .{ \"saiki.oita.jp\", {} },\n        .{ \"taketa.oita.jp\", {} },\n        .{ \"tsukumi.oita.jp\", {} },\n        .{ \"usa.oita.jp\", {} },\n        .{ \"usuki.oita.jp\", {} },\n        .{ \"yufu.oita.jp\", {} },\n        .{ \"akaiwa.okayama.jp\", {} },\n        .{ \"asakuchi.okayama.jp\", {} },\n        .{ \"bizen.okayama.jp\", {} },\n        .{ \"hayashima.okayama.jp\", {} },\n        .{ \"ibara.okayama.jp\", {} },\n        .{ \"kagamino.okayama.jp\", {} },\n        .{ \"kasaoka.okayama.jp\", {} },\n        .{ \"kibichuo.okayama.jp\", {} },\n        .{ \"kumenan.okayama.jp\", {} },\n        .{ \"kurashiki.okayama.jp\", {} },\n        .{ \"maniwa.okayama.jp\", {} },\n        .{ \"misaki.okayama.jp\", {} },\n        .{ \"nagi.okayama.jp\", {} },\n        .{ \"niimi.okayama.jp\", {} },\n        .{ \"nishiawakura.okayama.jp\", {} },\n        .{ \"okayama.okayama.jp\", {} },\n        .{ \"satosho.okayama.jp\", {} },\n        .{ \"setouchi.okayama.jp\", {} },\n        .{ \"shinjo.okayama.jp\", {} },\n        .{ \"shoo.okayama.jp\", {} },\n        .{ \"soja.okayama.jp\", {} },\n        .{ \"takahashi.okayama.jp\", {} },\n        .{ \"tamano.okayama.jp\", {} },\n        .{ \"tsuyama.okayama.jp\", {} },\n        .{ \"wake.okayama.jp\", {} },\n        .{ \"yakage.okayama.jp\", {} },\n        .{ \"aguni.okinawa.jp\", {} },\n        .{ \"ginowan.okinawa.jp\", {} },\n        .{ \"ginoza.okinawa.jp\", {} },\n        .{ \"gushikami.okinawa.jp\", {} },\n        .{ \"haebaru.okinawa.jp\", {} },\n        .{ \"higashi.okinawa.jp\", {} },\n        .{ \"hirara.okinawa.jp\", {} },\n        .{ \"iheya.okinawa.jp\", {} },\n        .{ \"ishigaki.okinawa.jp\", {} },\n        .{ \"ishikawa.okinawa.jp\", {} },\n        .{ \"itoman.okinawa.jp\", {} },\n        .{ \"izena.okinawa.jp\", {} },\n        .{ \"kadena.okinawa.jp\", {} },\n        .{ \"kin.okinawa.jp\", {} },\n        .{ \"kitadaito.okinawa.jp\", {} },\n        .{ \"kitanakagusuku.okinawa.jp\", {} },\n        .{ \"kumejima.okinawa.jp\", {} },\n        .{ \"kunigami.okinawa.jp\", {} },\n        .{ \"minamidaito.okinawa.jp\", {} },\n        .{ \"motobu.okinawa.jp\", {} },\n        .{ \"nago.okinawa.jp\", {} },\n        .{ \"naha.okinawa.jp\", {} },\n        .{ \"nakagusuku.okinawa.jp\", {} },\n        .{ \"nakijin.okinawa.jp\", {} },\n        .{ \"nanjo.okinawa.jp\", {} },\n        .{ \"nishihara.okinawa.jp\", {} },\n        .{ \"ogimi.okinawa.jp\", {} },\n        .{ \"okinawa.okinawa.jp\", {} },\n        .{ \"onna.okinawa.jp\", {} },\n        .{ \"shimoji.okinawa.jp\", {} },\n        .{ \"taketomi.okinawa.jp\", {} },\n        .{ \"tarama.okinawa.jp\", {} },\n        .{ \"tokashiki.okinawa.jp\", {} },\n        .{ \"tomigusuku.okinawa.jp\", {} },\n        .{ \"tonaki.okinawa.jp\", {} },\n        .{ \"urasoe.okinawa.jp\", {} },\n        .{ \"uruma.okinawa.jp\", {} },\n        .{ \"yaese.okinawa.jp\", {} },\n        .{ \"yomitan.okinawa.jp\", {} },\n        .{ \"yonabaru.okinawa.jp\", {} },\n        .{ \"yonaguni.okinawa.jp\", {} },\n        .{ \"zamami.okinawa.jp\", {} },\n        .{ \"abeno.osaka.jp\", {} },\n        .{ \"chihayaakasaka.osaka.jp\", {} },\n        .{ \"chuo.osaka.jp\", {} },\n        .{ \"daito.osaka.jp\", {} },\n        .{ \"fujiidera.osaka.jp\", {} },\n        .{ \"habikino.osaka.jp\", {} },\n        .{ \"hannan.osaka.jp\", {} },\n        .{ \"higashiosaka.osaka.jp\", {} },\n        .{ \"higashisumiyoshi.osaka.jp\", {} },\n        .{ \"higashiyodogawa.osaka.jp\", {} },\n        .{ \"hirakata.osaka.jp\", {} },\n        .{ \"ibaraki.osaka.jp\", {} },\n        .{ \"ikeda.osaka.jp\", {} },\n        .{ \"izumi.osaka.jp\", {} },\n        .{ \"izumiotsu.osaka.jp\", {} },\n        .{ \"izumisano.osaka.jp\", {} },\n        .{ \"kadoma.osaka.jp\", {} },\n        .{ \"kaizuka.osaka.jp\", {} },\n        .{ \"kanan.osaka.jp\", {} },\n        .{ \"kashiwara.osaka.jp\", {} },\n        .{ \"katano.osaka.jp\", {} },\n        .{ \"kawachinagano.osaka.jp\", {} },\n        .{ \"kishiwada.osaka.jp\", {} },\n        .{ \"kita.osaka.jp\", {} },\n        .{ \"kumatori.osaka.jp\", {} },\n        .{ \"matsubara.osaka.jp\", {} },\n        .{ \"minato.osaka.jp\", {} },\n        .{ \"minoh.osaka.jp\", {} },\n        .{ \"misaki.osaka.jp\", {} },\n        .{ \"moriguchi.osaka.jp\", {} },\n        .{ \"neyagawa.osaka.jp\", {} },\n        .{ \"nishi.osaka.jp\", {} },\n        .{ \"nose.osaka.jp\", {} },\n        .{ \"osakasayama.osaka.jp\", {} },\n        .{ \"sakai.osaka.jp\", {} },\n        .{ \"sayama.osaka.jp\", {} },\n        .{ \"sennan.osaka.jp\", {} },\n        .{ \"settsu.osaka.jp\", {} },\n        .{ \"shijonawate.osaka.jp\", {} },\n        .{ \"shimamoto.osaka.jp\", {} },\n        .{ \"suita.osaka.jp\", {} },\n        .{ \"tadaoka.osaka.jp\", {} },\n        .{ \"taishi.osaka.jp\", {} },\n        .{ \"tajiri.osaka.jp\", {} },\n        .{ \"takaishi.osaka.jp\", {} },\n        .{ \"takatsuki.osaka.jp\", {} },\n        .{ \"tondabayashi.osaka.jp\", {} },\n        .{ \"toyonaka.osaka.jp\", {} },\n        .{ \"toyono.osaka.jp\", {} },\n        .{ \"yao.osaka.jp\", {} },\n        .{ \"ariake.saga.jp\", {} },\n        .{ \"arita.saga.jp\", {} },\n        .{ \"fukudomi.saga.jp\", {} },\n        .{ \"genkai.saga.jp\", {} },\n        .{ \"hamatama.saga.jp\", {} },\n        .{ \"hizen.saga.jp\", {} },\n        .{ \"imari.saga.jp\", {} },\n        .{ \"kamimine.saga.jp\", {} },\n        .{ \"kanzaki.saga.jp\", {} },\n        .{ \"karatsu.saga.jp\", {} },\n        .{ \"kashima.saga.jp\", {} },\n        .{ \"kitagata.saga.jp\", {} },\n        .{ \"kitahata.saga.jp\", {} },\n        .{ \"kiyama.saga.jp\", {} },\n        .{ \"kouhoku.saga.jp\", {} },\n        .{ \"kyuragi.saga.jp\", {} },\n        .{ \"nishiarita.saga.jp\", {} },\n        .{ \"ogi.saga.jp\", {} },\n        .{ \"omachi.saga.jp\", {} },\n        .{ \"ouchi.saga.jp\", {} },\n        .{ \"saga.saga.jp\", {} },\n        .{ \"shiroishi.saga.jp\", {} },\n        .{ \"taku.saga.jp\", {} },\n        .{ \"tara.saga.jp\", {} },\n        .{ \"tosu.saga.jp\", {} },\n        .{ \"yoshinogari.saga.jp\", {} },\n        .{ \"arakawa.saitama.jp\", {} },\n        .{ \"asaka.saitama.jp\", {} },\n        .{ \"chichibu.saitama.jp\", {} },\n        .{ \"fujimi.saitama.jp\", {} },\n        .{ \"fujimino.saitama.jp\", {} },\n        .{ \"fukaya.saitama.jp\", {} },\n        .{ \"hanno.saitama.jp\", {} },\n        .{ \"hanyu.saitama.jp\", {} },\n        .{ \"hasuda.saitama.jp\", {} },\n        .{ \"hatogaya.saitama.jp\", {} },\n        .{ \"hatoyama.saitama.jp\", {} },\n        .{ \"hidaka.saitama.jp\", {} },\n        .{ \"higashichichibu.saitama.jp\", {} },\n        .{ \"higashimatsuyama.saitama.jp\", {} },\n        .{ \"honjo.saitama.jp\", {} },\n        .{ \"ina.saitama.jp\", {} },\n        .{ \"iruma.saitama.jp\", {} },\n        .{ \"iwatsuki.saitama.jp\", {} },\n        .{ \"kamiizumi.saitama.jp\", {} },\n        .{ \"kamikawa.saitama.jp\", {} },\n        .{ \"kamisato.saitama.jp\", {} },\n        .{ \"kasukabe.saitama.jp\", {} },\n        .{ \"kawagoe.saitama.jp\", {} },\n        .{ \"kawaguchi.saitama.jp\", {} },\n        .{ \"kawajima.saitama.jp\", {} },\n        .{ \"kazo.saitama.jp\", {} },\n        .{ \"kitamoto.saitama.jp\", {} },\n        .{ \"koshigaya.saitama.jp\", {} },\n        .{ \"kounosu.saitama.jp\", {} },\n        .{ \"kuki.saitama.jp\", {} },\n        .{ \"kumagaya.saitama.jp\", {} },\n        .{ \"matsubushi.saitama.jp\", {} },\n        .{ \"minano.saitama.jp\", {} },\n        .{ \"misato.saitama.jp\", {} },\n        .{ \"miyashiro.saitama.jp\", {} },\n        .{ \"miyoshi.saitama.jp\", {} },\n        .{ \"moroyama.saitama.jp\", {} },\n        .{ \"nagatoro.saitama.jp\", {} },\n        .{ \"namegawa.saitama.jp\", {} },\n        .{ \"niiza.saitama.jp\", {} },\n        .{ \"ogano.saitama.jp\", {} },\n        .{ \"ogawa.saitama.jp\", {} },\n        .{ \"ogose.saitama.jp\", {} },\n        .{ \"okegawa.saitama.jp\", {} },\n        .{ \"omiya.saitama.jp\", {} },\n        .{ \"otaki.saitama.jp\", {} },\n        .{ \"ranzan.saitama.jp\", {} },\n        .{ \"ryokami.saitama.jp\", {} },\n        .{ \"saitama.saitama.jp\", {} },\n        .{ \"sakado.saitama.jp\", {} },\n        .{ \"satte.saitama.jp\", {} },\n        .{ \"sayama.saitama.jp\", {} },\n        .{ \"shiki.saitama.jp\", {} },\n        .{ \"shiraoka.saitama.jp\", {} },\n        .{ \"soka.saitama.jp\", {} },\n        .{ \"sugito.saitama.jp\", {} },\n        .{ \"toda.saitama.jp\", {} },\n        .{ \"tokigawa.saitama.jp\", {} },\n        .{ \"tokorozawa.saitama.jp\", {} },\n        .{ \"tsurugashima.saitama.jp\", {} },\n        .{ \"urawa.saitama.jp\", {} },\n        .{ \"warabi.saitama.jp\", {} },\n        .{ \"yashio.saitama.jp\", {} },\n        .{ \"yokoze.saitama.jp\", {} },\n        .{ \"yono.saitama.jp\", {} },\n        .{ \"yorii.saitama.jp\", {} },\n        .{ \"yoshida.saitama.jp\", {} },\n        .{ \"yoshikawa.saitama.jp\", {} },\n        .{ \"yoshimi.saitama.jp\", {} },\n        .{ \"aisho.shiga.jp\", {} },\n        .{ \"gamo.shiga.jp\", {} },\n        .{ \"higashiomi.shiga.jp\", {} },\n        .{ \"hikone.shiga.jp\", {} },\n        .{ \"koka.shiga.jp\", {} },\n        .{ \"konan.shiga.jp\", {} },\n        .{ \"kosei.shiga.jp\", {} },\n        .{ \"koto.shiga.jp\", {} },\n        .{ \"kusatsu.shiga.jp\", {} },\n        .{ \"maibara.shiga.jp\", {} },\n        .{ \"moriyama.shiga.jp\", {} },\n        .{ \"nagahama.shiga.jp\", {} },\n        .{ \"nishiazai.shiga.jp\", {} },\n        .{ \"notogawa.shiga.jp\", {} },\n        .{ \"omihachiman.shiga.jp\", {} },\n        .{ \"otsu.shiga.jp\", {} },\n        .{ \"ritto.shiga.jp\", {} },\n        .{ \"ryuoh.shiga.jp\", {} },\n        .{ \"takashima.shiga.jp\", {} },\n        .{ \"takatsuki.shiga.jp\", {} },\n        .{ \"torahime.shiga.jp\", {} },\n        .{ \"toyosato.shiga.jp\", {} },\n        .{ \"yasu.shiga.jp\", {} },\n        .{ \"akagi.shimane.jp\", {} },\n        .{ \"ama.shimane.jp\", {} },\n        .{ \"gotsu.shimane.jp\", {} },\n        .{ \"hamada.shimane.jp\", {} },\n        .{ \"higashiizumo.shimane.jp\", {} },\n        .{ \"hikawa.shimane.jp\", {} },\n        .{ \"hikimi.shimane.jp\", {} },\n        .{ \"izumo.shimane.jp\", {} },\n        .{ \"kakinoki.shimane.jp\", {} },\n        .{ \"masuda.shimane.jp\", {} },\n        .{ \"matsue.shimane.jp\", {} },\n        .{ \"misato.shimane.jp\", {} },\n        .{ \"nishinoshima.shimane.jp\", {} },\n        .{ \"ohda.shimane.jp\", {} },\n        .{ \"okinoshima.shimane.jp\", {} },\n        .{ \"okuizumo.shimane.jp\", {} },\n        .{ \"shimane.shimane.jp\", {} },\n        .{ \"tamayu.shimane.jp\", {} },\n        .{ \"tsuwano.shimane.jp\", {} },\n        .{ \"unnan.shimane.jp\", {} },\n        .{ \"yakumo.shimane.jp\", {} },\n        .{ \"yasugi.shimane.jp\", {} },\n        .{ \"yatsuka.shimane.jp\", {} },\n        .{ \"arai.shizuoka.jp\", {} },\n        .{ \"atami.shizuoka.jp\", {} },\n        .{ \"fuji.shizuoka.jp\", {} },\n        .{ \"fujieda.shizuoka.jp\", {} },\n        .{ \"fujikawa.shizuoka.jp\", {} },\n        .{ \"fujinomiya.shizuoka.jp\", {} },\n        .{ \"fukuroi.shizuoka.jp\", {} },\n        .{ \"gotemba.shizuoka.jp\", {} },\n        .{ \"haibara.shizuoka.jp\", {} },\n        .{ \"hamamatsu.shizuoka.jp\", {} },\n        .{ \"higashiizu.shizuoka.jp\", {} },\n        .{ \"ito.shizuoka.jp\", {} },\n        .{ \"iwata.shizuoka.jp\", {} },\n        .{ \"izu.shizuoka.jp\", {} },\n        .{ \"izunokuni.shizuoka.jp\", {} },\n        .{ \"kakegawa.shizuoka.jp\", {} },\n        .{ \"kannami.shizuoka.jp\", {} },\n        .{ \"kawanehon.shizuoka.jp\", {} },\n        .{ \"kawazu.shizuoka.jp\", {} },\n        .{ \"kikugawa.shizuoka.jp\", {} },\n        .{ \"kosai.shizuoka.jp\", {} },\n        .{ \"makinohara.shizuoka.jp\", {} },\n        .{ \"matsuzaki.shizuoka.jp\", {} },\n        .{ \"minamiizu.shizuoka.jp\", {} },\n        .{ \"mishima.shizuoka.jp\", {} },\n        .{ \"morimachi.shizuoka.jp\", {} },\n        .{ \"nishiizu.shizuoka.jp\", {} },\n        .{ \"numazu.shizuoka.jp\", {} },\n        .{ \"omaezaki.shizuoka.jp\", {} },\n        .{ \"shimada.shizuoka.jp\", {} },\n        .{ \"shimizu.shizuoka.jp\", {} },\n        .{ \"shimoda.shizuoka.jp\", {} },\n        .{ \"shizuoka.shizuoka.jp\", {} },\n        .{ \"susono.shizuoka.jp\", {} },\n        .{ \"yaizu.shizuoka.jp\", {} },\n        .{ \"yoshida.shizuoka.jp\", {} },\n        .{ \"ashikaga.tochigi.jp\", {} },\n        .{ \"bato.tochigi.jp\", {} },\n        .{ \"haga.tochigi.jp\", {} },\n        .{ \"ichikai.tochigi.jp\", {} },\n        .{ \"iwafune.tochigi.jp\", {} },\n        .{ \"kaminokawa.tochigi.jp\", {} },\n        .{ \"kanuma.tochigi.jp\", {} },\n        .{ \"karasuyama.tochigi.jp\", {} },\n        .{ \"kuroiso.tochigi.jp\", {} },\n        .{ \"mashiko.tochigi.jp\", {} },\n        .{ \"mibu.tochigi.jp\", {} },\n        .{ \"moka.tochigi.jp\", {} },\n        .{ \"motegi.tochigi.jp\", {} },\n        .{ \"nasu.tochigi.jp\", {} },\n        .{ \"nasushiobara.tochigi.jp\", {} },\n        .{ \"nikko.tochigi.jp\", {} },\n        .{ \"nishikata.tochigi.jp\", {} },\n        .{ \"nogi.tochigi.jp\", {} },\n        .{ \"ohira.tochigi.jp\", {} },\n        .{ \"ohtawara.tochigi.jp\", {} },\n        .{ \"oyama.tochigi.jp\", {} },\n        .{ \"sakura.tochigi.jp\", {} },\n        .{ \"sano.tochigi.jp\", {} },\n        .{ \"shimotsuke.tochigi.jp\", {} },\n        .{ \"shioya.tochigi.jp\", {} },\n        .{ \"takanezawa.tochigi.jp\", {} },\n        .{ \"tochigi.tochigi.jp\", {} },\n        .{ \"tsuga.tochigi.jp\", {} },\n        .{ \"ujiie.tochigi.jp\", {} },\n        .{ \"utsunomiya.tochigi.jp\", {} },\n        .{ \"yaita.tochigi.jp\", {} },\n        .{ \"aizumi.tokushima.jp\", {} },\n        .{ \"anan.tokushima.jp\", {} },\n        .{ \"ichiba.tokushima.jp\", {} },\n        .{ \"itano.tokushima.jp\", {} },\n        .{ \"kainan.tokushima.jp\", {} },\n        .{ \"komatsushima.tokushima.jp\", {} },\n        .{ \"matsushige.tokushima.jp\", {} },\n        .{ \"mima.tokushima.jp\", {} },\n        .{ \"minami.tokushima.jp\", {} },\n        .{ \"miyoshi.tokushima.jp\", {} },\n        .{ \"mugi.tokushima.jp\", {} },\n        .{ \"nakagawa.tokushima.jp\", {} },\n        .{ \"naruto.tokushima.jp\", {} },\n        .{ \"sanagochi.tokushima.jp\", {} },\n        .{ \"shishikui.tokushima.jp\", {} },\n        .{ \"tokushima.tokushima.jp\", {} },\n        .{ \"wajiki.tokushima.jp\", {} },\n        .{ \"adachi.tokyo.jp\", {} },\n        .{ \"akiruno.tokyo.jp\", {} },\n        .{ \"akishima.tokyo.jp\", {} },\n        .{ \"aogashima.tokyo.jp\", {} },\n        .{ \"arakawa.tokyo.jp\", {} },\n        .{ \"bunkyo.tokyo.jp\", {} },\n        .{ \"chiyoda.tokyo.jp\", {} },\n        .{ \"chofu.tokyo.jp\", {} },\n        .{ \"chuo.tokyo.jp\", {} },\n        .{ \"edogawa.tokyo.jp\", {} },\n        .{ \"fuchu.tokyo.jp\", {} },\n        .{ \"fussa.tokyo.jp\", {} },\n        .{ \"hachijo.tokyo.jp\", {} },\n        .{ \"hachioji.tokyo.jp\", {} },\n        .{ \"hamura.tokyo.jp\", {} },\n        .{ \"higashikurume.tokyo.jp\", {} },\n        .{ \"higashimurayama.tokyo.jp\", {} },\n        .{ \"higashiyamato.tokyo.jp\", {} },\n        .{ \"hino.tokyo.jp\", {} },\n        .{ \"hinode.tokyo.jp\", {} },\n        .{ \"hinohara.tokyo.jp\", {} },\n        .{ \"inagi.tokyo.jp\", {} },\n        .{ \"itabashi.tokyo.jp\", {} },\n        .{ \"katsushika.tokyo.jp\", {} },\n        .{ \"kita.tokyo.jp\", {} },\n        .{ \"kiyose.tokyo.jp\", {} },\n        .{ \"kodaira.tokyo.jp\", {} },\n        .{ \"koganei.tokyo.jp\", {} },\n        .{ \"kokubunji.tokyo.jp\", {} },\n        .{ \"komae.tokyo.jp\", {} },\n        .{ \"koto.tokyo.jp\", {} },\n        .{ \"kouzushima.tokyo.jp\", {} },\n        .{ \"kunitachi.tokyo.jp\", {} },\n        .{ \"machida.tokyo.jp\", {} },\n        .{ \"meguro.tokyo.jp\", {} },\n        .{ \"minato.tokyo.jp\", {} },\n        .{ \"mitaka.tokyo.jp\", {} },\n        .{ \"mizuho.tokyo.jp\", {} },\n        .{ \"musashimurayama.tokyo.jp\", {} },\n        .{ \"musashino.tokyo.jp\", {} },\n        .{ \"nakano.tokyo.jp\", {} },\n        .{ \"nerima.tokyo.jp\", {} },\n        .{ \"ogasawara.tokyo.jp\", {} },\n        .{ \"okutama.tokyo.jp\", {} },\n        .{ \"ome.tokyo.jp\", {} },\n        .{ \"oshima.tokyo.jp\", {} },\n        .{ \"ota.tokyo.jp\", {} },\n        .{ \"setagaya.tokyo.jp\", {} },\n        .{ \"shibuya.tokyo.jp\", {} },\n        .{ \"shinagawa.tokyo.jp\", {} },\n        .{ \"shinjuku.tokyo.jp\", {} },\n        .{ \"suginami.tokyo.jp\", {} },\n        .{ \"sumida.tokyo.jp\", {} },\n        .{ \"tachikawa.tokyo.jp\", {} },\n        .{ \"taito.tokyo.jp\", {} },\n        .{ \"tama.tokyo.jp\", {} },\n        .{ \"toshima.tokyo.jp\", {} },\n        .{ \"chizu.tottori.jp\", {} },\n        .{ \"hino.tottori.jp\", {} },\n        .{ \"kawahara.tottori.jp\", {} },\n        .{ \"koge.tottori.jp\", {} },\n        .{ \"kotoura.tottori.jp\", {} },\n        .{ \"misasa.tottori.jp\", {} },\n        .{ \"nanbu.tottori.jp\", {} },\n        .{ \"nichinan.tottori.jp\", {} },\n        .{ \"sakaiminato.tottori.jp\", {} },\n        .{ \"tottori.tottori.jp\", {} },\n        .{ \"wakasa.tottori.jp\", {} },\n        .{ \"yazu.tottori.jp\", {} },\n        .{ \"yonago.tottori.jp\", {} },\n        .{ \"asahi.toyama.jp\", {} },\n        .{ \"fuchu.toyama.jp\", {} },\n        .{ \"fukumitsu.toyama.jp\", {} },\n        .{ \"funahashi.toyama.jp\", {} },\n        .{ \"himi.toyama.jp\", {} },\n        .{ \"imizu.toyama.jp\", {} },\n        .{ \"inami.toyama.jp\", {} },\n        .{ \"johana.toyama.jp\", {} },\n        .{ \"kamiichi.toyama.jp\", {} },\n        .{ \"kurobe.toyama.jp\", {} },\n        .{ \"nakaniikawa.toyama.jp\", {} },\n        .{ \"namerikawa.toyama.jp\", {} },\n        .{ \"nanto.toyama.jp\", {} },\n        .{ \"nyuzen.toyama.jp\", {} },\n        .{ \"oyabe.toyama.jp\", {} },\n        .{ \"taira.toyama.jp\", {} },\n        .{ \"takaoka.toyama.jp\", {} },\n        .{ \"tateyama.toyama.jp\", {} },\n        .{ \"toga.toyama.jp\", {} },\n        .{ \"tonami.toyama.jp\", {} },\n        .{ \"toyama.toyama.jp\", {} },\n        .{ \"unazuki.toyama.jp\", {} },\n        .{ \"uozu.toyama.jp\", {} },\n        .{ \"yamada.toyama.jp\", {} },\n        .{ \"arida.wakayama.jp\", {} },\n        .{ \"aridagawa.wakayama.jp\", {} },\n        .{ \"gobo.wakayama.jp\", {} },\n        .{ \"hashimoto.wakayama.jp\", {} },\n        .{ \"hidaka.wakayama.jp\", {} },\n        .{ \"hirogawa.wakayama.jp\", {} },\n        .{ \"inami.wakayama.jp\", {} },\n        .{ \"iwade.wakayama.jp\", {} },\n        .{ \"kainan.wakayama.jp\", {} },\n        .{ \"kamitonda.wakayama.jp\", {} },\n        .{ \"katsuragi.wakayama.jp\", {} },\n        .{ \"kimino.wakayama.jp\", {} },\n        .{ \"kinokawa.wakayama.jp\", {} },\n        .{ \"kitayama.wakayama.jp\", {} },\n        .{ \"koya.wakayama.jp\", {} },\n        .{ \"koza.wakayama.jp\", {} },\n        .{ \"kozagawa.wakayama.jp\", {} },\n        .{ \"kudoyama.wakayama.jp\", {} },\n        .{ \"kushimoto.wakayama.jp\", {} },\n        .{ \"mihama.wakayama.jp\", {} },\n        .{ \"misato.wakayama.jp\", {} },\n        .{ \"nachikatsuura.wakayama.jp\", {} },\n        .{ \"shingu.wakayama.jp\", {} },\n        .{ \"shirahama.wakayama.jp\", {} },\n        .{ \"taiji.wakayama.jp\", {} },\n        .{ \"tanabe.wakayama.jp\", {} },\n        .{ \"wakayama.wakayama.jp\", {} },\n        .{ \"yuasa.wakayama.jp\", {} },\n        .{ \"yura.wakayama.jp\", {} },\n        .{ \"asahi.yamagata.jp\", {} },\n        .{ \"funagata.yamagata.jp\", {} },\n        .{ \"higashine.yamagata.jp\", {} },\n        .{ \"iide.yamagata.jp\", {} },\n        .{ \"kahoku.yamagata.jp\", {} },\n        .{ \"kaminoyama.yamagata.jp\", {} },\n        .{ \"kaneyama.yamagata.jp\", {} },\n        .{ \"kawanishi.yamagata.jp\", {} },\n        .{ \"mamurogawa.yamagata.jp\", {} },\n        .{ \"mikawa.yamagata.jp\", {} },\n        .{ \"murayama.yamagata.jp\", {} },\n        .{ \"nagai.yamagata.jp\", {} },\n        .{ \"nakayama.yamagata.jp\", {} },\n        .{ \"nanyo.yamagata.jp\", {} },\n        .{ \"nishikawa.yamagata.jp\", {} },\n        .{ \"obanazawa.yamagata.jp\", {} },\n        .{ \"oe.yamagata.jp\", {} },\n        .{ \"oguni.yamagata.jp\", {} },\n        .{ \"ohkura.yamagata.jp\", {} },\n        .{ \"oishida.yamagata.jp\", {} },\n        .{ \"sagae.yamagata.jp\", {} },\n        .{ \"sakata.yamagata.jp\", {} },\n        .{ \"sakegawa.yamagata.jp\", {} },\n        .{ \"shinjo.yamagata.jp\", {} },\n        .{ \"shirataka.yamagata.jp\", {} },\n        .{ \"shonai.yamagata.jp\", {} },\n        .{ \"takahata.yamagata.jp\", {} },\n        .{ \"tendo.yamagata.jp\", {} },\n        .{ \"tozawa.yamagata.jp\", {} },\n        .{ \"tsuruoka.yamagata.jp\", {} },\n        .{ \"yamagata.yamagata.jp\", {} },\n        .{ \"yamanobe.yamagata.jp\", {} },\n        .{ \"yonezawa.yamagata.jp\", {} },\n        .{ \"yuza.yamagata.jp\", {} },\n        .{ \"abu.yamaguchi.jp\", {} },\n        .{ \"hagi.yamaguchi.jp\", {} },\n        .{ \"hikari.yamaguchi.jp\", {} },\n        .{ \"hofu.yamaguchi.jp\", {} },\n        .{ \"iwakuni.yamaguchi.jp\", {} },\n        .{ \"kudamatsu.yamaguchi.jp\", {} },\n        .{ \"mitou.yamaguchi.jp\", {} },\n        .{ \"nagato.yamaguchi.jp\", {} },\n        .{ \"oshima.yamaguchi.jp\", {} },\n        .{ \"shimonoseki.yamaguchi.jp\", {} },\n        .{ \"shunan.yamaguchi.jp\", {} },\n        .{ \"tabuse.yamaguchi.jp\", {} },\n        .{ \"tokuyama.yamaguchi.jp\", {} },\n        .{ \"toyota.yamaguchi.jp\", {} },\n        .{ \"ube.yamaguchi.jp\", {} },\n        .{ \"yuu.yamaguchi.jp\", {} },\n        .{ \"chuo.yamanashi.jp\", {} },\n        .{ \"doshi.yamanashi.jp\", {} },\n        .{ \"fuefuki.yamanashi.jp\", {} },\n        .{ \"fujikawa.yamanashi.jp\", {} },\n        .{ \"fujikawaguchiko.yamanashi.jp\", {} },\n        .{ \"fujiyoshida.yamanashi.jp\", {} },\n        .{ \"hayakawa.yamanashi.jp\", {} },\n        .{ \"hokuto.yamanashi.jp\", {} },\n        .{ \"ichikawamisato.yamanashi.jp\", {} },\n        .{ \"kai.yamanashi.jp\", {} },\n        .{ \"kofu.yamanashi.jp\", {} },\n        .{ \"koshu.yamanashi.jp\", {} },\n        .{ \"kosuge.yamanashi.jp\", {} },\n        .{ \"minami-alps.yamanashi.jp\", {} },\n        .{ \"minobu.yamanashi.jp\", {} },\n        .{ \"nakamichi.yamanashi.jp\", {} },\n        .{ \"nanbu.yamanashi.jp\", {} },\n        .{ \"narusawa.yamanashi.jp\", {} },\n        .{ \"nirasaki.yamanashi.jp\", {} },\n        .{ \"nishikatsura.yamanashi.jp\", {} },\n        .{ \"oshino.yamanashi.jp\", {} },\n        .{ \"otsuki.yamanashi.jp\", {} },\n        .{ \"showa.yamanashi.jp\", {} },\n        .{ \"tabayama.yamanashi.jp\", {} },\n        .{ \"tsuru.yamanashi.jp\", {} },\n        .{ \"uenohara.yamanashi.jp\", {} },\n        .{ \"yamanakako.yamanashi.jp\", {} },\n        .{ \"yamanashi.yamanashi.jp\", {} },\n        .{ \"ke\", {} },\n        .{ \"ac.ke\", {} },\n        .{ \"co.ke\", {} },\n        .{ \"go.ke\", {} },\n        .{ \"info.ke\", {} },\n        .{ \"me.ke\", {} },\n        .{ \"mobi.ke\", {} },\n        .{ \"ne.ke\", {} },\n        .{ \"or.ke\", {} },\n        .{ \"sc.ke\", {} },\n        .{ \"kg\", {} },\n        .{ \"com.kg\", {} },\n        .{ \"edu.kg\", {} },\n        .{ \"gov.kg\", {} },\n        .{ \"mil.kg\", {} },\n        .{ \"net.kg\", {} },\n        .{ \"org.kg\", {} },\n        .{ \"*.kh\", {} },\n        .{ \"ki\", {} },\n        .{ \"biz.ki\", {} },\n        .{ \"com.ki\", {} },\n        .{ \"edu.ki\", {} },\n        .{ \"gov.ki\", {} },\n        .{ \"info.ki\", {} },\n        .{ \"net.ki\", {} },\n        .{ \"org.ki\", {} },\n        .{ \"km\", {} },\n        .{ \"ass.km\", {} },\n        .{ \"com.km\", {} },\n        .{ \"edu.km\", {} },\n        .{ \"gov.km\", {} },\n        .{ \"mil.km\", {} },\n        .{ \"nom.km\", {} },\n        .{ \"org.km\", {} },\n        .{ \"prd.km\", {} },\n        .{ \"tm.km\", {} },\n        .{ \"asso.km\", {} },\n        .{ \"coop.km\", {} },\n        .{ \"gouv.km\", {} },\n        .{ \"medecin.km\", {} },\n        .{ \"notaires.km\", {} },\n        .{ \"pharmaciens.km\", {} },\n        .{ \"presse.km\", {} },\n        .{ \"veterinaire.km\", {} },\n        .{ \"kn\", {} },\n        .{ \"edu.kn\", {} },\n        .{ \"gov.kn\", {} },\n        .{ \"net.kn\", {} },\n        .{ \"org.kn\", {} },\n        .{ \"kp\", {} },\n        .{ \"com.kp\", {} },\n        .{ \"edu.kp\", {} },\n        .{ \"gov.kp\", {} },\n        .{ \"org.kp\", {} },\n        .{ \"rep.kp\", {} },\n        .{ \"tra.kp\", {} },\n        .{ \"kr\", {} },\n        .{ \"ac.kr\", {} },\n        .{ \"ai.kr\", {} },\n        .{ \"co.kr\", {} },\n        .{ \"es.kr\", {} },\n        .{ \"go.kr\", {} },\n        .{ \"hs.kr\", {} },\n        .{ \"io.kr\", {} },\n        .{ \"it.kr\", {} },\n        .{ \"kg.kr\", {} },\n        .{ \"me.kr\", {} },\n        .{ \"mil.kr\", {} },\n        .{ \"ms.kr\", {} },\n        .{ \"ne.kr\", {} },\n        .{ \"or.kr\", {} },\n        .{ \"pe.kr\", {} },\n        .{ \"re.kr\", {} },\n        .{ \"sc.kr\", {} },\n        .{ \"busan.kr\", {} },\n        .{ \"chungbuk.kr\", {} },\n        .{ \"chungnam.kr\", {} },\n        .{ \"daegu.kr\", {} },\n        .{ \"daejeon.kr\", {} },\n        .{ \"gangwon.kr\", {} },\n        .{ \"gwangju.kr\", {} },\n        .{ \"gyeongbuk.kr\", {} },\n        .{ \"gyeonggi.kr\", {} },\n        .{ \"gyeongnam.kr\", {} },\n        .{ \"incheon.kr\", {} },\n        .{ \"jeju.kr\", {} },\n        .{ \"jeonbuk.kr\", {} },\n        .{ \"jeonnam.kr\", {} },\n        .{ \"seoul.kr\", {} },\n        .{ \"ulsan.kr\", {} },\n        .{ \"kw\", {} },\n        .{ \"com.kw\", {} },\n        .{ \"edu.kw\", {} },\n        .{ \"emb.kw\", {} },\n        .{ \"gov.kw\", {} },\n        .{ \"ind.kw\", {} },\n        .{ \"net.kw\", {} },\n        .{ \"org.kw\", {} },\n        .{ \"ky\", {} },\n        .{ \"com.ky\", {} },\n        .{ \"edu.ky\", {} },\n        .{ \"net.ky\", {} },\n        .{ \"org.ky\", {} },\n        .{ \"kz\", {} },\n        .{ \"com.kz\", {} },\n        .{ \"edu.kz\", {} },\n        .{ \"gov.kz\", {} },\n        .{ \"mil.kz\", {} },\n        .{ \"net.kz\", {} },\n        .{ \"org.kz\", {} },\n        .{ \"la\", {} },\n        .{ \"com.la\", {} },\n        .{ \"edu.la\", {} },\n        .{ \"gov.la\", {} },\n        .{ \"info.la\", {} },\n        .{ \"int.la\", {} },\n        .{ \"net.la\", {} },\n        .{ \"org.la\", {} },\n        .{ \"per.la\", {} },\n        .{ \"lb\", {} },\n        .{ \"com.lb\", {} },\n        .{ \"edu.lb\", {} },\n        .{ \"gov.lb\", {} },\n        .{ \"net.lb\", {} },\n        .{ \"org.lb\", {} },\n        .{ \"lc\", {} },\n        .{ \"co.lc\", {} },\n        .{ \"com.lc\", {} },\n        .{ \"edu.lc\", {} },\n        .{ \"gov.lc\", {} },\n        .{ \"net.lc\", {} },\n        .{ \"org.lc\", {} },\n        .{ \"li\", {} },\n        .{ \"lk\", {} },\n        .{ \"ac.lk\", {} },\n        .{ \"assn.lk\", {} },\n        .{ \"com.lk\", {} },\n        .{ \"edu.lk\", {} },\n        .{ \"gov.lk\", {} },\n        .{ \"grp.lk\", {} },\n        .{ \"hotel.lk\", {} },\n        .{ \"int.lk\", {} },\n        .{ \"ltd.lk\", {} },\n        .{ \"net.lk\", {} },\n        .{ \"ngo.lk\", {} },\n        .{ \"org.lk\", {} },\n        .{ \"sch.lk\", {} },\n        .{ \"soc.lk\", {} },\n        .{ \"web.lk\", {} },\n        .{ \"lr\", {} },\n        .{ \"com.lr\", {} },\n        .{ \"edu.lr\", {} },\n        .{ \"gov.lr\", {} },\n        .{ \"net.lr\", {} },\n        .{ \"org.lr\", {} },\n        .{ \"ls\", {} },\n        .{ \"ac.ls\", {} },\n        .{ \"biz.ls\", {} },\n        .{ \"co.ls\", {} },\n        .{ \"edu.ls\", {} },\n        .{ \"gov.ls\", {} },\n        .{ \"info.ls\", {} },\n        .{ \"net.ls\", {} },\n        .{ \"org.ls\", {} },\n        .{ \"sc.ls\", {} },\n        .{ \"lt\", {} },\n        .{ \"gov.lt\", {} },\n        .{ \"lu\", {} },\n        .{ \"lv\", {} },\n        .{ \"asn.lv\", {} },\n        .{ \"com.lv\", {} },\n        .{ \"conf.lv\", {} },\n        .{ \"edu.lv\", {} },\n        .{ \"gov.lv\", {} },\n        .{ \"id.lv\", {} },\n        .{ \"mil.lv\", {} },\n        .{ \"net.lv\", {} },\n        .{ \"org.lv\", {} },\n        .{ \"ly\", {} },\n        .{ \"com.ly\", {} },\n        .{ \"edu.ly\", {} },\n        .{ \"gov.ly\", {} },\n        .{ \"id.ly\", {} },\n        .{ \"med.ly\", {} },\n        .{ \"net.ly\", {} },\n        .{ \"org.ly\", {} },\n        .{ \"plc.ly\", {} },\n        .{ \"sch.ly\", {} },\n        .{ \"ma\", {} },\n        .{ \"ac.ma\", {} },\n        .{ \"co.ma\", {} },\n        .{ \"gov.ma\", {} },\n        .{ \"net.ma\", {} },\n        .{ \"org.ma\", {} },\n        .{ \"press.ma\", {} },\n        .{ \"mc\", {} },\n        .{ \"asso.mc\", {} },\n        .{ \"tm.mc\", {} },\n        .{ \"md\", {} },\n        .{ \"me\", {} },\n        .{ \"ac.me\", {} },\n        .{ \"co.me\", {} },\n        .{ \"edu.me\", {} },\n        .{ \"gov.me\", {} },\n        .{ \"its.me\", {} },\n        .{ \"net.me\", {} },\n        .{ \"org.me\", {} },\n        .{ \"priv.me\", {} },\n        .{ \"mg\", {} },\n        .{ \"co.mg\", {} },\n        .{ \"com.mg\", {} },\n        .{ \"edu.mg\", {} },\n        .{ \"gov.mg\", {} },\n        .{ \"mil.mg\", {} },\n        .{ \"nom.mg\", {} },\n        .{ \"org.mg\", {} },\n        .{ \"prd.mg\", {} },\n        .{ \"mh\", {} },\n        .{ \"mil\", {} },\n        .{ \"mk\", {} },\n        .{ \"com.mk\", {} },\n        .{ \"edu.mk\", {} },\n        .{ \"gov.mk\", {} },\n        .{ \"inf.mk\", {} },\n        .{ \"name.mk\", {} },\n        .{ \"net.mk\", {} },\n        .{ \"org.mk\", {} },\n        .{ \"ml\", {} },\n        .{ \"ac.ml\", {} },\n        .{ \"art.ml\", {} },\n        .{ \"asso.ml\", {} },\n        .{ \"com.ml\", {} },\n        .{ \"edu.ml\", {} },\n        .{ \"gouv.ml\", {} },\n        .{ \"gov.ml\", {} },\n        .{ \"info.ml\", {} },\n        .{ \"inst.ml\", {} },\n        .{ \"net.ml\", {} },\n        .{ \"org.ml\", {} },\n        .{ \"pr.ml\", {} },\n        .{ \"presse.ml\", {} },\n        .{ \"*.mm\", {} },\n        .{ \"mn\", {} },\n        .{ \"edu.mn\", {} },\n        .{ \"gov.mn\", {} },\n        .{ \"org.mn\", {} },\n        .{ \"mo\", {} },\n        .{ \"com.mo\", {} },\n        .{ \"edu.mo\", {} },\n        .{ \"gov.mo\", {} },\n        .{ \"net.mo\", {} },\n        .{ \"org.mo\", {} },\n        .{ \"mobi\", {} },\n        .{ \"mp\", {} },\n        .{ \"mq\", {} },\n        .{ \"mr\", {} },\n        .{ \"gov.mr\", {} },\n        .{ \"ms\", {} },\n        .{ \"com.ms\", {} },\n        .{ \"edu.ms\", {} },\n        .{ \"gov.ms\", {} },\n        .{ \"net.ms\", {} },\n        .{ \"org.ms\", {} },\n        .{ \"mt\", {} },\n        .{ \"com.mt\", {} },\n        .{ \"edu.mt\", {} },\n        .{ \"net.mt\", {} },\n        .{ \"org.mt\", {} },\n        .{ \"mu\", {} },\n        .{ \"ac.mu\", {} },\n        .{ \"co.mu\", {} },\n        .{ \"com.mu\", {} },\n        .{ \"gov.mu\", {} },\n        .{ \"net.mu\", {} },\n        .{ \"or.mu\", {} },\n        .{ \"org.mu\", {} },\n        .{ \"museum\", {} },\n        .{ \"mv\", {} },\n        .{ \"aero.mv\", {} },\n        .{ \"biz.mv\", {} },\n        .{ \"com.mv\", {} },\n        .{ \"coop.mv\", {} },\n        .{ \"edu.mv\", {} },\n        .{ \"gov.mv\", {} },\n        .{ \"info.mv\", {} },\n        .{ \"int.mv\", {} },\n        .{ \"mil.mv\", {} },\n        .{ \"museum.mv\", {} },\n        .{ \"name.mv\", {} },\n        .{ \"net.mv\", {} },\n        .{ \"org.mv\", {} },\n        .{ \"pro.mv\", {} },\n        .{ \"mw\", {} },\n        .{ \"ac.mw\", {} },\n        .{ \"biz.mw\", {} },\n        .{ \"co.mw\", {} },\n        .{ \"com.mw\", {} },\n        .{ \"coop.mw\", {} },\n        .{ \"edu.mw\", {} },\n        .{ \"gov.mw\", {} },\n        .{ \"int.mw\", {} },\n        .{ \"net.mw\", {} },\n        .{ \"org.mw\", {} },\n        .{ \"mx\", {} },\n        .{ \"com.mx\", {} },\n        .{ \"edu.mx\", {} },\n        .{ \"gob.mx\", {} },\n        .{ \"net.mx\", {} },\n        .{ \"org.mx\", {} },\n        .{ \"my\", {} },\n        .{ \"biz.my\", {} },\n        .{ \"com.my\", {} },\n        .{ \"edu.my\", {} },\n        .{ \"gov.my\", {} },\n        .{ \"mil.my\", {} },\n        .{ \"name.my\", {} },\n        .{ \"net.my\", {} },\n        .{ \"org.my\", {} },\n        .{ \"mz\", {} },\n        .{ \"ac.mz\", {} },\n        .{ \"adv.mz\", {} },\n        .{ \"co.mz\", {} },\n        .{ \"edu.mz\", {} },\n        .{ \"gov.mz\", {} },\n        .{ \"mil.mz\", {} },\n        .{ \"net.mz\", {} },\n        .{ \"org.mz\", {} },\n        .{ \"na\", {} },\n        .{ \"alt.na\", {} },\n        .{ \"co.na\", {} },\n        .{ \"com.na\", {} },\n        .{ \"gov.na\", {} },\n        .{ \"net.na\", {} },\n        .{ \"org.na\", {} },\n        .{ \"name\", {} },\n        .{ \"nc\", {} },\n        .{ \"asso.nc\", {} },\n        .{ \"nom.nc\", {} },\n        .{ \"ne\", {} },\n        .{ \"net\", {} },\n        .{ \"nf\", {} },\n        .{ \"arts.nf\", {} },\n        .{ \"com.nf\", {} },\n        .{ \"firm.nf\", {} },\n        .{ \"info.nf\", {} },\n        .{ \"net.nf\", {} },\n        .{ \"other.nf\", {} },\n        .{ \"per.nf\", {} },\n        .{ \"rec.nf\", {} },\n        .{ \"store.nf\", {} },\n        .{ \"web.nf\", {} },\n        .{ \"ng\", {} },\n        .{ \"com.ng\", {} },\n        .{ \"edu.ng\", {} },\n        .{ \"gov.ng\", {} },\n        .{ \"i.ng\", {} },\n        .{ \"mil.ng\", {} },\n        .{ \"mobi.ng\", {} },\n        .{ \"name.ng\", {} },\n        .{ \"net.ng\", {} },\n        .{ \"org.ng\", {} },\n        .{ \"sch.ng\", {} },\n        .{ \"ni\", {} },\n        .{ \"ac.ni\", {} },\n        .{ \"biz.ni\", {} },\n        .{ \"co.ni\", {} },\n        .{ \"com.ni\", {} },\n        .{ \"edu.ni\", {} },\n        .{ \"gob.ni\", {} },\n        .{ \"in.ni\", {} },\n        .{ \"info.ni\", {} },\n        .{ \"int.ni\", {} },\n        .{ \"mil.ni\", {} },\n        .{ \"net.ni\", {} },\n        .{ \"nom.ni\", {} },\n        .{ \"org.ni\", {} },\n        .{ \"web.ni\", {} },\n        .{ \"nl\", {} },\n        .{ \"no\", {} },\n        .{ \"fhs.no\", {} },\n        .{ \"folkebibl.no\", {} },\n        .{ \"fylkesbibl.no\", {} },\n        .{ \"idrett.no\", {} },\n        .{ \"museum.no\", {} },\n        .{ \"priv.no\", {} },\n        .{ \"vgs.no\", {} },\n        .{ \"dep.no\", {} },\n        .{ \"herad.no\", {} },\n        .{ \"kommune.no\", {} },\n        .{ \"mil.no\", {} },\n        .{ \"stat.no\", {} },\n        .{ \"aa.no\", {} },\n        .{ \"ah.no\", {} },\n        .{ \"bu.no\", {} },\n        .{ \"fm.no\", {} },\n        .{ \"hl.no\", {} },\n        .{ \"hm.no\", {} },\n        .{ \"jan-mayen.no\", {} },\n        .{ \"mr.no\", {} },\n        .{ \"nl.no\", {} },\n        .{ \"nt.no\", {} },\n        .{ \"of.no\", {} },\n        .{ \"ol.no\", {} },\n        .{ \"oslo.no\", {} },\n        .{ \"rl.no\", {} },\n        .{ \"sf.no\", {} },\n        .{ \"st.no\", {} },\n        .{ \"svalbard.no\", {} },\n        .{ \"tm.no\", {} },\n        .{ \"tr.no\", {} },\n        .{ \"va.no\", {} },\n        .{ \"vf.no\", {} },\n        .{ \"gs.aa.no\", {} },\n        .{ \"gs.ah.no\", {} },\n        .{ \"gs.bu.no\", {} },\n        .{ \"gs.fm.no\", {} },\n        .{ \"gs.hl.no\", {} },\n        .{ \"gs.hm.no\", {} },\n        .{ \"gs.jan-mayen.no\", {} },\n        .{ \"gs.mr.no\", {} },\n        .{ \"gs.nl.no\", {} },\n        .{ \"gs.nt.no\", {} },\n        .{ \"gs.of.no\", {} },\n        .{ \"gs.ol.no\", {} },\n        .{ \"gs.oslo.no\", {} },\n        .{ \"gs.rl.no\", {} },\n        .{ \"gs.sf.no\", {} },\n        .{ \"gs.st.no\", {} },\n        .{ \"gs.svalbard.no\", {} },\n        .{ \"gs.tm.no\", {} },\n        .{ \"gs.tr.no\", {} },\n        .{ \"gs.va.no\", {} },\n        .{ \"gs.vf.no\", {} },\n        .{ \"akrehamn.no\", {} },\n        .{ \"åkrehamn.no\", {} },\n        .{ \"algard.no\", {} },\n        .{ \"ålgård.no\", {} },\n        .{ \"arna.no\", {} },\n        .{ \"bronnoysund.no\", {} },\n        .{ \"brønnøysund.no\", {} },\n        .{ \"brumunddal.no\", {} },\n        .{ \"bryne.no\", {} },\n        .{ \"drobak.no\", {} },\n        .{ \"drøbak.no\", {} },\n        .{ \"egersund.no\", {} },\n        .{ \"fetsund.no\", {} },\n        .{ \"floro.no\", {} },\n        .{ \"florø.no\", {} },\n        .{ \"fredrikstad.no\", {} },\n        .{ \"hokksund.no\", {} },\n        .{ \"honefoss.no\", {} },\n        .{ \"hønefoss.no\", {} },\n        .{ \"jessheim.no\", {} },\n        .{ \"jorpeland.no\", {} },\n        .{ \"jørpeland.no\", {} },\n        .{ \"kirkenes.no\", {} },\n        .{ \"kopervik.no\", {} },\n        .{ \"krokstadelva.no\", {} },\n        .{ \"langevag.no\", {} },\n        .{ \"langevåg.no\", {} },\n        .{ \"leirvik.no\", {} },\n        .{ \"mjondalen.no\", {} },\n        .{ \"mjøndalen.no\", {} },\n        .{ \"mo-i-rana.no\", {} },\n        .{ \"mosjoen.no\", {} },\n        .{ \"mosjøen.no\", {} },\n        .{ \"nesoddtangen.no\", {} },\n        .{ \"orkanger.no\", {} },\n        .{ \"osoyro.no\", {} },\n        .{ \"osøyro.no\", {} },\n        .{ \"raholt.no\", {} },\n        .{ \"råholt.no\", {} },\n        .{ \"sandnessjoen.no\", {} },\n        .{ \"sandnessjøen.no\", {} },\n        .{ \"skedsmokorset.no\", {} },\n        .{ \"slattum.no\", {} },\n        .{ \"spjelkavik.no\", {} },\n        .{ \"stathelle.no\", {} },\n        .{ \"stavern.no\", {} },\n        .{ \"stjordalshalsen.no\", {} },\n        .{ \"stjørdalshalsen.no\", {} },\n        .{ \"tananger.no\", {} },\n        .{ \"tranby.no\", {} },\n        .{ \"vossevangen.no\", {} },\n        .{ \"aarborte.no\", {} },\n        .{ \"aejrie.no\", {} },\n        .{ \"afjord.no\", {} },\n        .{ \"åfjord.no\", {} },\n        .{ \"agdenes.no\", {} },\n        .{ \"nes.akershus.no\", {} },\n        .{ \"aknoluokta.no\", {} },\n        .{ \"ákŋoluokta.no\", {} },\n        .{ \"al.no\", {} },\n        .{ \"ål.no\", {} },\n        .{ \"alaheadju.no\", {} },\n        .{ \"álaheadju.no\", {} },\n        .{ \"alesund.no\", {} },\n        .{ \"ålesund.no\", {} },\n        .{ \"alstahaug.no\", {} },\n        .{ \"alta.no\", {} },\n        .{ \"áltá.no\", {} },\n        .{ \"alvdal.no\", {} },\n        .{ \"amli.no\", {} },\n        .{ \"åmli.no\", {} },\n        .{ \"amot.no\", {} },\n        .{ \"åmot.no\", {} },\n        .{ \"andasuolo.no\", {} },\n        .{ \"andebu.no\", {} },\n        .{ \"andoy.no\", {} },\n        .{ \"andøy.no\", {} },\n        .{ \"ardal.no\", {} },\n        .{ \"årdal.no\", {} },\n        .{ \"aremark.no\", {} },\n        .{ \"arendal.no\", {} },\n        .{ \"ås.no\", {} },\n        .{ \"aseral.no\", {} },\n        .{ \"åseral.no\", {} },\n        .{ \"asker.no\", {} },\n        .{ \"askim.no\", {} },\n        .{ \"askoy.no\", {} },\n        .{ \"askøy.no\", {} },\n        .{ \"askvoll.no\", {} },\n        .{ \"asnes.no\", {} },\n        .{ \"åsnes.no\", {} },\n        .{ \"audnedaln.no\", {} },\n        .{ \"aukra.no\", {} },\n        .{ \"aure.no\", {} },\n        .{ \"aurland.no\", {} },\n        .{ \"aurskog-holand.no\", {} },\n        .{ \"aurskog-høland.no\", {} },\n        .{ \"austevoll.no\", {} },\n        .{ \"austrheim.no\", {} },\n        .{ \"averoy.no\", {} },\n        .{ \"averøy.no\", {} },\n        .{ \"badaddja.no\", {} },\n        .{ \"bådåddjå.no\", {} },\n        .{ \"bærum.no\", {} },\n        .{ \"bahcavuotna.no\", {} },\n        .{ \"báhcavuotna.no\", {} },\n        .{ \"bahccavuotna.no\", {} },\n        .{ \"báhccavuotna.no\", {} },\n        .{ \"baidar.no\", {} },\n        .{ \"báidár.no\", {} },\n        .{ \"bajddar.no\", {} },\n        .{ \"bájddar.no\", {} },\n        .{ \"balat.no\", {} },\n        .{ \"bálát.no\", {} },\n        .{ \"balestrand.no\", {} },\n        .{ \"ballangen.no\", {} },\n        .{ \"balsfjord.no\", {} },\n        .{ \"bamble.no\", {} },\n        .{ \"bardu.no\", {} },\n        .{ \"barum.no\", {} },\n        .{ \"batsfjord.no\", {} },\n        .{ \"båtsfjord.no\", {} },\n        .{ \"bearalvahki.no\", {} },\n        .{ \"bearalváhki.no\", {} },\n        .{ \"beardu.no\", {} },\n        .{ \"beiarn.no\", {} },\n        .{ \"berg.no\", {} },\n        .{ \"bergen.no\", {} },\n        .{ \"berlevag.no\", {} },\n        .{ \"berlevåg.no\", {} },\n        .{ \"bievat.no\", {} },\n        .{ \"bievát.no\", {} },\n        .{ \"bindal.no\", {} },\n        .{ \"birkenes.no\", {} },\n        .{ \"bjerkreim.no\", {} },\n        .{ \"bjugn.no\", {} },\n        .{ \"bodo.no\", {} },\n        .{ \"bodø.no\", {} },\n        .{ \"bokn.no\", {} },\n        .{ \"bomlo.no\", {} },\n        .{ \"bømlo.no\", {} },\n        .{ \"bremanger.no\", {} },\n        .{ \"bronnoy.no\", {} },\n        .{ \"brønnøy.no\", {} },\n        .{ \"budejju.no\", {} },\n        .{ \"nes.buskerud.no\", {} },\n        .{ \"bygland.no\", {} },\n        .{ \"bykle.no\", {} },\n        .{ \"cahcesuolo.no\", {} },\n        .{ \"čáhcesuolo.no\", {} },\n        .{ \"davvenjarga.no\", {} },\n        .{ \"davvenjárga.no\", {} },\n        .{ \"davvesiida.no\", {} },\n        .{ \"deatnu.no\", {} },\n        .{ \"dielddanuorri.no\", {} },\n        .{ \"divtasvuodna.no\", {} },\n        .{ \"divttasvuotna.no\", {} },\n        .{ \"donna.no\", {} },\n        .{ \"dønna.no\", {} },\n        .{ \"dovre.no\", {} },\n        .{ \"drammen.no\", {} },\n        .{ \"drangedal.no\", {} },\n        .{ \"dyroy.no\", {} },\n        .{ \"dyrøy.no\", {} },\n        .{ \"eid.no\", {} },\n        .{ \"eidfjord.no\", {} },\n        .{ \"eidsberg.no\", {} },\n        .{ \"eidskog.no\", {} },\n        .{ \"eidsvoll.no\", {} },\n        .{ \"eigersund.no\", {} },\n        .{ \"elverum.no\", {} },\n        .{ \"enebakk.no\", {} },\n        .{ \"engerdal.no\", {} },\n        .{ \"etne.no\", {} },\n        .{ \"etnedal.no\", {} },\n        .{ \"evenassi.no\", {} },\n        .{ \"evenášši.no\", {} },\n        .{ \"evenes.no\", {} },\n        .{ \"evje-og-hornnes.no\", {} },\n        .{ \"farsund.no\", {} },\n        .{ \"fauske.no\", {} },\n        .{ \"fedje.no\", {} },\n        .{ \"fet.no\", {} },\n        .{ \"finnoy.no\", {} },\n        .{ \"finnøy.no\", {} },\n        .{ \"fitjar.no\", {} },\n        .{ \"fjaler.no\", {} },\n        .{ \"fjell.no\", {} },\n        .{ \"fla.no\", {} },\n        .{ \"flå.no\", {} },\n        .{ \"flakstad.no\", {} },\n        .{ \"flatanger.no\", {} },\n        .{ \"flekkefjord.no\", {} },\n        .{ \"flesberg.no\", {} },\n        .{ \"flora.no\", {} },\n        .{ \"folldal.no\", {} },\n        .{ \"forde.no\", {} },\n        .{ \"førde.no\", {} },\n        .{ \"forsand.no\", {} },\n        .{ \"fosnes.no\", {} },\n        .{ \"fræna.no\", {} },\n        .{ \"frana.no\", {} },\n        .{ \"frei.no\", {} },\n        .{ \"frogn.no\", {} },\n        .{ \"froland.no\", {} },\n        .{ \"frosta.no\", {} },\n        .{ \"froya.no\", {} },\n        .{ \"frøya.no\", {} },\n        .{ \"fuoisku.no\", {} },\n        .{ \"fuossko.no\", {} },\n        .{ \"fusa.no\", {} },\n        .{ \"fyresdal.no\", {} },\n        .{ \"gaivuotna.no\", {} },\n        .{ \"gáivuotna.no\", {} },\n        .{ \"galsa.no\", {} },\n        .{ \"gálsá.no\", {} },\n        .{ \"gamvik.no\", {} },\n        .{ \"gangaviika.no\", {} },\n        .{ \"gáŋgaviika.no\", {} },\n        .{ \"gaular.no\", {} },\n        .{ \"gausdal.no\", {} },\n        .{ \"giehtavuoatna.no\", {} },\n        .{ \"gildeskal.no\", {} },\n        .{ \"gildeskål.no\", {} },\n        .{ \"giske.no\", {} },\n        .{ \"gjemnes.no\", {} },\n        .{ \"gjerdrum.no\", {} },\n        .{ \"gjerstad.no\", {} },\n        .{ \"gjesdal.no\", {} },\n        .{ \"gjovik.no\", {} },\n        .{ \"gjøvik.no\", {} },\n        .{ \"gloppen.no\", {} },\n        .{ \"gol.no\", {} },\n        .{ \"gran.no\", {} },\n        .{ \"grane.no\", {} },\n        .{ \"granvin.no\", {} },\n        .{ \"gratangen.no\", {} },\n        .{ \"grimstad.no\", {} },\n        .{ \"grong.no\", {} },\n        .{ \"grue.no\", {} },\n        .{ \"gulen.no\", {} },\n        .{ \"guovdageaidnu.no\", {} },\n        .{ \"ha.no\", {} },\n        .{ \"hå.no\", {} },\n        .{ \"habmer.no\", {} },\n        .{ \"hábmer.no\", {} },\n        .{ \"hadsel.no\", {} },\n        .{ \"hægebostad.no\", {} },\n        .{ \"hagebostad.no\", {} },\n        .{ \"halden.no\", {} },\n        .{ \"halsa.no\", {} },\n        .{ \"hamar.no\", {} },\n        .{ \"hamaroy.no\", {} },\n        .{ \"hammarfeasta.no\", {} },\n        .{ \"hámmárfeasta.no\", {} },\n        .{ \"hammerfest.no\", {} },\n        .{ \"hapmir.no\", {} },\n        .{ \"hápmir.no\", {} },\n        .{ \"haram.no\", {} },\n        .{ \"hareid.no\", {} },\n        .{ \"harstad.no\", {} },\n        .{ \"hasvik.no\", {} },\n        .{ \"hattfjelldal.no\", {} },\n        .{ \"haugesund.no\", {} },\n        .{ \"os.hedmark.no\", {} },\n        .{ \"valer.hedmark.no\", {} },\n        .{ \"våler.hedmark.no\", {} },\n        .{ \"hemne.no\", {} },\n        .{ \"hemnes.no\", {} },\n        .{ \"hemsedal.no\", {} },\n        .{ \"hitra.no\", {} },\n        .{ \"hjartdal.no\", {} },\n        .{ \"hjelmeland.no\", {} },\n        .{ \"hobol.no\", {} },\n        .{ \"hobøl.no\", {} },\n        .{ \"hof.no\", {} },\n        .{ \"hol.no\", {} },\n        .{ \"hole.no\", {} },\n        .{ \"holmestrand.no\", {} },\n        .{ \"holtalen.no\", {} },\n        .{ \"holtålen.no\", {} },\n        .{ \"os.hordaland.no\", {} },\n        .{ \"hornindal.no\", {} },\n        .{ \"horten.no\", {} },\n        .{ \"hoyanger.no\", {} },\n        .{ \"høyanger.no\", {} },\n        .{ \"hoylandet.no\", {} },\n        .{ \"høylandet.no\", {} },\n        .{ \"hurdal.no\", {} },\n        .{ \"hurum.no\", {} },\n        .{ \"hvaler.no\", {} },\n        .{ \"hyllestad.no\", {} },\n        .{ \"ibestad.no\", {} },\n        .{ \"inderoy.no\", {} },\n        .{ \"inderøy.no\", {} },\n        .{ \"iveland.no\", {} },\n        .{ \"ivgu.no\", {} },\n        .{ \"jevnaker.no\", {} },\n        .{ \"jolster.no\", {} },\n        .{ \"jølster.no\", {} },\n        .{ \"jondal.no\", {} },\n        .{ \"kafjord.no\", {} },\n        .{ \"kåfjord.no\", {} },\n        .{ \"karasjohka.no\", {} },\n        .{ \"kárášjohka.no\", {} },\n        .{ \"karasjok.no\", {} },\n        .{ \"karlsoy.no\", {} },\n        .{ \"karmoy.no\", {} },\n        .{ \"karmøy.no\", {} },\n        .{ \"kautokeino.no\", {} },\n        .{ \"klabu.no\", {} },\n        .{ \"klæbu.no\", {} },\n        .{ \"klepp.no\", {} },\n        .{ \"kongsberg.no\", {} },\n        .{ \"kongsvinger.no\", {} },\n        .{ \"kraanghke.no\", {} },\n        .{ \"kråanghke.no\", {} },\n        .{ \"kragero.no\", {} },\n        .{ \"kragerø.no\", {} },\n        .{ \"kristiansand.no\", {} },\n        .{ \"kristiansund.no\", {} },\n        .{ \"krodsherad.no\", {} },\n        .{ \"krødsherad.no\", {} },\n        .{ \"kvæfjord.no\", {} },\n        .{ \"kvænangen.no\", {} },\n        .{ \"kvafjord.no\", {} },\n        .{ \"kvalsund.no\", {} },\n        .{ \"kvam.no\", {} },\n        .{ \"kvanangen.no\", {} },\n        .{ \"kvinesdal.no\", {} },\n        .{ \"kvinnherad.no\", {} },\n        .{ \"kviteseid.no\", {} },\n        .{ \"kvitsoy.no\", {} },\n        .{ \"kvitsøy.no\", {} },\n        .{ \"laakesvuemie.no\", {} },\n        .{ \"lærdal.no\", {} },\n        .{ \"lahppi.no\", {} },\n        .{ \"láhppi.no\", {} },\n        .{ \"lardal.no\", {} },\n        .{ \"larvik.no\", {} },\n        .{ \"lavagis.no\", {} },\n        .{ \"lavangen.no\", {} },\n        .{ \"leangaviika.no\", {} },\n        .{ \"leaŋgaviika.no\", {} },\n        .{ \"lebesby.no\", {} },\n        .{ \"leikanger.no\", {} },\n        .{ \"leirfjord.no\", {} },\n        .{ \"leka.no\", {} },\n        .{ \"leksvik.no\", {} },\n        .{ \"lenvik.no\", {} },\n        .{ \"lerdal.no\", {} },\n        .{ \"lesja.no\", {} },\n        .{ \"levanger.no\", {} },\n        .{ \"lier.no\", {} },\n        .{ \"lierne.no\", {} },\n        .{ \"lillehammer.no\", {} },\n        .{ \"lillesand.no\", {} },\n        .{ \"lindas.no\", {} },\n        .{ \"lindås.no\", {} },\n        .{ \"lindesnes.no\", {} },\n        .{ \"loabat.no\", {} },\n        .{ \"loabát.no\", {} },\n        .{ \"lodingen.no\", {} },\n        .{ \"lødingen.no\", {} },\n        .{ \"lom.no\", {} },\n        .{ \"loppa.no\", {} },\n        .{ \"lorenskog.no\", {} },\n        .{ \"lørenskog.no\", {} },\n        .{ \"loten.no\", {} },\n        .{ \"løten.no\", {} },\n        .{ \"lund.no\", {} },\n        .{ \"lunner.no\", {} },\n        .{ \"luroy.no\", {} },\n        .{ \"lurøy.no\", {} },\n        .{ \"luster.no\", {} },\n        .{ \"lyngdal.no\", {} },\n        .{ \"lyngen.no\", {} },\n        .{ \"malatvuopmi.no\", {} },\n        .{ \"málatvuopmi.no\", {} },\n        .{ \"malselv.no\", {} },\n        .{ \"målselv.no\", {} },\n        .{ \"malvik.no\", {} },\n        .{ \"mandal.no\", {} },\n        .{ \"marker.no\", {} },\n        .{ \"marnardal.no\", {} },\n        .{ \"masfjorden.no\", {} },\n        .{ \"masoy.no\", {} },\n        .{ \"måsøy.no\", {} },\n        .{ \"matta-varjjat.no\", {} },\n        .{ \"mátta-várjjat.no\", {} },\n        .{ \"meland.no\", {} },\n        .{ \"meldal.no\", {} },\n        .{ \"melhus.no\", {} },\n        .{ \"meloy.no\", {} },\n        .{ \"meløy.no\", {} },\n        .{ \"meraker.no\", {} },\n        .{ \"meråker.no\", {} },\n        .{ \"midsund.no\", {} },\n        .{ \"midtre-gauldal.no\", {} },\n        .{ \"moareke.no\", {} },\n        .{ \"moåreke.no\", {} },\n        .{ \"modalen.no\", {} },\n        .{ \"modum.no\", {} },\n        .{ \"molde.no\", {} },\n        .{ \"heroy.more-og-romsdal.no\", {} },\n        .{ \"sande.more-og-romsdal.no\", {} },\n        .{ \"herøy.møre-og-romsdal.no\", {} },\n        .{ \"sande.møre-og-romsdal.no\", {} },\n        .{ \"moskenes.no\", {} },\n        .{ \"moss.no\", {} },\n        .{ \"muosat.no\", {} },\n        .{ \"muosát.no\", {} },\n        .{ \"naamesjevuemie.no\", {} },\n        .{ \"nååmesjevuemie.no\", {} },\n        .{ \"nærøy.no\", {} },\n        .{ \"namdalseid.no\", {} },\n        .{ \"namsos.no\", {} },\n        .{ \"namsskogan.no\", {} },\n        .{ \"nannestad.no\", {} },\n        .{ \"naroy.no\", {} },\n        .{ \"narviika.no\", {} },\n        .{ \"narvik.no\", {} },\n        .{ \"naustdal.no\", {} },\n        .{ \"navuotna.no\", {} },\n        .{ \"návuotna.no\", {} },\n        .{ \"nedre-eiker.no\", {} },\n        .{ \"nesna.no\", {} },\n        .{ \"nesodden.no\", {} },\n        .{ \"nesseby.no\", {} },\n        .{ \"nesset.no\", {} },\n        .{ \"nissedal.no\", {} },\n        .{ \"nittedal.no\", {} },\n        .{ \"nord-aurdal.no\", {} },\n        .{ \"nord-fron.no\", {} },\n        .{ \"nord-odal.no\", {} },\n        .{ \"norddal.no\", {} },\n        .{ \"nordkapp.no\", {} },\n        .{ \"bo.nordland.no\", {} },\n        .{ \"bø.nordland.no\", {} },\n        .{ \"heroy.nordland.no\", {} },\n        .{ \"herøy.nordland.no\", {} },\n        .{ \"nordre-land.no\", {} },\n        .{ \"nordreisa.no\", {} },\n        .{ \"nore-og-uvdal.no\", {} },\n        .{ \"notodden.no\", {} },\n        .{ \"notteroy.no\", {} },\n        .{ \"nøtterøy.no\", {} },\n        .{ \"odda.no\", {} },\n        .{ \"oksnes.no\", {} },\n        .{ \"øksnes.no\", {} },\n        .{ \"omasvuotna.no\", {} },\n        .{ \"oppdal.no\", {} },\n        .{ \"oppegard.no\", {} },\n        .{ \"oppegård.no\", {} },\n        .{ \"orkdal.no\", {} },\n        .{ \"orland.no\", {} },\n        .{ \"ørland.no\", {} },\n        .{ \"orskog.no\", {} },\n        .{ \"ørskog.no\", {} },\n        .{ \"orsta.no\", {} },\n        .{ \"ørsta.no\", {} },\n        .{ \"osen.no\", {} },\n        .{ \"osteroy.no\", {} },\n        .{ \"osterøy.no\", {} },\n        .{ \"valer.ostfold.no\", {} },\n        .{ \"våler.østfold.no\", {} },\n        .{ \"ostre-toten.no\", {} },\n        .{ \"østre-toten.no\", {} },\n        .{ \"overhalla.no\", {} },\n        .{ \"ovre-eiker.no\", {} },\n        .{ \"øvre-eiker.no\", {} },\n        .{ \"oyer.no\", {} },\n        .{ \"øyer.no\", {} },\n        .{ \"oygarden.no\", {} },\n        .{ \"øygarden.no\", {} },\n        .{ \"oystre-slidre.no\", {} },\n        .{ \"øystre-slidre.no\", {} },\n        .{ \"porsanger.no\", {} },\n        .{ \"porsangu.no\", {} },\n        .{ \"porsáŋgu.no\", {} },\n        .{ \"porsgrunn.no\", {} },\n        .{ \"rade.no\", {} },\n        .{ \"råde.no\", {} },\n        .{ \"radoy.no\", {} },\n        .{ \"radøy.no\", {} },\n        .{ \"rælingen.no\", {} },\n        .{ \"rahkkeravju.no\", {} },\n        .{ \"ráhkkerávju.no\", {} },\n        .{ \"raisa.no\", {} },\n        .{ \"ráisa.no\", {} },\n        .{ \"rakkestad.no\", {} },\n        .{ \"ralingen.no\", {} },\n        .{ \"rana.no\", {} },\n        .{ \"randaberg.no\", {} },\n        .{ \"rauma.no\", {} },\n        .{ \"rendalen.no\", {} },\n        .{ \"rennebu.no\", {} },\n        .{ \"rennesoy.no\", {} },\n        .{ \"rennesøy.no\", {} },\n        .{ \"rindal.no\", {} },\n        .{ \"ringebu.no\", {} },\n        .{ \"ringerike.no\", {} },\n        .{ \"ringsaker.no\", {} },\n        .{ \"risor.no\", {} },\n        .{ \"risør.no\", {} },\n        .{ \"rissa.no\", {} },\n        .{ \"roan.no\", {} },\n        .{ \"rodoy.no\", {} },\n        .{ \"rødøy.no\", {} },\n        .{ \"rollag.no\", {} },\n        .{ \"romsa.no\", {} },\n        .{ \"romskog.no\", {} },\n        .{ \"rømskog.no\", {} },\n        .{ \"roros.no\", {} },\n        .{ \"røros.no\", {} },\n        .{ \"rost.no\", {} },\n        .{ \"røst.no\", {} },\n        .{ \"royken.no\", {} },\n        .{ \"røyken.no\", {} },\n        .{ \"royrvik.no\", {} },\n        .{ \"røyrvik.no\", {} },\n        .{ \"ruovat.no\", {} },\n        .{ \"rygge.no\", {} },\n        .{ \"salangen.no\", {} },\n        .{ \"salat.no\", {} },\n        .{ \"sálat.no\", {} },\n        .{ \"sálát.no\", {} },\n        .{ \"saltdal.no\", {} },\n        .{ \"samnanger.no\", {} },\n        .{ \"sandefjord.no\", {} },\n        .{ \"sandnes.no\", {} },\n        .{ \"sandoy.no\", {} },\n        .{ \"sandøy.no\", {} },\n        .{ \"sarpsborg.no\", {} },\n        .{ \"sauda.no\", {} },\n        .{ \"sauherad.no\", {} },\n        .{ \"sel.no\", {} },\n        .{ \"selbu.no\", {} },\n        .{ \"selje.no\", {} },\n        .{ \"seljord.no\", {} },\n        .{ \"siellak.no\", {} },\n        .{ \"sigdal.no\", {} },\n        .{ \"siljan.no\", {} },\n        .{ \"sirdal.no\", {} },\n        .{ \"skanit.no\", {} },\n        .{ \"skánit.no\", {} },\n        .{ \"skanland.no\", {} },\n        .{ \"skånland.no\", {} },\n        .{ \"skaun.no\", {} },\n        .{ \"skedsmo.no\", {} },\n        .{ \"ski.no\", {} },\n        .{ \"skien.no\", {} },\n        .{ \"skierva.no\", {} },\n        .{ \"skiervá.no\", {} },\n        .{ \"skiptvet.no\", {} },\n        .{ \"skjak.no\", {} },\n        .{ \"skjåk.no\", {} },\n        .{ \"skjervoy.no\", {} },\n        .{ \"skjervøy.no\", {} },\n        .{ \"skodje.no\", {} },\n        .{ \"smola.no\", {} },\n        .{ \"smøla.no\", {} },\n        .{ \"snaase.no\", {} },\n        .{ \"snåase.no\", {} },\n        .{ \"snasa.no\", {} },\n        .{ \"snåsa.no\", {} },\n        .{ \"snillfjord.no\", {} },\n        .{ \"snoasa.no\", {} },\n        .{ \"sogndal.no\", {} },\n        .{ \"sogne.no\", {} },\n        .{ \"søgne.no\", {} },\n        .{ \"sokndal.no\", {} },\n        .{ \"sola.no\", {} },\n        .{ \"solund.no\", {} },\n        .{ \"somna.no\", {} },\n        .{ \"sømna.no\", {} },\n        .{ \"sondre-land.no\", {} },\n        .{ \"søndre-land.no\", {} },\n        .{ \"songdalen.no\", {} },\n        .{ \"sor-aurdal.no\", {} },\n        .{ \"sør-aurdal.no\", {} },\n        .{ \"sor-fron.no\", {} },\n        .{ \"sør-fron.no\", {} },\n        .{ \"sor-odal.no\", {} },\n        .{ \"sør-odal.no\", {} },\n        .{ \"sor-varanger.no\", {} },\n        .{ \"sør-varanger.no\", {} },\n        .{ \"sorfold.no\", {} },\n        .{ \"sørfold.no\", {} },\n        .{ \"sorreisa.no\", {} },\n        .{ \"sørreisa.no\", {} },\n        .{ \"sortland.no\", {} },\n        .{ \"sorum.no\", {} },\n        .{ \"sørum.no\", {} },\n        .{ \"spydeberg.no\", {} },\n        .{ \"stange.no\", {} },\n        .{ \"stavanger.no\", {} },\n        .{ \"steigen.no\", {} },\n        .{ \"steinkjer.no\", {} },\n        .{ \"stjordal.no\", {} },\n        .{ \"stjørdal.no\", {} },\n        .{ \"stokke.no\", {} },\n        .{ \"stor-elvdal.no\", {} },\n        .{ \"stord.no\", {} },\n        .{ \"stordal.no\", {} },\n        .{ \"storfjord.no\", {} },\n        .{ \"strand.no\", {} },\n        .{ \"stranda.no\", {} },\n        .{ \"stryn.no\", {} },\n        .{ \"sula.no\", {} },\n        .{ \"suldal.no\", {} },\n        .{ \"sund.no\", {} },\n        .{ \"sunndal.no\", {} },\n        .{ \"surnadal.no\", {} },\n        .{ \"sveio.no\", {} },\n        .{ \"svelvik.no\", {} },\n        .{ \"sykkylven.no\", {} },\n        .{ \"tana.no\", {} },\n        .{ \"bo.telemark.no\", {} },\n        .{ \"bø.telemark.no\", {} },\n        .{ \"time.no\", {} },\n        .{ \"tingvoll.no\", {} },\n        .{ \"tinn.no\", {} },\n        .{ \"tjeldsund.no\", {} },\n        .{ \"tjome.no\", {} },\n        .{ \"tjøme.no\", {} },\n        .{ \"tokke.no\", {} },\n        .{ \"tolga.no\", {} },\n        .{ \"tonsberg.no\", {} },\n        .{ \"tønsberg.no\", {} },\n        .{ \"torsken.no\", {} },\n        .{ \"træna.no\", {} },\n        .{ \"trana.no\", {} },\n        .{ \"tranoy.no\", {} },\n        .{ \"tranøy.no\", {} },\n        .{ \"troandin.no\", {} },\n        .{ \"trogstad.no\", {} },\n        .{ \"trøgstad.no\", {} },\n        .{ \"tromsa.no\", {} },\n        .{ \"tromso.no\", {} },\n        .{ \"tromsø.no\", {} },\n        .{ \"trondheim.no\", {} },\n        .{ \"trysil.no\", {} },\n        .{ \"tvedestrand.no\", {} },\n        .{ \"tydal.no\", {} },\n        .{ \"tynset.no\", {} },\n        .{ \"tysfjord.no\", {} },\n        .{ \"tysnes.no\", {} },\n        .{ \"tysvær.no\", {} },\n        .{ \"tysvar.no\", {} },\n        .{ \"ullensaker.no\", {} },\n        .{ \"ullensvang.no\", {} },\n        .{ \"ulvik.no\", {} },\n        .{ \"unjarga.no\", {} },\n        .{ \"unjárga.no\", {} },\n        .{ \"utsira.no\", {} },\n        .{ \"vaapste.no\", {} },\n        .{ \"vadso.no\", {} },\n        .{ \"vadsø.no\", {} },\n        .{ \"værøy.no\", {} },\n        .{ \"vaga.no\", {} },\n        .{ \"vågå.no\", {} },\n        .{ \"vagan.no\", {} },\n        .{ \"vågan.no\", {} },\n        .{ \"vagsoy.no\", {} },\n        .{ \"vågsøy.no\", {} },\n        .{ \"vaksdal.no\", {} },\n        .{ \"valle.no\", {} },\n        .{ \"vang.no\", {} },\n        .{ \"vanylven.no\", {} },\n        .{ \"vardo.no\", {} },\n        .{ \"vardø.no\", {} },\n        .{ \"varggat.no\", {} },\n        .{ \"várggát.no\", {} },\n        .{ \"varoy.no\", {} },\n        .{ \"vefsn.no\", {} },\n        .{ \"vega.no\", {} },\n        .{ \"vegarshei.no\", {} },\n        .{ \"vegårshei.no\", {} },\n        .{ \"vennesla.no\", {} },\n        .{ \"verdal.no\", {} },\n        .{ \"verran.no\", {} },\n        .{ \"vestby.no\", {} },\n        .{ \"sande.vestfold.no\", {} },\n        .{ \"vestnes.no\", {} },\n        .{ \"vestre-slidre.no\", {} },\n        .{ \"vestre-toten.no\", {} },\n        .{ \"vestvagoy.no\", {} },\n        .{ \"vestvågøy.no\", {} },\n        .{ \"vevelstad.no\", {} },\n        .{ \"vik.no\", {} },\n        .{ \"vikna.no\", {} },\n        .{ \"vindafjord.no\", {} },\n        .{ \"voagat.no\", {} },\n        .{ \"volda.no\", {} },\n        .{ \"voss.no\", {} },\n        .{ \"*.np\", {} },\n        .{ \"nr\", {} },\n        .{ \"biz.nr\", {} },\n        .{ \"com.nr\", {} },\n        .{ \"edu.nr\", {} },\n        .{ \"gov.nr\", {} },\n        .{ \"info.nr\", {} },\n        .{ \"net.nr\", {} },\n        .{ \"org.nr\", {} },\n        .{ \"nu\", {} },\n        .{ \"nz\", {} },\n        .{ \"ac.nz\", {} },\n        .{ \"co.nz\", {} },\n        .{ \"cri.nz\", {} },\n        .{ \"geek.nz\", {} },\n        .{ \"gen.nz\", {} },\n        .{ \"govt.nz\", {} },\n        .{ \"health.nz\", {} },\n        .{ \"iwi.nz\", {} },\n        .{ \"kiwi.nz\", {} },\n        .{ \"maori.nz\", {} },\n        .{ \"māori.nz\", {} },\n        .{ \"mil.nz\", {} },\n        .{ \"net.nz\", {} },\n        .{ \"org.nz\", {} },\n        .{ \"parliament.nz\", {} },\n        .{ \"school.nz\", {} },\n        .{ \"om\", {} },\n        .{ \"co.om\", {} },\n        .{ \"com.om\", {} },\n        .{ \"edu.om\", {} },\n        .{ \"gov.om\", {} },\n        .{ \"med.om\", {} },\n        .{ \"museum.om\", {} },\n        .{ \"net.om\", {} },\n        .{ \"org.om\", {} },\n        .{ \"pro.om\", {} },\n        .{ \"onion\", {} },\n        .{ \"org\", {} },\n        .{ \"pa\", {} },\n        .{ \"abo.pa\", {} },\n        .{ \"ac.pa\", {} },\n        .{ \"com.pa\", {} },\n        .{ \"edu.pa\", {} },\n        .{ \"gob.pa\", {} },\n        .{ \"ing.pa\", {} },\n        .{ \"med.pa\", {} },\n        .{ \"net.pa\", {} },\n        .{ \"nom.pa\", {} },\n        .{ \"org.pa\", {} },\n        .{ \"sld.pa\", {} },\n        .{ \"pe\", {} },\n        .{ \"com.pe\", {} },\n        .{ \"edu.pe\", {} },\n        .{ \"gob.pe\", {} },\n        .{ \"mil.pe\", {} },\n        .{ \"net.pe\", {} },\n        .{ \"nom.pe\", {} },\n        .{ \"org.pe\", {} },\n        .{ \"pf\", {} },\n        .{ \"com.pf\", {} },\n        .{ \"edu.pf\", {} },\n        .{ \"org.pf\", {} },\n        .{ \"*.pg\", {} },\n        .{ \"ph\", {} },\n        .{ \"com.ph\", {} },\n        .{ \"edu.ph\", {} },\n        .{ \"gov.ph\", {} },\n        .{ \"i.ph\", {} },\n        .{ \"mil.ph\", {} },\n        .{ \"net.ph\", {} },\n        .{ \"ngo.ph\", {} },\n        .{ \"org.ph\", {} },\n        .{ \"pk\", {} },\n        .{ \"ac.pk\", {} },\n        .{ \"biz.pk\", {} },\n        .{ \"com.pk\", {} },\n        .{ \"edu.pk\", {} },\n        .{ \"fam.pk\", {} },\n        .{ \"gkp.pk\", {} },\n        .{ \"gob.pk\", {} },\n        .{ \"gog.pk\", {} },\n        .{ \"gok.pk\", {} },\n        .{ \"gop.pk\", {} },\n        .{ \"gos.pk\", {} },\n        .{ \"gov.pk\", {} },\n        .{ \"net.pk\", {} },\n        .{ \"org.pk\", {} },\n        .{ \"web.pk\", {} },\n        .{ \"pl\", {} },\n        .{ \"com.pl\", {} },\n        .{ \"net.pl\", {} },\n        .{ \"org.pl\", {} },\n        .{ \"agro.pl\", {} },\n        .{ \"aid.pl\", {} },\n        .{ \"atm.pl\", {} },\n        .{ \"auto.pl\", {} },\n        .{ \"biz.pl\", {} },\n        .{ \"edu.pl\", {} },\n        .{ \"gmina.pl\", {} },\n        .{ \"gsm.pl\", {} },\n        .{ \"info.pl\", {} },\n        .{ \"mail.pl\", {} },\n        .{ \"media.pl\", {} },\n        .{ \"miasta.pl\", {} },\n        .{ \"mil.pl\", {} },\n        .{ \"nieruchomosci.pl\", {} },\n        .{ \"nom.pl\", {} },\n        .{ \"pc.pl\", {} },\n        .{ \"powiat.pl\", {} },\n        .{ \"priv.pl\", {} },\n        .{ \"realestate.pl\", {} },\n        .{ \"rel.pl\", {} },\n        .{ \"sex.pl\", {} },\n        .{ \"shop.pl\", {} },\n        .{ \"sklep.pl\", {} },\n        .{ \"sos.pl\", {} },\n        .{ \"szkola.pl\", {} },\n        .{ \"targi.pl\", {} },\n        .{ \"tm.pl\", {} },\n        .{ \"tourism.pl\", {} },\n        .{ \"travel.pl\", {} },\n        .{ \"turystyka.pl\", {} },\n        .{ \"gov.pl\", {} },\n        .{ \"ap.gov.pl\", {} },\n        .{ \"griw.gov.pl\", {} },\n        .{ \"ic.gov.pl\", {} },\n        .{ \"is.gov.pl\", {} },\n        .{ \"kmpsp.gov.pl\", {} },\n        .{ \"konsulat.gov.pl\", {} },\n        .{ \"kppsp.gov.pl\", {} },\n        .{ \"kwp.gov.pl\", {} },\n        .{ \"kwpsp.gov.pl\", {} },\n        .{ \"mup.gov.pl\", {} },\n        .{ \"mw.gov.pl\", {} },\n        .{ \"oia.gov.pl\", {} },\n        .{ \"oirm.gov.pl\", {} },\n        .{ \"oke.gov.pl\", {} },\n        .{ \"oow.gov.pl\", {} },\n        .{ \"oschr.gov.pl\", {} },\n        .{ \"oum.gov.pl\", {} },\n        .{ \"pa.gov.pl\", {} },\n        .{ \"pinb.gov.pl\", {} },\n        .{ \"piw.gov.pl\", {} },\n        .{ \"po.gov.pl\", {} },\n        .{ \"pr.gov.pl\", {} },\n        .{ \"psp.gov.pl\", {} },\n        .{ \"psse.gov.pl\", {} },\n        .{ \"pup.gov.pl\", {} },\n        .{ \"rzgw.gov.pl\", {} },\n        .{ \"sa.gov.pl\", {} },\n        .{ \"sdn.gov.pl\", {} },\n        .{ \"sko.gov.pl\", {} },\n        .{ \"so.gov.pl\", {} },\n        .{ \"sr.gov.pl\", {} },\n        .{ \"starostwo.gov.pl\", {} },\n        .{ \"ug.gov.pl\", {} },\n        .{ \"ugim.gov.pl\", {} },\n        .{ \"um.gov.pl\", {} },\n        .{ \"umig.gov.pl\", {} },\n        .{ \"upow.gov.pl\", {} },\n        .{ \"uppo.gov.pl\", {} },\n        .{ \"us.gov.pl\", {} },\n        .{ \"uw.gov.pl\", {} },\n        .{ \"uzs.gov.pl\", {} },\n        .{ \"wif.gov.pl\", {} },\n        .{ \"wiih.gov.pl\", {} },\n        .{ \"winb.gov.pl\", {} },\n        .{ \"wios.gov.pl\", {} },\n        .{ \"witd.gov.pl\", {} },\n        .{ \"wiw.gov.pl\", {} },\n        .{ \"wkz.gov.pl\", {} },\n        .{ \"wsa.gov.pl\", {} },\n        .{ \"wskr.gov.pl\", {} },\n        .{ \"wsse.gov.pl\", {} },\n        .{ \"wuoz.gov.pl\", {} },\n        .{ \"wzmiuw.gov.pl\", {} },\n        .{ \"zp.gov.pl\", {} },\n        .{ \"zpisdn.gov.pl\", {} },\n        .{ \"augustow.pl\", {} },\n        .{ \"babia-gora.pl\", {} },\n        .{ \"bedzin.pl\", {} },\n        .{ \"beskidy.pl\", {} },\n        .{ \"bialowieza.pl\", {} },\n        .{ \"bialystok.pl\", {} },\n        .{ \"bielawa.pl\", {} },\n        .{ \"bieszczady.pl\", {} },\n        .{ \"boleslawiec.pl\", {} },\n        .{ \"bydgoszcz.pl\", {} },\n        .{ \"bytom.pl\", {} },\n        .{ \"cieszyn.pl\", {} },\n        .{ \"czeladz.pl\", {} },\n        .{ \"czest.pl\", {} },\n        .{ \"dlugoleka.pl\", {} },\n        .{ \"elblag.pl\", {} },\n        .{ \"elk.pl\", {} },\n        .{ \"glogow.pl\", {} },\n        .{ \"gniezno.pl\", {} },\n        .{ \"gorlice.pl\", {} },\n        .{ \"grajewo.pl\", {} },\n        .{ \"ilawa.pl\", {} },\n        .{ \"jaworzno.pl\", {} },\n        .{ \"jelenia-gora.pl\", {} },\n        .{ \"jgora.pl\", {} },\n        .{ \"kalisz.pl\", {} },\n        .{ \"karpacz.pl\", {} },\n        .{ \"kartuzy.pl\", {} },\n        .{ \"kaszuby.pl\", {} },\n        .{ \"katowice.pl\", {} },\n        .{ \"kazimierz-dolny.pl\", {} },\n        .{ \"kepno.pl\", {} },\n        .{ \"ketrzyn.pl\", {} },\n        .{ \"klodzko.pl\", {} },\n        .{ \"kobierzyce.pl\", {} },\n        .{ \"kolobrzeg.pl\", {} },\n        .{ \"konin.pl\", {} },\n        .{ \"konskowola.pl\", {} },\n        .{ \"kutno.pl\", {} },\n        .{ \"lapy.pl\", {} },\n        .{ \"lebork.pl\", {} },\n        .{ \"legnica.pl\", {} },\n        .{ \"lezajsk.pl\", {} },\n        .{ \"limanowa.pl\", {} },\n        .{ \"lomza.pl\", {} },\n        .{ \"lowicz.pl\", {} },\n        .{ \"lubin.pl\", {} },\n        .{ \"lukow.pl\", {} },\n        .{ \"malbork.pl\", {} },\n        .{ \"malopolska.pl\", {} },\n        .{ \"mazowsze.pl\", {} },\n        .{ \"mazury.pl\", {} },\n        .{ \"mielec.pl\", {} },\n        .{ \"mielno.pl\", {} },\n        .{ \"mragowo.pl\", {} },\n        .{ \"naklo.pl\", {} },\n        .{ \"nowaruda.pl\", {} },\n        .{ \"nysa.pl\", {} },\n        .{ \"olawa.pl\", {} },\n        .{ \"olecko.pl\", {} },\n        .{ \"olkusz.pl\", {} },\n        .{ \"olsztyn.pl\", {} },\n        .{ \"opoczno.pl\", {} },\n        .{ \"opole.pl\", {} },\n        .{ \"ostroda.pl\", {} },\n        .{ \"ostroleka.pl\", {} },\n        .{ \"ostrowiec.pl\", {} },\n        .{ \"ostrowwlkp.pl\", {} },\n        .{ \"pila.pl\", {} },\n        .{ \"pisz.pl\", {} },\n        .{ \"podhale.pl\", {} },\n        .{ \"podlasie.pl\", {} },\n        .{ \"polkowice.pl\", {} },\n        .{ \"pomorskie.pl\", {} },\n        .{ \"pomorze.pl\", {} },\n        .{ \"prochowice.pl\", {} },\n        .{ \"pruszkow.pl\", {} },\n        .{ \"przeworsk.pl\", {} },\n        .{ \"pulawy.pl\", {} },\n        .{ \"radom.pl\", {} },\n        .{ \"rawa-maz.pl\", {} },\n        .{ \"rybnik.pl\", {} },\n        .{ \"rzeszow.pl\", {} },\n        .{ \"sanok.pl\", {} },\n        .{ \"sejny.pl\", {} },\n        .{ \"skoczow.pl\", {} },\n        .{ \"slask.pl\", {} },\n        .{ \"slupsk.pl\", {} },\n        .{ \"sosnowiec.pl\", {} },\n        .{ \"stalowa-wola.pl\", {} },\n        .{ \"starachowice.pl\", {} },\n        .{ \"stargard.pl\", {} },\n        .{ \"suwalki.pl\", {} },\n        .{ \"swidnica.pl\", {} },\n        .{ \"swiebodzin.pl\", {} },\n        .{ \"swinoujscie.pl\", {} },\n        .{ \"szczecin.pl\", {} },\n        .{ \"szczytno.pl\", {} },\n        .{ \"tarnobrzeg.pl\", {} },\n        .{ \"tgory.pl\", {} },\n        .{ \"turek.pl\", {} },\n        .{ \"tychy.pl\", {} },\n        .{ \"ustka.pl\", {} },\n        .{ \"walbrzych.pl\", {} },\n        .{ \"warmia.pl\", {} },\n        .{ \"warszawa.pl\", {} },\n        .{ \"waw.pl\", {} },\n        .{ \"wegrow.pl\", {} },\n        .{ \"wielun.pl\", {} },\n        .{ \"wlocl.pl\", {} },\n        .{ \"wloclawek.pl\", {} },\n        .{ \"wodzislaw.pl\", {} },\n        .{ \"wolomin.pl\", {} },\n        .{ \"wroclaw.pl\", {} },\n        .{ \"zachpomor.pl\", {} },\n        .{ \"zagan.pl\", {} },\n        .{ \"zarow.pl\", {} },\n        .{ \"zgora.pl\", {} },\n        .{ \"zgorzelec.pl\", {} },\n        .{ \"pm\", {} },\n        .{ \"pn\", {} },\n        .{ \"co.pn\", {} },\n        .{ \"edu.pn\", {} },\n        .{ \"gov.pn\", {} },\n        .{ \"net.pn\", {} },\n        .{ \"org.pn\", {} },\n        .{ \"post\", {} },\n        .{ \"pr\", {} },\n        .{ \"biz.pr\", {} },\n        .{ \"com.pr\", {} },\n        .{ \"edu.pr\", {} },\n        .{ \"gov.pr\", {} },\n        .{ \"info.pr\", {} },\n        .{ \"isla.pr\", {} },\n        .{ \"name.pr\", {} },\n        .{ \"net.pr\", {} },\n        .{ \"org.pr\", {} },\n        .{ \"pro.pr\", {} },\n        .{ \"ac.pr\", {} },\n        .{ \"est.pr\", {} },\n        .{ \"prof.pr\", {} },\n        .{ \"pro\", {} },\n        .{ \"aaa.pro\", {} },\n        .{ \"aca.pro\", {} },\n        .{ \"acct.pro\", {} },\n        .{ \"avocat.pro\", {} },\n        .{ \"bar.pro\", {} },\n        .{ \"cpa.pro\", {} },\n        .{ \"eng.pro\", {} },\n        .{ \"jur.pro\", {} },\n        .{ \"law.pro\", {} },\n        .{ \"med.pro\", {} },\n        .{ \"recht.pro\", {} },\n        .{ \"ps\", {} },\n        .{ \"com.ps\", {} },\n        .{ \"edu.ps\", {} },\n        .{ \"gov.ps\", {} },\n        .{ \"net.ps\", {} },\n        .{ \"org.ps\", {} },\n        .{ \"plo.ps\", {} },\n        .{ \"sec.ps\", {} },\n        .{ \"pt\", {} },\n        .{ \"com.pt\", {} },\n        .{ \"edu.pt\", {} },\n        .{ \"gov.pt\", {} },\n        .{ \"int.pt\", {} },\n        .{ \"net.pt\", {} },\n        .{ \"nome.pt\", {} },\n        .{ \"org.pt\", {} },\n        .{ \"publ.pt\", {} },\n        .{ \"pw\", {} },\n        .{ \"gov.pw\", {} },\n        .{ \"py\", {} },\n        .{ \"com.py\", {} },\n        .{ \"coop.py\", {} },\n        .{ \"edu.py\", {} },\n        .{ \"gov.py\", {} },\n        .{ \"mil.py\", {} },\n        .{ \"net.py\", {} },\n        .{ \"org.py\", {} },\n        .{ \"qa\", {} },\n        .{ \"com.qa\", {} },\n        .{ \"edu.qa\", {} },\n        .{ \"gov.qa\", {} },\n        .{ \"mil.qa\", {} },\n        .{ \"name.qa\", {} },\n        .{ \"net.qa\", {} },\n        .{ \"org.qa\", {} },\n        .{ \"sch.qa\", {} },\n        .{ \"re\", {} },\n        .{ \"asso.re\", {} },\n        .{ \"com.re\", {} },\n        .{ \"ro\", {} },\n        .{ \"arts.ro\", {} },\n        .{ \"com.ro\", {} },\n        .{ \"firm.ro\", {} },\n        .{ \"info.ro\", {} },\n        .{ \"nom.ro\", {} },\n        .{ \"nt.ro\", {} },\n        .{ \"org.ro\", {} },\n        .{ \"rec.ro\", {} },\n        .{ \"store.ro\", {} },\n        .{ \"tm.ro\", {} },\n        .{ \"www.ro\", {} },\n        .{ \"rs\", {} },\n        .{ \"ac.rs\", {} },\n        .{ \"co.rs\", {} },\n        .{ \"edu.rs\", {} },\n        .{ \"gov.rs\", {} },\n        .{ \"in.rs\", {} },\n        .{ \"org.rs\", {} },\n        .{ \"ru\", {} },\n        .{ \"rw\", {} },\n        .{ \"ac.rw\", {} },\n        .{ \"co.rw\", {} },\n        .{ \"coop.rw\", {} },\n        .{ \"gov.rw\", {} },\n        .{ \"mil.rw\", {} },\n        .{ \"net.rw\", {} },\n        .{ \"org.rw\", {} },\n        .{ \"sa\", {} },\n        .{ \"com.sa\", {} },\n        .{ \"edu.sa\", {} },\n        .{ \"gov.sa\", {} },\n        .{ \"med.sa\", {} },\n        .{ \"net.sa\", {} },\n        .{ \"org.sa\", {} },\n        .{ \"pub.sa\", {} },\n        .{ \"sch.sa\", {} },\n        .{ \"sb\", {} },\n        .{ \"com.sb\", {} },\n        .{ \"edu.sb\", {} },\n        .{ \"gov.sb\", {} },\n        .{ \"net.sb\", {} },\n        .{ \"org.sb\", {} },\n        .{ \"sc\", {} },\n        .{ \"com.sc\", {} },\n        .{ \"edu.sc\", {} },\n        .{ \"gov.sc\", {} },\n        .{ \"net.sc\", {} },\n        .{ \"org.sc\", {} },\n        .{ \"sd\", {} },\n        .{ \"com.sd\", {} },\n        .{ \"edu.sd\", {} },\n        .{ \"gov.sd\", {} },\n        .{ \"info.sd\", {} },\n        .{ \"med.sd\", {} },\n        .{ \"net.sd\", {} },\n        .{ \"org.sd\", {} },\n        .{ \"tv.sd\", {} },\n        .{ \"se\", {} },\n        .{ \"a.se\", {} },\n        .{ \"ac.se\", {} },\n        .{ \"b.se\", {} },\n        .{ \"bd.se\", {} },\n        .{ \"brand.se\", {} },\n        .{ \"c.se\", {} },\n        .{ \"d.se\", {} },\n        .{ \"e.se\", {} },\n        .{ \"f.se\", {} },\n        .{ \"fh.se\", {} },\n        .{ \"fhsk.se\", {} },\n        .{ \"fhv.se\", {} },\n        .{ \"g.se\", {} },\n        .{ \"h.se\", {} },\n        .{ \"i.se\", {} },\n        .{ \"k.se\", {} },\n        .{ \"komforb.se\", {} },\n        .{ \"kommunalforbund.se\", {} },\n        .{ \"komvux.se\", {} },\n        .{ \"l.se\", {} },\n        .{ \"lanbib.se\", {} },\n        .{ \"m.se\", {} },\n        .{ \"n.se\", {} },\n        .{ \"naturbruksgymn.se\", {} },\n        .{ \"o.se\", {} },\n        .{ \"org.se\", {} },\n        .{ \"p.se\", {} },\n        .{ \"parti.se\", {} },\n        .{ \"pp.se\", {} },\n        .{ \"press.se\", {} },\n        .{ \"r.se\", {} },\n        .{ \"s.se\", {} },\n        .{ \"t.se\", {} },\n        .{ \"tm.se\", {} },\n        .{ \"u.se\", {} },\n        .{ \"w.se\", {} },\n        .{ \"x.se\", {} },\n        .{ \"y.se\", {} },\n        .{ \"z.se\", {} },\n        .{ \"sg\", {} },\n        .{ \"com.sg\", {} },\n        .{ \"edu.sg\", {} },\n        .{ \"gov.sg\", {} },\n        .{ \"net.sg\", {} },\n        .{ \"org.sg\", {} },\n        .{ \"sh\", {} },\n        .{ \"com.sh\", {} },\n        .{ \"gov.sh\", {} },\n        .{ \"mil.sh\", {} },\n        .{ \"net.sh\", {} },\n        .{ \"org.sh\", {} },\n        .{ \"si\", {} },\n        .{ \"sj\", {} },\n        .{ \"sk\", {} },\n        .{ \"org.sk\", {} },\n        .{ \"sl\", {} },\n        .{ \"com.sl\", {} },\n        .{ \"edu.sl\", {} },\n        .{ \"gov.sl\", {} },\n        .{ \"net.sl\", {} },\n        .{ \"org.sl\", {} },\n        .{ \"sm\", {} },\n        .{ \"sn\", {} },\n        .{ \"art.sn\", {} },\n        .{ \"com.sn\", {} },\n        .{ \"edu.sn\", {} },\n        .{ \"gouv.sn\", {} },\n        .{ \"org.sn\", {} },\n        .{ \"univ.sn\", {} },\n        .{ \"so\", {} },\n        .{ \"com.so\", {} },\n        .{ \"edu.so\", {} },\n        .{ \"gov.so\", {} },\n        .{ \"me.so\", {} },\n        .{ \"net.so\", {} },\n        .{ \"org.so\", {} },\n        .{ \"sr\", {} },\n        .{ \"ss\", {} },\n        .{ \"biz.ss\", {} },\n        .{ \"co.ss\", {} },\n        .{ \"com.ss\", {} },\n        .{ \"edu.ss\", {} },\n        .{ \"gov.ss\", {} },\n        .{ \"me.ss\", {} },\n        .{ \"net.ss\", {} },\n        .{ \"org.ss\", {} },\n        .{ \"sch.ss\", {} },\n        .{ \"st\", {} },\n        .{ \"co.st\", {} },\n        .{ \"com.st\", {} },\n        .{ \"consulado.st\", {} },\n        .{ \"edu.st\", {} },\n        .{ \"embaixada.st\", {} },\n        .{ \"mil.st\", {} },\n        .{ \"net.st\", {} },\n        .{ \"org.st\", {} },\n        .{ \"principe.st\", {} },\n        .{ \"saotome.st\", {} },\n        .{ \"store.st\", {} },\n        .{ \"su\", {} },\n        .{ \"sv\", {} },\n        .{ \"com.sv\", {} },\n        .{ \"edu.sv\", {} },\n        .{ \"gob.sv\", {} },\n        .{ \"org.sv\", {} },\n        .{ \"red.sv\", {} },\n        .{ \"sx\", {} },\n        .{ \"gov.sx\", {} },\n        .{ \"sy\", {} },\n        .{ \"com.sy\", {} },\n        .{ \"edu.sy\", {} },\n        .{ \"gov.sy\", {} },\n        .{ \"mil.sy\", {} },\n        .{ \"net.sy\", {} },\n        .{ \"org.sy\", {} },\n        .{ \"sz\", {} },\n        .{ \"ac.sz\", {} },\n        .{ \"co.sz\", {} },\n        .{ \"org.sz\", {} },\n        .{ \"tc\", {} },\n        .{ \"td\", {} },\n        .{ \"tel\", {} },\n        .{ \"tf\", {} },\n        .{ \"tg\", {} },\n        .{ \"th\", {} },\n        .{ \"ac.th\", {} },\n        .{ \"co.th\", {} },\n        .{ \"go.th\", {} },\n        .{ \"in.th\", {} },\n        .{ \"mi.th\", {} },\n        .{ \"net.th\", {} },\n        .{ \"or.th\", {} },\n        .{ \"tj\", {} },\n        .{ \"ac.tj\", {} },\n        .{ \"biz.tj\", {} },\n        .{ \"co.tj\", {} },\n        .{ \"com.tj\", {} },\n        .{ \"edu.tj\", {} },\n        .{ \"go.tj\", {} },\n        .{ \"gov.tj\", {} },\n        .{ \"int.tj\", {} },\n        .{ \"mil.tj\", {} },\n        .{ \"name.tj\", {} },\n        .{ \"net.tj\", {} },\n        .{ \"nic.tj\", {} },\n        .{ \"org.tj\", {} },\n        .{ \"test.tj\", {} },\n        .{ \"web.tj\", {} },\n        .{ \"tk\", {} },\n        .{ \"tl\", {} },\n        .{ \"gov.tl\", {} },\n        .{ \"tm\", {} },\n        .{ \"co.tm\", {} },\n        .{ \"com.tm\", {} },\n        .{ \"edu.tm\", {} },\n        .{ \"gov.tm\", {} },\n        .{ \"mil.tm\", {} },\n        .{ \"net.tm\", {} },\n        .{ \"nom.tm\", {} },\n        .{ \"org.tm\", {} },\n        .{ \"tn\", {} },\n        .{ \"com.tn\", {} },\n        .{ \"ens.tn\", {} },\n        .{ \"fin.tn\", {} },\n        .{ \"gov.tn\", {} },\n        .{ \"ind.tn\", {} },\n        .{ \"info.tn\", {} },\n        .{ \"intl.tn\", {} },\n        .{ \"mincom.tn\", {} },\n        .{ \"nat.tn\", {} },\n        .{ \"net.tn\", {} },\n        .{ \"org.tn\", {} },\n        .{ \"perso.tn\", {} },\n        .{ \"tourism.tn\", {} },\n        .{ \"to\", {} },\n        .{ \"com.to\", {} },\n        .{ \"edu.to\", {} },\n        .{ \"gov.to\", {} },\n        .{ \"mil.to\", {} },\n        .{ \"net.to\", {} },\n        .{ \"org.to\", {} },\n        .{ \"tr\", {} },\n        .{ \"av.tr\", {} },\n        .{ \"bbs.tr\", {} },\n        .{ \"bel.tr\", {} },\n        .{ \"biz.tr\", {} },\n        .{ \"com.tr\", {} },\n        .{ \"dr.tr\", {} },\n        .{ \"edu.tr\", {} },\n        .{ \"gen.tr\", {} },\n        .{ \"gov.tr\", {} },\n        .{ \"info.tr\", {} },\n        .{ \"k12.tr\", {} },\n        .{ \"kep.tr\", {} },\n        .{ \"mil.tr\", {} },\n        .{ \"name.tr\", {} },\n        .{ \"net.tr\", {} },\n        .{ \"org.tr\", {} },\n        .{ \"pol.tr\", {} },\n        .{ \"tel.tr\", {} },\n        .{ \"tsk.tr\", {} },\n        .{ \"tv.tr\", {} },\n        .{ \"web.tr\", {} },\n        .{ \"nc.tr\", {} },\n        .{ \"gov.nc.tr\", {} },\n        .{ \"tt\", {} },\n        .{ \"biz.tt\", {} },\n        .{ \"co.tt\", {} },\n        .{ \"com.tt\", {} },\n        .{ \"edu.tt\", {} },\n        .{ \"gov.tt\", {} },\n        .{ \"info.tt\", {} },\n        .{ \"mil.tt\", {} },\n        .{ \"name.tt\", {} },\n        .{ \"net.tt\", {} },\n        .{ \"org.tt\", {} },\n        .{ \"pro.tt\", {} },\n        .{ \"tv\", {} },\n        .{ \"tw\", {} },\n        .{ \"club.tw\", {} },\n        .{ \"com.tw\", {} },\n        .{ \"ebiz.tw\", {} },\n        .{ \"edu.tw\", {} },\n        .{ \"game.tw\", {} },\n        .{ \"gov.tw\", {} },\n        .{ \"idv.tw\", {} },\n        .{ \"mil.tw\", {} },\n        .{ \"net.tw\", {} },\n        .{ \"org.tw\", {} },\n        .{ \"tz\", {} },\n        .{ \"ac.tz\", {} },\n        .{ \"co.tz\", {} },\n        .{ \"go.tz\", {} },\n        .{ \"hotel.tz\", {} },\n        .{ \"info.tz\", {} },\n        .{ \"me.tz\", {} },\n        .{ \"mil.tz\", {} },\n        .{ \"mobi.tz\", {} },\n        .{ \"ne.tz\", {} },\n        .{ \"or.tz\", {} },\n        .{ \"sc.tz\", {} },\n        .{ \"tv.tz\", {} },\n        .{ \"ua\", {} },\n        .{ \"com.ua\", {} },\n        .{ \"edu.ua\", {} },\n        .{ \"gov.ua\", {} },\n        .{ \"in.ua\", {} },\n        .{ \"net.ua\", {} },\n        .{ \"org.ua\", {} },\n        .{ \"cherkassy.ua\", {} },\n        .{ \"cherkasy.ua\", {} },\n        .{ \"chernigov.ua\", {} },\n        .{ \"chernihiv.ua\", {} },\n        .{ \"chernivtsi.ua\", {} },\n        .{ \"chernovtsy.ua\", {} },\n        .{ \"ck.ua\", {} },\n        .{ \"cn.ua\", {} },\n        .{ \"cr.ua\", {} },\n        .{ \"crimea.ua\", {} },\n        .{ \"cv.ua\", {} },\n        .{ \"dn.ua\", {} },\n        .{ \"dnepropetrovsk.ua\", {} },\n        .{ \"dnipropetrovsk.ua\", {} },\n        .{ \"donetsk.ua\", {} },\n        .{ \"dp.ua\", {} },\n        .{ \"if.ua\", {} },\n        .{ \"ivano-frankivsk.ua\", {} },\n        .{ \"kh.ua\", {} },\n        .{ \"kharkiv.ua\", {} },\n        .{ \"kharkov.ua\", {} },\n        .{ \"kherson.ua\", {} },\n        .{ \"khmelnitskiy.ua\", {} },\n        .{ \"khmelnytskyi.ua\", {} },\n        .{ \"kiev.ua\", {} },\n        .{ \"kirovograd.ua\", {} },\n        .{ \"km.ua\", {} },\n        .{ \"kr.ua\", {} },\n        .{ \"kropyvnytskyi.ua\", {} },\n        .{ \"krym.ua\", {} },\n        .{ \"ks.ua\", {} },\n        .{ \"kv.ua\", {} },\n        .{ \"kyiv.ua\", {} },\n        .{ \"lg.ua\", {} },\n        .{ \"lt.ua\", {} },\n        .{ \"lugansk.ua\", {} },\n        .{ \"luhansk.ua\", {} },\n        .{ \"lutsk.ua\", {} },\n        .{ \"lv.ua\", {} },\n        .{ \"lviv.ua\", {} },\n        .{ \"mk.ua\", {} },\n        .{ \"mykolaiv.ua\", {} },\n        .{ \"nikolaev.ua\", {} },\n        .{ \"od.ua\", {} },\n        .{ \"odesa.ua\", {} },\n        .{ \"odessa.ua\", {} },\n        .{ \"pl.ua\", {} },\n        .{ \"poltava.ua\", {} },\n        .{ \"rivne.ua\", {} },\n        .{ \"rovno.ua\", {} },\n        .{ \"rv.ua\", {} },\n        .{ \"sb.ua\", {} },\n        .{ \"sebastopol.ua\", {} },\n        .{ \"sevastopol.ua\", {} },\n        .{ \"sm.ua\", {} },\n        .{ \"sumy.ua\", {} },\n        .{ \"te.ua\", {} },\n        .{ \"ternopil.ua\", {} },\n        .{ \"uz.ua\", {} },\n        .{ \"uzhgorod.ua\", {} },\n        .{ \"uzhhorod.ua\", {} },\n        .{ \"vinnica.ua\", {} },\n        .{ \"vinnytsia.ua\", {} },\n        .{ \"vn.ua\", {} },\n        .{ \"volyn.ua\", {} },\n        .{ \"yalta.ua\", {} },\n        .{ \"zakarpattia.ua\", {} },\n        .{ \"zaporizhzhe.ua\", {} },\n        .{ \"zaporizhzhia.ua\", {} },\n        .{ \"zhitomir.ua\", {} },\n        .{ \"zhytomyr.ua\", {} },\n        .{ \"zp.ua\", {} },\n        .{ \"zt.ua\", {} },\n        .{ \"ug\", {} },\n        .{ \"ac.ug\", {} },\n        .{ \"co.ug\", {} },\n        .{ \"com.ug\", {} },\n        .{ \"edu.ug\", {} },\n        .{ \"go.ug\", {} },\n        .{ \"gov.ug\", {} },\n        .{ \"mil.ug\", {} },\n        .{ \"ne.ug\", {} },\n        .{ \"or.ug\", {} },\n        .{ \"org.ug\", {} },\n        .{ \"sc.ug\", {} },\n        .{ \"us.ug\", {} },\n        .{ \"uk\", {} },\n        .{ \"ac.uk\", {} },\n        .{ \"co.uk\", {} },\n        .{ \"gov.uk\", {} },\n        .{ \"ltd.uk\", {} },\n        .{ \"me.uk\", {} },\n        .{ \"net.uk\", {} },\n        .{ \"nhs.uk\", {} },\n        .{ \"org.uk\", {} },\n        .{ \"plc.uk\", {} },\n        .{ \"police.uk\", {} },\n        .{ \"*.sch.uk\", {} },\n        .{ \"us\", {} },\n        .{ \"dni.us\", {} },\n        .{ \"isa.us\", {} },\n        .{ \"nsn.us\", {} },\n        .{ \"ak.us\", {} },\n        .{ \"al.us\", {} },\n        .{ \"ar.us\", {} },\n        .{ \"as.us\", {} },\n        .{ \"az.us\", {} },\n        .{ \"ca.us\", {} },\n        .{ \"co.us\", {} },\n        .{ \"ct.us\", {} },\n        .{ \"dc.us\", {} },\n        .{ \"de.us\", {} },\n        .{ \"fl.us\", {} },\n        .{ \"ga.us\", {} },\n        .{ \"gu.us\", {} },\n        .{ \"hi.us\", {} },\n        .{ \"ia.us\", {} },\n        .{ \"id.us\", {} },\n        .{ \"il.us\", {} },\n        .{ \"in.us\", {} },\n        .{ \"ks.us\", {} },\n        .{ \"ky.us\", {} },\n        .{ \"la.us\", {} },\n        .{ \"ma.us\", {} },\n        .{ \"md.us\", {} },\n        .{ \"me.us\", {} },\n        .{ \"mi.us\", {} },\n        .{ \"mn.us\", {} },\n        .{ \"mo.us\", {} },\n        .{ \"ms.us\", {} },\n        .{ \"mt.us\", {} },\n        .{ \"nc.us\", {} },\n        .{ \"nd.us\", {} },\n        .{ \"ne.us\", {} },\n        .{ \"nh.us\", {} },\n        .{ \"nj.us\", {} },\n        .{ \"nm.us\", {} },\n        .{ \"nv.us\", {} },\n        .{ \"ny.us\", {} },\n        .{ \"oh.us\", {} },\n        .{ \"ok.us\", {} },\n        .{ \"or.us\", {} },\n        .{ \"pa.us\", {} },\n        .{ \"pr.us\", {} },\n        .{ \"ri.us\", {} },\n        .{ \"sc.us\", {} },\n        .{ \"sd.us\", {} },\n        .{ \"tn.us\", {} },\n        .{ \"tx.us\", {} },\n        .{ \"ut.us\", {} },\n        .{ \"va.us\", {} },\n        .{ \"vi.us\", {} },\n        .{ \"vt.us\", {} },\n        .{ \"wa.us\", {} },\n        .{ \"wi.us\", {} },\n        .{ \"wv.us\", {} },\n        .{ \"wy.us\", {} },\n        .{ \"k12.ak.us\", {} },\n        .{ \"k12.al.us\", {} },\n        .{ \"k12.ar.us\", {} },\n        .{ \"k12.as.us\", {} },\n        .{ \"k12.az.us\", {} },\n        .{ \"k12.ca.us\", {} },\n        .{ \"k12.co.us\", {} },\n        .{ \"k12.ct.us\", {} },\n        .{ \"k12.dc.us\", {} },\n        .{ \"k12.fl.us\", {} },\n        .{ \"k12.ga.us\", {} },\n        .{ \"k12.gu.us\", {} },\n        .{ \"k12.ia.us\", {} },\n        .{ \"k12.id.us\", {} },\n        .{ \"k12.il.us\", {} },\n        .{ \"k12.in.us\", {} },\n        .{ \"k12.ks.us\", {} },\n        .{ \"k12.ky.us\", {} },\n        .{ \"k12.la.us\", {} },\n        .{ \"k12.ma.us\", {} },\n        .{ \"k12.md.us\", {} },\n        .{ \"k12.me.us\", {} },\n        .{ \"k12.mi.us\", {} },\n        .{ \"k12.mn.us\", {} },\n        .{ \"k12.mo.us\", {} },\n        .{ \"k12.ms.us\", {} },\n        .{ \"k12.mt.us\", {} },\n        .{ \"k12.nc.us\", {} },\n        .{ \"k12.ne.us\", {} },\n        .{ \"k12.nh.us\", {} },\n        .{ \"k12.nj.us\", {} },\n        .{ \"k12.nm.us\", {} },\n        .{ \"k12.nv.us\", {} },\n        .{ \"k12.ny.us\", {} },\n        .{ \"k12.oh.us\", {} },\n        .{ \"k12.ok.us\", {} },\n        .{ \"k12.or.us\", {} },\n        .{ \"k12.pa.us\", {} },\n        .{ \"k12.pr.us\", {} },\n        .{ \"k12.sc.us\", {} },\n        .{ \"k12.tn.us\", {} },\n        .{ \"k12.tx.us\", {} },\n        .{ \"k12.ut.us\", {} },\n        .{ \"k12.va.us\", {} },\n        .{ \"k12.vi.us\", {} },\n        .{ \"k12.vt.us\", {} },\n        .{ \"k12.wa.us\", {} },\n        .{ \"k12.wi.us\", {} },\n        .{ \"cc.ak.us\", {} },\n        .{ \"lib.ak.us\", {} },\n        .{ \"cc.al.us\", {} },\n        .{ \"lib.al.us\", {} },\n        .{ \"cc.ar.us\", {} },\n        .{ \"lib.ar.us\", {} },\n        .{ \"cc.as.us\", {} },\n        .{ \"lib.as.us\", {} },\n        .{ \"cc.az.us\", {} },\n        .{ \"lib.az.us\", {} },\n        .{ \"cc.ca.us\", {} },\n        .{ \"lib.ca.us\", {} },\n        .{ \"cc.co.us\", {} },\n        .{ \"lib.co.us\", {} },\n        .{ \"cc.ct.us\", {} },\n        .{ \"lib.ct.us\", {} },\n        .{ \"cc.dc.us\", {} },\n        .{ \"lib.dc.us\", {} },\n        .{ \"cc.de.us\", {} },\n        .{ \"cc.fl.us\", {} },\n        .{ \"lib.fl.us\", {} },\n        .{ \"cc.ga.us\", {} },\n        .{ \"lib.ga.us\", {} },\n        .{ \"cc.gu.us\", {} },\n        .{ \"lib.gu.us\", {} },\n        .{ \"cc.hi.us\", {} },\n        .{ \"lib.hi.us\", {} },\n        .{ \"cc.ia.us\", {} },\n        .{ \"lib.ia.us\", {} },\n        .{ \"cc.id.us\", {} },\n        .{ \"lib.id.us\", {} },\n        .{ \"cc.il.us\", {} },\n        .{ \"lib.il.us\", {} },\n        .{ \"cc.in.us\", {} },\n        .{ \"lib.in.us\", {} },\n        .{ \"cc.ks.us\", {} },\n        .{ \"lib.ks.us\", {} },\n        .{ \"cc.ky.us\", {} },\n        .{ \"lib.ky.us\", {} },\n        .{ \"cc.la.us\", {} },\n        .{ \"lib.la.us\", {} },\n        .{ \"cc.ma.us\", {} },\n        .{ \"lib.ma.us\", {} },\n        .{ \"cc.md.us\", {} },\n        .{ \"lib.md.us\", {} },\n        .{ \"cc.me.us\", {} },\n        .{ \"lib.me.us\", {} },\n        .{ \"cc.mi.us\", {} },\n        .{ \"lib.mi.us\", {} },\n        .{ \"cc.mn.us\", {} },\n        .{ \"lib.mn.us\", {} },\n        .{ \"cc.mo.us\", {} },\n        .{ \"lib.mo.us\", {} },\n        .{ \"cc.ms.us\", {} },\n        .{ \"cc.mt.us\", {} },\n        .{ \"lib.mt.us\", {} },\n        .{ \"cc.nc.us\", {} },\n        .{ \"lib.nc.us\", {} },\n        .{ \"cc.nd.us\", {} },\n        .{ \"lib.nd.us\", {} },\n        .{ \"cc.ne.us\", {} },\n        .{ \"lib.ne.us\", {} },\n        .{ \"cc.nh.us\", {} },\n        .{ \"lib.nh.us\", {} },\n        .{ \"cc.nj.us\", {} },\n        .{ \"lib.nj.us\", {} },\n        .{ \"cc.nm.us\", {} },\n        .{ \"lib.nm.us\", {} },\n        .{ \"cc.nv.us\", {} },\n        .{ \"lib.nv.us\", {} },\n        .{ \"cc.ny.us\", {} },\n        .{ \"lib.ny.us\", {} },\n        .{ \"cc.oh.us\", {} },\n        .{ \"lib.oh.us\", {} },\n        .{ \"cc.ok.us\", {} },\n        .{ \"lib.ok.us\", {} },\n        .{ \"cc.or.us\", {} },\n        .{ \"lib.or.us\", {} },\n        .{ \"cc.pa.us\", {} },\n        .{ \"lib.pa.us\", {} },\n        .{ \"cc.pr.us\", {} },\n        .{ \"lib.pr.us\", {} },\n        .{ \"cc.ri.us\", {} },\n        .{ \"lib.ri.us\", {} },\n        .{ \"cc.sc.us\", {} },\n        .{ \"lib.sc.us\", {} },\n        .{ \"cc.sd.us\", {} },\n        .{ \"lib.sd.us\", {} },\n        .{ \"cc.tn.us\", {} },\n        .{ \"lib.tn.us\", {} },\n        .{ \"cc.tx.us\", {} },\n        .{ \"lib.tx.us\", {} },\n        .{ \"cc.ut.us\", {} },\n        .{ \"lib.ut.us\", {} },\n        .{ \"cc.va.us\", {} },\n        .{ \"lib.va.us\", {} },\n        .{ \"cc.vi.us\", {} },\n        .{ \"lib.vi.us\", {} },\n        .{ \"cc.vt.us\", {} },\n        .{ \"lib.vt.us\", {} },\n        .{ \"cc.wa.us\", {} },\n        .{ \"lib.wa.us\", {} },\n        .{ \"cc.wi.us\", {} },\n        .{ \"lib.wi.us\", {} },\n        .{ \"cc.wv.us\", {} },\n        .{ \"cc.wy.us\", {} },\n        .{ \"k12.wy.us\", {} },\n        .{ \"lib.wy.us\", {} },\n        .{ \"chtr.k12.ma.us\", {} },\n        .{ \"paroch.k12.ma.us\", {} },\n        .{ \"pvt.k12.ma.us\", {} },\n        .{ \"ann-arbor.mi.us\", {} },\n        .{ \"cog.mi.us\", {} },\n        .{ \"dst.mi.us\", {} },\n        .{ \"eaton.mi.us\", {} },\n        .{ \"gen.mi.us\", {} },\n        .{ \"mus.mi.us\", {} },\n        .{ \"tec.mi.us\", {} },\n        .{ \"washtenaw.mi.us\", {} },\n        .{ \"uy\", {} },\n        .{ \"com.uy\", {} },\n        .{ \"edu.uy\", {} },\n        .{ \"gub.uy\", {} },\n        .{ \"mil.uy\", {} },\n        .{ \"net.uy\", {} },\n        .{ \"org.uy\", {} },\n        .{ \"uz\", {} },\n        .{ \"co.uz\", {} },\n        .{ \"com.uz\", {} },\n        .{ \"net.uz\", {} },\n        .{ \"org.uz\", {} },\n        .{ \"va\", {} },\n        .{ \"vc\", {} },\n        .{ \"com.vc\", {} },\n        .{ \"edu.vc\", {} },\n        .{ \"gov.vc\", {} },\n        .{ \"mil.vc\", {} },\n        .{ \"net.vc\", {} },\n        .{ \"org.vc\", {} },\n        .{ \"ve\", {} },\n        .{ \"arts.ve\", {} },\n        .{ \"bib.ve\", {} },\n        .{ \"co.ve\", {} },\n        .{ \"com.ve\", {} },\n        .{ \"e12.ve\", {} },\n        .{ \"edu.ve\", {} },\n        .{ \"emprende.ve\", {} },\n        .{ \"firm.ve\", {} },\n        .{ \"gob.ve\", {} },\n        .{ \"gov.ve\", {} },\n        .{ \"ia.ve\", {} },\n        .{ \"info.ve\", {} },\n        .{ \"int.ve\", {} },\n        .{ \"mil.ve\", {} },\n        .{ \"net.ve\", {} },\n        .{ \"nom.ve\", {} },\n        .{ \"org.ve\", {} },\n        .{ \"rar.ve\", {} },\n        .{ \"rec.ve\", {} },\n        .{ \"store.ve\", {} },\n        .{ \"tec.ve\", {} },\n        .{ \"web.ve\", {} },\n        .{ \"vg\", {} },\n        .{ \"edu.vg\", {} },\n        .{ \"vi\", {} },\n        .{ \"co.vi\", {} },\n        .{ \"com.vi\", {} },\n        .{ \"k12.vi\", {} },\n        .{ \"net.vi\", {} },\n        .{ \"org.vi\", {} },\n        .{ \"vn\", {} },\n        .{ \"ac.vn\", {} },\n        .{ \"ai.vn\", {} },\n        .{ \"biz.vn\", {} },\n        .{ \"com.vn\", {} },\n        .{ \"edu.vn\", {} },\n        .{ \"gov.vn\", {} },\n        .{ \"health.vn\", {} },\n        .{ \"id.vn\", {} },\n        .{ \"info.vn\", {} },\n        .{ \"int.vn\", {} },\n        .{ \"io.vn\", {} },\n        .{ \"name.vn\", {} },\n        .{ \"net.vn\", {} },\n        .{ \"org.vn\", {} },\n        .{ \"pro.vn\", {} },\n        .{ \"angiang.vn\", {} },\n        .{ \"bacgiang.vn\", {} },\n        .{ \"backan.vn\", {} },\n        .{ \"baclieu.vn\", {} },\n        .{ \"bacninh.vn\", {} },\n        .{ \"baria-vungtau.vn\", {} },\n        .{ \"bentre.vn\", {} },\n        .{ \"binhdinh.vn\", {} },\n        .{ \"binhduong.vn\", {} },\n        .{ \"binhphuoc.vn\", {} },\n        .{ \"binhthuan.vn\", {} },\n        .{ \"camau.vn\", {} },\n        .{ \"cantho.vn\", {} },\n        .{ \"caobang.vn\", {} },\n        .{ \"daklak.vn\", {} },\n        .{ \"daknong.vn\", {} },\n        .{ \"danang.vn\", {} },\n        .{ \"dienbien.vn\", {} },\n        .{ \"dongnai.vn\", {} },\n        .{ \"dongthap.vn\", {} },\n        .{ \"gialai.vn\", {} },\n        .{ \"hagiang.vn\", {} },\n        .{ \"haiduong.vn\", {} },\n        .{ \"haiphong.vn\", {} },\n        .{ \"hanam.vn\", {} },\n        .{ \"hanoi.vn\", {} },\n        .{ \"hatinh.vn\", {} },\n        .{ \"haugiang.vn\", {} },\n        .{ \"hoabinh.vn\", {} },\n        .{ \"hungyen.vn\", {} },\n        .{ \"khanhhoa.vn\", {} },\n        .{ \"kiengiang.vn\", {} },\n        .{ \"kontum.vn\", {} },\n        .{ \"laichau.vn\", {} },\n        .{ \"lamdong.vn\", {} },\n        .{ \"langson.vn\", {} },\n        .{ \"laocai.vn\", {} },\n        .{ \"longan.vn\", {} },\n        .{ \"namdinh.vn\", {} },\n        .{ \"nghean.vn\", {} },\n        .{ \"ninhbinh.vn\", {} },\n        .{ \"ninhthuan.vn\", {} },\n        .{ \"phutho.vn\", {} },\n        .{ \"phuyen.vn\", {} },\n        .{ \"quangbinh.vn\", {} },\n        .{ \"quangnam.vn\", {} },\n        .{ \"quangngai.vn\", {} },\n        .{ \"quangninh.vn\", {} },\n        .{ \"quangtri.vn\", {} },\n        .{ \"soctrang.vn\", {} },\n        .{ \"sonla.vn\", {} },\n        .{ \"tayninh.vn\", {} },\n        .{ \"thaibinh.vn\", {} },\n        .{ \"thainguyen.vn\", {} },\n        .{ \"thanhhoa.vn\", {} },\n        .{ \"thanhphohochiminh.vn\", {} },\n        .{ \"thuathienhue.vn\", {} },\n        .{ \"tiengiang.vn\", {} },\n        .{ \"travinh.vn\", {} },\n        .{ \"tuyenquang.vn\", {} },\n        .{ \"vinhlong.vn\", {} },\n        .{ \"vinhphuc.vn\", {} },\n        .{ \"yenbai.vn\", {} },\n        .{ \"vu\", {} },\n        .{ \"com.vu\", {} },\n        .{ \"edu.vu\", {} },\n        .{ \"net.vu\", {} },\n        .{ \"org.vu\", {} },\n        .{ \"wf\", {} },\n        .{ \"ws\", {} },\n        .{ \"com.ws\", {} },\n        .{ \"edu.ws\", {} },\n        .{ \"gov.ws\", {} },\n        .{ \"net.ws\", {} },\n        .{ \"org.ws\", {} },\n        .{ \"yt\", {} },\n        .{ \"امارات\", {} },\n        .{ \"հայ\", {} },\n        .{ \"বাংলা\", {} },\n        .{ \"бг\", {} },\n        .{ \"البحرين\", {} },\n        .{ \"бел\", {} },\n        .{ \"中国\", {} },\n        .{ \"中國\", {} },\n        .{ \"الجزائر\", {} },\n        .{ \"مصر\", {} },\n        .{ \"ею\", {} },\n        .{ \"ευ\", {} },\n        .{ \"موريتانيا\", {} },\n        .{ \"გე\", {} },\n        .{ \"ελ\", {} },\n        .{ \"香港\", {} },\n        .{ \"個人.香港\", {} },\n        .{ \"公司.香港\", {} },\n        .{ \"政府.香港\", {} },\n        .{ \"教育.香港\", {} },\n        .{ \"組織.香港\", {} },\n        .{ \"網絡.香港\", {} },\n        .{ \"ಭಾರತ\", {} },\n        .{ \"ଭାରତ\", {} },\n        .{ \"ভাৰত\", {} },\n        .{ \"भारतम्\", {} },\n        .{ \"भारोत\", {} },\n        .{ \"ڀارت\", {} },\n        .{ \"ഭാരതം\", {} },\n        .{ \"भारत\", {} },\n        .{ \"بارت\", {} },\n        .{ \"بھارت\", {} },\n        .{ \"భారత్\", {} },\n        .{ \"ભારત\", {} },\n        .{ \"ਭਾਰਤ\", {} },\n        .{ \"ভারত\", {} },\n        .{ \"இந்தியா\", {} },\n        .{ \"ایران\", {} },\n        .{ \"ايران\", {} },\n        .{ \"عراق\", {} },\n        .{ \"الاردن\", {} },\n        .{ \"한국\", {} },\n        .{ \"қаз\", {} },\n        .{ \"ລາວ\", {} },\n        .{ \"ලංකා\", {} },\n        .{ \"இலங்கை\", {} },\n        .{ \"المغرب\", {} },\n        .{ \"мкд\", {} },\n        .{ \"мон\", {} },\n        .{ \"澳門\", {} },\n        .{ \"澳门\", {} },\n        .{ \"مليسيا\", {} },\n        .{ \"عمان\", {} },\n        .{ \"پاکستان\", {} },\n        .{ \"پاكستان\", {} },\n        .{ \"فلسطين\", {} },\n        .{ \"срб\", {} },\n        .{ \"ак.срб\", {} },\n        .{ \"обр.срб\", {} },\n        .{ \"од.срб\", {} },\n        .{ \"орг.срб\", {} },\n        .{ \"пр.срб\", {} },\n        .{ \"упр.срб\", {} },\n        .{ \"рф\", {} },\n        .{ \"قطر\", {} },\n        .{ \"السعودية\", {} },\n        .{ \"السعودیة\", {} },\n        .{ \"السعودیۃ\", {} },\n        .{ \"السعوديه\", {} },\n        .{ \"سودان\", {} },\n        .{ \"新加坡\", {} },\n        .{ \"சிங்கப்பூர்\", {} },\n        .{ \"سورية\", {} },\n        .{ \"سوريا\", {} },\n        .{ \"ไทย\", {} },\n        .{ \"ทหาร.ไทย\", {} },\n        .{ \"ธุรกิจ.ไทย\", {} },\n        .{ \"เน็ต.ไทย\", {} },\n        .{ \"รัฐบาล.ไทย\", {} },\n        .{ \"ศึกษา.ไทย\", {} },\n        .{ \"องค์กร.ไทย\", {} },\n        .{ \"تونس\", {} },\n        .{ \"台灣\", {} },\n        .{ \"台湾\", {} },\n        .{ \"臺灣\", {} },\n        .{ \"укр\", {} },\n        .{ \"اليمن\", {} },\n        .{ \"xxx\", {} },\n        .{ \"ye\", {} },\n        .{ \"com.ye\", {} },\n        .{ \"edu.ye\", {} },\n        .{ \"gov.ye\", {} },\n        .{ \"mil.ye\", {} },\n        .{ \"net.ye\", {} },\n        .{ \"org.ye\", {} },\n        .{ \"ac.za\", {} },\n        .{ \"agric.za\", {} },\n        .{ \"alt.za\", {} },\n        .{ \"co.za\", {} },\n        .{ \"edu.za\", {} },\n        .{ \"gov.za\", {} },\n        .{ \"grondar.za\", {} },\n        .{ \"law.za\", {} },\n        .{ \"mil.za\", {} },\n        .{ \"net.za\", {} },\n        .{ \"ngo.za\", {} },\n        .{ \"nic.za\", {} },\n        .{ \"nis.za\", {} },\n        .{ \"nom.za\", {} },\n        .{ \"org.za\", {} },\n        .{ \"school.za\", {} },\n        .{ \"tm.za\", {} },\n        .{ \"web.za\", {} },\n        .{ \"zm\", {} },\n        .{ \"ac.zm\", {} },\n        .{ \"biz.zm\", {} },\n        .{ \"co.zm\", {} },\n        .{ \"com.zm\", {} },\n        .{ \"edu.zm\", {} },\n        .{ \"gov.zm\", {} },\n        .{ \"info.zm\", {} },\n        .{ \"mil.zm\", {} },\n        .{ \"net.zm\", {} },\n        .{ \"org.zm\", {} },\n        .{ \"sch.zm\", {} },\n        .{ \"zw\", {} },\n        .{ \"ac.zw\", {} },\n        .{ \"co.zw\", {} },\n        .{ \"gov.zw\", {} },\n        .{ \"mil.zw\", {} },\n        .{ \"org.zw\", {} },\n        .{ \"aaa\", {} },\n        .{ \"aarp\", {} },\n        .{ \"abb\", {} },\n        .{ \"abbott\", {} },\n        .{ \"abbvie\", {} },\n        .{ \"abc\", {} },\n        .{ \"able\", {} },\n        .{ \"abogado\", {} },\n        .{ \"abudhabi\", {} },\n        .{ \"academy\", {} },\n        .{ \"accenture\", {} },\n        .{ \"accountant\", {} },\n        .{ \"accountants\", {} },\n        .{ \"aco\", {} },\n        .{ \"actor\", {} },\n        .{ \"ads\", {} },\n        .{ \"adult\", {} },\n        .{ \"aeg\", {} },\n        .{ \"aetna\", {} },\n        .{ \"afl\", {} },\n        .{ \"africa\", {} },\n        .{ \"agakhan\", {} },\n        .{ \"agency\", {} },\n        .{ \"aig\", {} },\n        .{ \"airbus\", {} },\n        .{ \"airforce\", {} },\n        .{ \"airtel\", {} },\n        .{ \"akdn\", {} },\n        .{ \"alibaba\", {} },\n        .{ \"alipay\", {} },\n        .{ \"allfinanz\", {} },\n        .{ \"allstate\", {} },\n        .{ \"ally\", {} },\n        .{ \"alsace\", {} },\n        .{ \"alstom\", {} },\n        .{ \"amazon\", {} },\n        .{ \"americanexpress\", {} },\n        .{ \"americanfamily\", {} },\n        .{ \"amex\", {} },\n        .{ \"amfam\", {} },\n        .{ \"amica\", {} },\n        .{ \"amsterdam\", {} },\n        .{ \"analytics\", {} },\n        .{ \"android\", {} },\n        .{ \"anquan\", {} },\n        .{ \"anz\", {} },\n        .{ \"aol\", {} },\n        .{ \"apartments\", {} },\n        .{ \"app\", {} },\n        .{ \"apple\", {} },\n        .{ \"aquarelle\", {} },\n        .{ \"arab\", {} },\n        .{ \"aramco\", {} },\n        .{ \"archi\", {} },\n        .{ \"army\", {} },\n        .{ \"art\", {} },\n        .{ \"arte\", {} },\n        .{ \"asda\", {} },\n        .{ \"associates\", {} },\n        .{ \"athleta\", {} },\n        .{ \"attorney\", {} },\n        .{ \"auction\", {} },\n        .{ \"audi\", {} },\n        .{ \"audible\", {} },\n        .{ \"audio\", {} },\n        .{ \"auspost\", {} },\n        .{ \"author\", {} },\n        .{ \"auto\", {} },\n        .{ \"autos\", {} },\n        .{ \"aws\", {} },\n        .{ \"axa\", {} },\n        .{ \"azure\", {} },\n        .{ \"baby\", {} },\n        .{ \"baidu\", {} },\n        .{ \"banamex\", {} },\n        .{ \"band\", {} },\n        .{ \"bank\", {} },\n        .{ \"bar\", {} },\n        .{ \"barcelona\", {} },\n        .{ \"barclaycard\", {} },\n        .{ \"barclays\", {} },\n        .{ \"barefoot\", {} },\n        .{ \"bargains\", {} },\n        .{ \"baseball\", {} },\n        .{ \"basketball\", {} },\n        .{ \"bauhaus\", {} },\n        .{ \"bayern\", {} },\n        .{ \"bbc\", {} },\n        .{ \"bbt\", {} },\n        .{ \"bbva\", {} },\n        .{ \"bcg\", {} },\n        .{ \"bcn\", {} },\n        .{ \"beats\", {} },\n        .{ \"beauty\", {} },\n        .{ \"beer\", {} },\n        .{ \"berlin\", {} },\n        .{ \"best\", {} },\n        .{ \"bestbuy\", {} },\n        .{ \"bet\", {} },\n        .{ \"bharti\", {} },\n        .{ \"bible\", {} },\n        .{ \"bid\", {} },\n        .{ \"bike\", {} },\n        .{ \"bing\", {} },\n        .{ \"bingo\", {} },\n        .{ \"bio\", {} },\n        .{ \"black\", {} },\n        .{ \"blackfriday\", {} },\n        .{ \"blockbuster\", {} },\n        .{ \"blog\", {} },\n        .{ \"bloomberg\", {} },\n        .{ \"blue\", {} },\n        .{ \"bms\", {} },\n        .{ \"bmw\", {} },\n        .{ \"bnpparibas\", {} },\n        .{ \"boats\", {} },\n        .{ \"boehringer\", {} },\n        .{ \"bofa\", {} },\n        .{ \"bom\", {} },\n        .{ \"bond\", {} },\n        .{ \"boo\", {} },\n        .{ \"book\", {} },\n        .{ \"booking\", {} },\n        .{ \"bosch\", {} },\n        .{ \"bostik\", {} },\n        .{ \"boston\", {} },\n        .{ \"bot\", {} },\n        .{ \"boutique\", {} },\n        .{ \"box\", {} },\n        .{ \"bradesco\", {} },\n        .{ \"bridgestone\", {} },\n        .{ \"broadway\", {} },\n        .{ \"broker\", {} },\n        .{ \"brother\", {} },\n        .{ \"brussels\", {} },\n        .{ \"build\", {} },\n        .{ \"builders\", {} },\n        .{ \"business\", {} },\n        .{ \"buy\", {} },\n        .{ \"buzz\", {} },\n        .{ \"bzh\", {} },\n        .{ \"cab\", {} },\n        .{ \"cafe\", {} },\n        .{ \"cal\", {} },\n        .{ \"call\", {} },\n        .{ \"calvinklein\", {} },\n        .{ \"cam\", {} },\n        .{ \"camera\", {} },\n        .{ \"camp\", {} },\n        .{ \"canon\", {} },\n        .{ \"capetown\", {} },\n        .{ \"capital\", {} },\n        .{ \"capitalone\", {} },\n        .{ \"car\", {} },\n        .{ \"caravan\", {} },\n        .{ \"cards\", {} },\n        .{ \"care\", {} },\n        .{ \"career\", {} },\n        .{ \"careers\", {} },\n        .{ \"cars\", {} },\n        .{ \"casa\", {} },\n        .{ \"case\", {} },\n        .{ \"cash\", {} },\n        .{ \"casino\", {} },\n        .{ \"catering\", {} },\n        .{ \"catholic\", {} },\n        .{ \"cba\", {} },\n        .{ \"cbn\", {} },\n        .{ \"cbre\", {} },\n        .{ \"center\", {} },\n        .{ \"ceo\", {} },\n        .{ \"cern\", {} },\n        .{ \"cfa\", {} },\n        .{ \"cfd\", {} },\n        .{ \"chanel\", {} },\n        .{ \"channel\", {} },\n        .{ \"charity\", {} },\n        .{ \"chase\", {} },\n        .{ \"chat\", {} },\n        .{ \"cheap\", {} },\n        .{ \"chintai\", {} },\n        .{ \"christmas\", {} },\n        .{ \"chrome\", {} },\n        .{ \"church\", {} },\n        .{ \"cipriani\", {} },\n        .{ \"circle\", {} },\n        .{ \"cisco\", {} },\n        .{ \"citadel\", {} },\n        .{ \"citi\", {} },\n        .{ \"citic\", {} },\n        .{ \"city\", {} },\n        .{ \"claims\", {} },\n        .{ \"cleaning\", {} },\n        .{ \"click\", {} },\n        .{ \"clinic\", {} },\n        .{ \"clinique\", {} },\n        .{ \"clothing\", {} },\n        .{ \"cloud\", {} },\n        .{ \"club\", {} },\n        .{ \"clubmed\", {} },\n        .{ \"coach\", {} },\n        .{ \"codes\", {} },\n        .{ \"coffee\", {} },\n        .{ \"college\", {} },\n        .{ \"cologne\", {} },\n        .{ \"commbank\", {} },\n        .{ \"community\", {} },\n        .{ \"company\", {} },\n        .{ \"compare\", {} },\n        .{ \"computer\", {} },\n        .{ \"comsec\", {} },\n        .{ \"condos\", {} },\n        .{ \"construction\", {} },\n        .{ \"consulting\", {} },\n        .{ \"contact\", {} },\n        .{ \"contractors\", {} },\n        .{ \"cooking\", {} },\n        .{ \"cool\", {} },\n        .{ \"corsica\", {} },\n        .{ \"country\", {} },\n        .{ \"coupon\", {} },\n        .{ \"coupons\", {} },\n        .{ \"courses\", {} },\n        .{ \"cpa\", {} },\n        .{ \"credit\", {} },\n        .{ \"creditcard\", {} },\n        .{ \"creditunion\", {} },\n        .{ \"cricket\", {} },\n        .{ \"crown\", {} },\n        .{ \"crs\", {} },\n        .{ \"cruise\", {} },\n        .{ \"cruises\", {} },\n        .{ \"cuisinella\", {} },\n        .{ \"cymru\", {} },\n        .{ \"cyou\", {} },\n        .{ \"dad\", {} },\n        .{ \"dance\", {} },\n        .{ \"data\", {} },\n        .{ \"date\", {} },\n        .{ \"dating\", {} },\n        .{ \"datsun\", {} },\n        .{ \"day\", {} },\n        .{ \"dclk\", {} },\n        .{ \"dds\", {} },\n        .{ \"deal\", {} },\n        .{ \"dealer\", {} },\n        .{ \"deals\", {} },\n        .{ \"degree\", {} },\n        .{ \"delivery\", {} },\n        .{ \"dell\", {} },\n        .{ \"deloitte\", {} },\n        .{ \"delta\", {} },\n        .{ \"democrat\", {} },\n        .{ \"dental\", {} },\n        .{ \"dentist\", {} },\n        .{ \"desi\", {} },\n        .{ \"design\", {} },\n        .{ \"dev\", {} },\n        .{ \"dhl\", {} },\n        .{ \"diamonds\", {} },\n        .{ \"diet\", {} },\n        .{ \"digital\", {} },\n        .{ \"direct\", {} },\n        .{ \"directory\", {} },\n        .{ \"discount\", {} },\n        .{ \"discover\", {} },\n        .{ \"dish\", {} },\n        .{ \"diy\", {} },\n        .{ \"dnp\", {} },\n        .{ \"docs\", {} },\n        .{ \"doctor\", {} },\n        .{ \"dog\", {} },\n        .{ \"domains\", {} },\n        .{ \"dot\", {} },\n        .{ \"download\", {} },\n        .{ \"drive\", {} },\n        .{ \"dtv\", {} },\n        .{ \"dubai\", {} },\n        .{ \"dupont\", {} },\n        .{ \"durban\", {} },\n        .{ \"dvag\", {} },\n        .{ \"dvr\", {} },\n        .{ \"earth\", {} },\n        .{ \"eat\", {} },\n        .{ \"eco\", {} },\n        .{ \"edeka\", {} },\n        .{ \"education\", {} },\n        .{ \"email\", {} },\n        .{ \"emerck\", {} },\n        .{ \"energy\", {} },\n        .{ \"engineer\", {} },\n        .{ \"engineering\", {} },\n        .{ \"enterprises\", {} },\n        .{ \"epson\", {} },\n        .{ \"equipment\", {} },\n        .{ \"ericsson\", {} },\n        .{ \"erni\", {} },\n        .{ \"esq\", {} },\n        .{ \"estate\", {} },\n        .{ \"eurovision\", {} },\n        .{ \"eus\", {} },\n        .{ \"events\", {} },\n        .{ \"exchange\", {} },\n        .{ \"expert\", {} },\n        .{ \"exposed\", {} },\n        .{ \"express\", {} },\n        .{ \"extraspace\", {} },\n        .{ \"fage\", {} },\n        .{ \"fail\", {} },\n        .{ \"fairwinds\", {} },\n        .{ \"faith\", {} },\n        .{ \"family\", {} },\n        .{ \"fan\", {} },\n        .{ \"fans\", {} },\n        .{ \"farm\", {} },\n        .{ \"farmers\", {} },\n        .{ \"fashion\", {} },\n        .{ \"fast\", {} },\n        .{ \"fedex\", {} },\n        .{ \"feedback\", {} },\n        .{ \"ferrari\", {} },\n        .{ \"ferrero\", {} },\n        .{ \"fidelity\", {} },\n        .{ \"fido\", {} },\n        .{ \"film\", {} },\n        .{ \"final\", {} },\n        .{ \"finance\", {} },\n        .{ \"financial\", {} },\n        .{ \"fire\", {} },\n        .{ \"firestone\", {} },\n        .{ \"firmdale\", {} },\n        .{ \"fish\", {} },\n        .{ \"fishing\", {} },\n        .{ \"fit\", {} },\n        .{ \"fitness\", {} },\n        .{ \"flickr\", {} },\n        .{ \"flights\", {} },\n        .{ \"flir\", {} },\n        .{ \"florist\", {} },\n        .{ \"flowers\", {} },\n        .{ \"fly\", {} },\n        .{ \"foo\", {} },\n        .{ \"food\", {} },\n        .{ \"football\", {} },\n        .{ \"ford\", {} },\n        .{ \"forex\", {} },\n        .{ \"forsale\", {} },\n        .{ \"forum\", {} },\n        .{ \"foundation\", {} },\n        .{ \"fox\", {} },\n        .{ \"free\", {} },\n        .{ \"fresenius\", {} },\n        .{ \"frl\", {} },\n        .{ \"frogans\", {} },\n        .{ \"frontier\", {} },\n        .{ \"ftr\", {} },\n        .{ \"fujitsu\", {} },\n        .{ \"fun\", {} },\n        .{ \"fund\", {} },\n        .{ \"furniture\", {} },\n        .{ \"futbol\", {} },\n        .{ \"fyi\", {} },\n        .{ \"gal\", {} },\n        .{ \"gallery\", {} },\n        .{ \"gallo\", {} },\n        .{ \"gallup\", {} },\n        .{ \"game\", {} },\n        .{ \"games\", {} },\n        .{ \"gap\", {} },\n        .{ \"garden\", {} },\n        .{ \"gay\", {} },\n        .{ \"gbiz\", {} },\n        .{ \"gdn\", {} },\n        .{ \"gea\", {} },\n        .{ \"gent\", {} },\n        .{ \"genting\", {} },\n        .{ \"george\", {} },\n        .{ \"ggee\", {} },\n        .{ \"gift\", {} },\n        .{ \"gifts\", {} },\n        .{ \"gives\", {} },\n        .{ \"giving\", {} },\n        .{ \"glass\", {} },\n        .{ \"gle\", {} },\n        .{ \"global\", {} },\n        .{ \"globo\", {} },\n        .{ \"gmail\", {} },\n        .{ \"gmbh\", {} },\n        .{ \"gmo\", {} },\n        .{ \"gmx\", {} },\n        .{ \"godaddy\", {} },\n        .{ \"gold\", {} },\n        .{ \"goldpoint\", {} },\n        .{ \"golf\", {} },\n        .{ \"goo\", {} },\n        .{ \"goodyear\", {} },\n        .{ \"goog\", {} },\n        .{ \"google\", {} },\n        .{ \"gop\", {} },\n        .{ \"got\", {} },\n        .{ \"grainger\", {} },\n        .{ \"graphics\", {} },\n        .{ \"gratis\", {} },\n        .{ \"green\", {} },\n        .{ \"gripe\", {} },\n        .{ \"grocery\", {} },\n        .{ \"group\", {} },\n        .{ \"gucci\", {} },\n        .{ \"guge\", {} },\n        .{ \"guide\", {} },\n        .{ \"guitars\", {} },\n        .{ \"guru\", {} },\n        .{ \"hair\", {} },\n        .{ \"hamburg\", {} },\n        .{ \"hangout\", {} },\n        .{ \"haus\", {} },\n        .{ \"hbo\", {} },\n        .{ \"hdfc\", {} },\n        .{ \"hdfcbank\", {} },\n        .{ \"health\", {} },\n        .{ \"healthcare\", {} },\n        .{ \"help\", {} },\n        .{ \"helsinki\", {} },\n        .{ \"here\", {} },\n        .{ \"hermes\", {} },\n        .{ \"hiphop\", {} },\n        .{ \"hisamitsu\", {} },\n        .{ \"hitachi\", {} },\n        .{ \"hiv\", {} },\n        .{ \"hkt\", {} },\n        .{ \"hockey\", {} },\n        .{ \"holdings\", {} },\n        .{ \"holiday\", {} },\n        .{ \"homedepot\", {} },\n        .{ \"homegoods\", {} },\n        .{ \"homes\", {} },\n        .{ \"homesense\", {} },\n        .{ \"honda\", {} },\n        .{ \"horse\", {} },\n        .{ \"hospital\", {} },\n        .{ \"host\", {} },\n        .{ \"hosting\", {} },\n        .{ \"hot\", {} },\n        .{ \"hotel\", {} },\n        .{ \"hotels\", {} },\n        .{ \"hotmail\", {} },\n        .{ \"house\", {} },\n        .{ \"how\", {} },\n        .{ \"hsbc\", {} },\n        .{ \"hughes\", {} },\n        .{ \"hyatt\", {} },\n        .{ \"hyundai\", {} },\n        .{ \"ibm\", {} },\n        .{ \"icbc\", {} },\n        .{ \"ice\", {} },\n        .{ \"icu\", {} },\n        .{ \"ieee\", {} },\n        .{ \"ifm\", {} },\n        .{ \"ikano\", {} },\n        .{ \"imamat\", {} },\n        .{ \"imdb\", {} },\n        .{ \"immo\", {} },\n        .{ \"immobilien\", {} },\n        .{ \"inc\", {} },\n        .{ \"industries\", {} },\n        .{ \"infiniti\", {} },\n        .{ \"ing\", {} },\n        .{ \"ink\", {} },\n        .{ \"institute\", {} },\n        .{ \"insurance\", {} },\n        .{ \"insure\", {} },\n        .{ \"international\", {} },\n        .{ \"intuit\", {} },\n        .{ \"investments\", {} },\n        .{ \"ipiranga\", {} },\n        .{ \"irish\", {} },\n        .{ \"ismaili\", {} },\n        .{ \"ist\", {} },\n        .{ \"istanbul\", {} },\n        .{ \"itau\", {} },\n        .{ \"itv\", {} },\n        .{ \"jaguar\", {} },\n        .{ \"java\", {} },\n        .{ \"jcb\", {} },\n        .{ \"jeep\", {} },\n        .{ \"jetzt\", {} },\n        .{ \"jewelry\", {} },\n        .{ \"jio\", {} },\n        .{ \"jll\", {} },\n        .{ \"jmp\", {} },\n        .{ \"jnj\", {} },\n        .{ \"joburg\", {} },\n        .{ \"jot\", {} },\n        .{ \"joy\", {} },\n        .{ \"jpmorgan\", {} },\n        .{ \"jprs\", {} },\n        .{ \"juegos\", {} },\n        .{ \"juniper\", {} },\n        .{ \"kaufen\", {} },\n        .{ \"kddi\", {} },\n        .{ \"kerryhotels\", {} },\n        .{ \"kerryproperties\", {} },\n        .{ \"kfh\", {} },\n        .{ \"kia\", {} },\n        .{ \"kids\", {} },\n        .{ \"kim\", {} },\n        .{ \"kindle\", {} },\n        .{ \"kitchen\", {} },\n        .{ \"kiwi\", {} },\n        .{ \"koeln\", {} },\n        .{ \"komatsu\", {} },\n        .{ \"kosher\", {} },\n        .{ \"kpmg\", {} },\n        .{ \"kpn\", {} },\n        .{ \"krd\", {} },\n        .{ \"kred\", {} },\n        .{ \"kuokgroup\", {} },\n        .{ \"kyoto\", {} },\n        .{ \"lacaixa\", {} },\n        .{ \"lamborghini\", {} },\n        .{ \"lamer\", {} },\n        .{ \"land\", {} },\n        .{ \"landrover\", {} },\n        .{ \"lanxess\", {} },\n        .{ \"lasalle\", {} },\n        .{ \"lat\", {} },\n        .{ \"latino\", {} },\n        .{ \"latrobe\", {} },\n        .{ \"law\", {} },\n        .{ \"lawyer\", {} },\n        .{ \"lds\", {} },\n        .{ \"lease\", {} },\n        .{ \"leclerc\", {} },\n        .{ \"lefrak\", {} },\n        .{ \"legal\", {} },\n        .{ \"lego\", {} },\n        .{ \"lexus\", {} },\n        .{ \"lgbt\", {} },\n        .{ \"lidl\", {} },\n        .{ \"life\", {} },\n        .{ \"lifeinsurance\", {} },\n        .{ \"lifestyle\", {} },\n        .{ \"lighting\", {} },\n        .{ \"like\", {} },\n        .{ \"lilly\", {} },\n        .{ \"limited\", {} },\n        .{ \"limo\", {} },\n        .{ \"lincoln\", {} },\n        .{ \"link\", {} },\n        .{ \"live\", {} },\n        .{ \"living\", {} },\n        .{ \"llc\", {} },\n        .{ \"llp\", {} },\n        .{ \"loan\", {} },\n        .{ \"loans\", {} },\n        .{ \"locker\", {} },\n        .{ \"locus\", {} },\n        .{ \"lol\", {} },\n        .{ \"london\", {} },\n        .{ \"lotte\", {} },\n        .{ \"lotto\", {} },\n        .{ \"love\", {} },\n        .{ \"lpl\", {} },\n        .{ \"lplfinancial\", {} },\n        .{ \"ltd\", {} },\n        .{ \"ltda\", {} },\n        .{ \"lundbeck\", {} },\n        .{ \"luxe\", {} },\n        .{ \"luxury\", {} },\n        .{ \"madrid\", {} },\n        .{ \"maif\", {} },\n        .{ \"maison\", {} },\n        .{ \"makeup\", {} },\n        .{ \"man\", {} },\n        .{ \"management\", {} },\n        .{ \"mango\", {} },\n        .{ \"map\", {} },\n        .{ \"market\", {} },\n        .{ \"marketing\", {} },\n        .{ \"markets\", {} },\n        .{ \"marriott\", {} },\n        .{ \"marshalls\", {} },\n        .{ \"mattel\", {} },\n        .{ \"mba\", {} },\n        .{ \"mckinsey\", {} },\n        .{ \"med\", {} },\n        .{ \"media\", {} },\n        .{ \"meet\", {} },\n        .{ \"melbourne\", {} },\n        .{ \"meme\", {} },\n        .{ \"memorial\", {} },\n        .{ \"men\", {} },\n        .{ \"menu\", {} },\n        .{ \"merck\", {} },\n        .{ \"merckmsd\", {} },\n        .{ \"miami\", {} },\n        .{ \"microsoft\", {} },\n        .{ \"mini\", {} },\n        .{ \"mint\", {} },\n        .{ \"mit\", {} },\n        .{ \"mitsubishi\", {} },\n        .{ \"mlb\", {} },\n        .{ \"mls\", {} },\n        .{ \"mma\", {} },\n        .{ \"mobile\", {} },\n        .{ \"moda\", {} },\n        .{ \"moe\", {} },\n        .{ \"moi\", {} },\n        .{ \"mom\", {} },\n        .{ \"monash\", {} },\n        .{ \"money\", {} },\n        .{ \"monster\", {} },\n        .{ \"mormon\", {} },\n        .{ \"mortgage\", {} },\n        .{ \"moscow\", {} },\n        .{ \"moto\", {} },\n        .{ \"motorcycles\", {} },\n        .{ \"mov\", {} },\n        .{ \"movie\", {} },\n        .{ \"msd\", {} },\n        .{ \"mtn\", {} },\n        .{ \"mtr\", {} },\n        .{ \"music\", {} },\n        .{ \"nab\", {} },\n        .{ \"nagoya\", {} },\n        .{ \"navy\", {} },\n        .{ \"nba\", {} },\n        .{ \"nec\", {} },\n        .{ \"netbank\", {} },\n        .{ \"netflix\", {} },\n        .{ \"network\", {} },\n        .{ \"neustar\", {} },\n        .{ \"new\", {} },\n        .{ \"news\", {} },\n        .{ \"next\", {} },\n        .{ \"nextdirect\", {} },\n        .{ \"nexus\", {} },\n        .{ \"nfl\", {} },\n        .{ \"ngo\", {} },\n        .{ \"nhk\", {} },\n        .{ \"nico\", {} },\n        .{ \"nike\", {} },\n        .{ \"nikon\", {} },\n        .{ \"ninja\", {} },\n        .{ \"nissan\", {} },\n        .{ \"nissay\", {} },\n        .{ \"nokia\", {} },\n        .{ \"norton\", {} },\n        .{ \"now\", {} },\n        .{ \"nowruz\", {} },\n        .{ \"nowtv\", {} },\n        .{ \"nra\", {} },\n        .{ \"nrw\", {} },\n        .{ \"ntt\", {} },\n        .{ \"nyc\", {} },\n        .{ \"obi\", {} },\n        .{ \"observer\", {} },\n        .{ \"office\", {} },\n        .{ \"okinawa\", {} },\n        .{ \"olayan\", {} },\n        .{ \"olayangroup\", {} },\n        .{ \"ollo\", {} },\n        .{ \"omega\", {} },\n        .{ \"one\", {} },\n        .{ \"ong\", {} },\n        .{ \"onl\", {} },\n        .{ \"online\", {} },\n        .{ \"ooo\", {} },\n        .{ \"open\", {} },\n        .{ \"oracle\", {} },\n        .{ \"orange\", {} },\n        .{ \"organic\", {} },\n        .{ \"origins\", {} },\n        .{ \"osaka\", {} },\n        .{ \"otsuka\", {} },\n        .{ \"ott\", {} },\n        .{ \"ovh\", {} },\n        .{ \"page\", {} },\n        .{ \"panasonic\", {} },\n        .{ \"paris\", {} },\n        .{ \"pars\", {} },\n        .{ \"partners\", {} },\n        .{ \"parts\", {} },\n        .{ \"party\", {} },\n        .{ \"pay\", {} },\n        .{ \"pccw\", {} },\n        .{ \"pet\", {} },\n        .{ \"pfizer\", {} },\n        .{ \"pharmacy\", {} },\n        .{ \"phd\", {} },\n        .{ \"philips\", {} },\n        .{ \"phone\", {} },\n        .{ \"photo\", {} },\n        .{ \"photography\", {} },\n        .{ \"photos\", {} },\n        .{ \"physio\", {} },\n        .{ \"pics\", {} },\n        .{ \"pictet\", {} },\n        .{ \"pictures\", {} },\n        .{ \"pid\", {} },\n        .{ \"pin\", {} },\n        .{ \"ping\", {} },\n        .{ \"pink\", {} },\n        .{ \"pioneer\", {} },\n        .{ \"pizza\", {} },\n        .{ \"place\", {} },\n        .{ \"play\", {} },\n        .{ \"playstation\", {} },\n        .{ \"plumbing\", {} },\n        .{ \"plus\", {} },\n        .{ \"pnc\", {} },\n        .{ \"pohl\", {} },\n        .{ \"poker\", {} },\n        .{ \"politie\", {} },\n        .{ \"porn\", {} },\n        .{ \"praxi\", {} },\n        .{ \"press\", {} },\n        .{ \"prime\", {} },\n        .{ \"prod\", {} },\n        .{ \"productions\", {} },\n        .{ \"prof\", {} },\n        .{ \"progressive\", {} },\n        .{ \"promo\", {} },\n        .{ \"properties\", {} },\n        .{ \"property\", {} },\n        .{ \"protection\", {} },\n        .{ \"pru\", {} },\n        .{ \"prudential\", {} },\n        .{ \"pub\", {} },\n        .{ \"pwc\", {} },\n        .{ \"qpon\", {} },\n        .{ \"quebec\", {} },\n        .{ \"quest\", {} },\n        .{ \"racing\", {} },\n        .{ \"radio\", {} },\n        .{ \"read\", {} },\n        .{ \"realestate\", {} },\n        .{ \"realtor\", {} },\n        .{ \"realty\", {} },\n        .{ \"recipes\", {} },\n        .{ \"red\", {} },\n        .{ \"redumbrella\", {} },\n        .{ \"rehab\", {} },\n        .{ \"reise\", {} },\n        .{ \"reisen\", {} },\n        .{ \"reit\", {} },\n        .{ \"reliance\", {} },\n        .{ \"ren\", {} },\n        .{ \"rent\", {} },\n        .{ \"rentals\", {} },\n        .{ \"repair\", {} },\n        .{ \"report\", {} },\n        .{ \"republican\", {} },\n        .{ \"rest\", {} },\n        .{ \"restaurant\", {} },\n        .{ \"review\", {} },\n        .{ \"reviews\", {} },\n        .{ \"rexroth\", {} },\n        .{ \"rich\", {} },\n        .{ \"richardli\", {} },\n        .{ \"ricoh\", {} },\n        .{ \"ril\", {} },\n        .{ \"rio\", {} },\n        .{ \"rip\", {} },\n        .{ \"rocks\", {} },\n        .{ \"rodeo\", {} },\n        .{ \"rogers\", {} },\n        .{ \"room\", {} },\n        .{ \"rsvp\", {} },\n        .{ \"rugby\", {} },\n        .{ \"ruhr\", {} },\n        .{ \"run\", {} },\n        .{ \"rwe\", {} },\n        .{ \"ryukyu\", {} },\n        .{ \"saarland\", {} },\n        .{ \"safe\", {} },\n        .{ \"safety\", {} },\n        .{ \"sakura\", {} },\n        .{ \"sale\", {} },\n        .{ \"salon\", {} },\n        .{ \"samsclub\", {} },\n        .{ \"samsung\", {} },\n        .{ \"sandvik\", {} },\n        .{ \"sandvikcoromant\", {} },\n        .{ \"sanofi\", {} },\n        .{ \"sap\", {} },\n        .{ \"sarl\", {} },\n        .{ \"sas\", {} },\n        .{ \"save\", {} },\n        .{ \"saxo\", {} },\n        .{ \"sbi\", {} },\n        .{ \"sbs\", {} },\n        .{ \"scb\", {} },\n        .{ \"schaeffler\", {} },\n        .{ \"schmidt\", {} },\n        .{ \"scholarships\", {} },\n        .{ \"school\", {} },\n        .{ \"schule\", {} },\n        .{ \"schwarz\", {} },\n        .{ \"science\", {} },\n        .{ \"scot\", {} },\n        .{ \"search\", {} },\n        .{ \"seat\", {} },\n        .{ \"secure\", {} },\n        .{ \"security\", {} },\n        .{ \"seek\", {} },\n        .{ \"select\", {} },\n        .{ \"sener\", {} },\n        .{ \"services\", {} },\n        .{ \"seven\", {} },\n        .{ \"sew\", {} },\n        .{ \"sex\", {} },\n        .{ \"sexy\", {} },\n        .{ \"sfr\", {} },\n        .{ \"shangrila\", {} },\n        .{ \"sharp\", {} },\n        .{ \"shell\", {} },\n        .{ \"shia\", {} },\n        .{ \"shiksha\", {} },\n        .{ \"shoes\", {} },\n        .{ \"shop\", {} },\n        .{ \"shopping\", {} },\n        .{ \"shouji\", {} },\n        .{ \"show\", {} },\n        .{ \"silk\", {} },\n        .{ \"sina\", {} },\n        .{ \"singles\", {} },\n        .{ \"site\", {} },\n        .{ \"ski\", {} },\n        .{ \"skin\", {} },\n        .{ \"sky\", {} },\n        .{ \"skype\", {} },\n        .{ \"sling\", {} },\n        .{ \"smart\", {} },\n        .{ \"smile\", {} },\n        .{ \"sncf\", {} },\n        .{ \"soccer\", {} },\n        .{ \"social\", {} },\n        .{ \"softbank\", {} },\n        .{ \"software\", {} },\n        .{ \"sohu\", {} },\n        .{ \"solar\", {} },\n        .{ \"solutions\", {} },\n        .{ \"song\", {} },\n        .{ \"sony\", {} },\n        .{ \"soy\", {} },\n        .{ \"spa\", {} },\n        .{ \"space\", {} },\n        .{ \"sport\", {} },\n        .{ \"spot\", {} },\n        .{ \"srl\", {} },\n        .{ \"stada\", {} },\n        .{ \"staples\", {} },\n        .{ \"star\", {} },\n        .{ \"statebank\", {} },\n        .{ \"statefarm\", {} },\n        .{ \"stc\", {} },\n        .{ \"stcgroup\", {} },\n        .{ \"stockholm\", {} },\n        .{ \"storage\", {} },\n        .{ \"store\", {} },\n        .{ \"stream\", {} },\n        .{ \"studio\", {} },\n        .{ \"study\", {} },\n        .{ \"style\", {} },\n        .{ \"sucks\", {} },\n        .{ \"supplies\", {} },\n        .{ \"supply\", {} },\n        .{ \"support\", {} },\n        .{ \"surf\", {} },\n        .{ \"surgery\", {} },\n        .{ \"suzuki\", {} },\n        .{ \"swatch\", {} },\n        .{ \"swiss\", {} },\n        .{ \"sydney\", {} },\n        .{ \"systems\", {} },\n        .{ \"tab\", {} },\n        .{ \"taipei\", {} },\n        .{ \"talk\", {} },\n        .{ \"taobao\", {} },\n        .{ \"target\", {} },\n        .{ \"tatamotors\", {} },\n        .{ \"tatar\", {} },\n        .{ \"tattoo\", {} },\n        .{ \"tax\", {} },\n        .{ \"taxi\", {} },\n        .{ \"tci\", {} },\n        .{ \"tdk\", {} },\n        .{ \"team\", {} },\n        .{ \"tech\", {} },\n        .{ \"technology\", {} },\n        .{ \"temasek\", {} },\n        .{ \"tennis\", {} },\n        .{ \"teva\", {} },\n        .{ \"thd\", {} },\n        .{ \"theater\", {} },\n        .{ \"theatre\", {} },\n        .{ \"tiaa\", {} },\n        .{ \"tickets\", {} },\n        .{ \"tienda\", {} },\n        .{ \"tips\", {} },\n        .{ \"tires\", {} },\n        .{ \"tirol\", {} },\n        .{ \"tjmaxx\", {} },\n        .{ \"tjx\", {} },\n        .{ \"tkmaxx\", {} },\n        .{ \"tmall\", {} },\n        .{ \"today\", {} },\n        .{ \"tokyo\", {} },\n        .{ \"tools\", {} },\n        .{ \"top\", {} },\n        .{ \"toray\", {} },\n        .{ \"toshiba\", {} },\n        .{ \"total\", {} },\n        .{ \"tours\", {} },\n        .{ \"town\", {} },\n        .{ \"toyota\", {} },\n        .{ \"toys\", {} },\n        .{ \"trade\", {} },\n        .{ \"trading\", {} },\n        .{ \"training\", {} },\n        .{ \"travel\", {} },\n        .{ \"travelers\", {} },\n        .{ \"travelersinsurance\", {} },\n        .{ \"trust\", {} },\n        .{ \"trv\", {} },\n        .{ \"tube\", {} },\n        .{ \"tui\", {} },\n        .{ \"tunes\", {} },\n        .{ \"tushu\", {} },\n        .{ \"tvs\", {} },\n        .{ \"ubank\", {} },\n        .{ \"ubs\", {} },\n        .{ \"unicom\", {} },\n        .{ \"university\", {} },\n        .{ \"uno\", {} },\n        .{ \"uol\", {} },\n        .{ \"ups\", {} },\n        .{ \"vacations\", {} },\n        .{ \"vana\", {} },\n        .{ \"vanguard\", {} },\n        .{ \"vegas\", {} },\n        .{ \"ventures\", {} },\n        .{ \"verisign\", {} },\n        .{ \"versicherung\", {} },\n        .{ \"vet\", {} },\n        .{ \"viajes\", {} },\n        .{ \"video\", {} },\n        .{ \"vig\", {} },\n        .{ \"viking\", {} },\n        .{ \"villas\", {} },\n        .{ \"vin\", {} },\n        .{ \"vip\", {} },\n        .{ \"virgin\", {} },\n        .{ \"visa\", {} },\n        .{ \"vision\", {} },\n        .{ \"viva\", {} },\n        .{ \"vivo\", {} },\n        .{ \"vlaanderen\", {} },\n        .{ \"vodka\", {} },\n        .{ \"volvo\", {} },\n        .{ \"vote\", {} },\n        .{ \"voting\", {} },\n        .{ \"voto\", {} },\n        .{ \"voyage\", {} },\n        .{ \"wales\", {} },\n        .{ \"walmart\", {} },\n        .{ \"walter\", {} },\n        .{ \"wang\", {} },\n        .{ \"wanggou\", {} },\n        .{ \"watch\", {} },\n        .{ \"watches\", {} },\n        .{ \"weather\", {} },\n        .{ \"weatherchannel\", {} },\n        .{ \"webcam\", {} },\n        .{ \"weber\", {} },\n        .{ \"website\", {} },\n        .{ \"wed\", {} },\n        .{ \"wedding\", {} },\n        .{ \"weibo\", {} },\n        .{ \"weir\", {} },\n        .{ \"whoswho\", {} },\n        .{ \"wien\", {} },\n        .{ \"wiki\", {} },\n        .{ \"williamhill\", {} },\n        .{ \"win\", {} },\n        .{ \"windows\", {} },\n        .{ \"wine\", {} },\n        .{ \"winners\", {} },\n        .{ \"wme\", {} },\n        .{ \"wolterskluwer\", {} },\n        .{ \"woodside\", {} },\n        .{ \"work\", {} },\n        .{ \"works\", {} },\n        .{ \"world\", {} },\n        .{ \"wow\", {} },\n        .{ \"wtc\", {} },\n        .{ \"wtf\", {} },\n        .{ \"xbox\", {} },\n        .{ \"xerox\", {} },\n        .{ \"xihuan\", {} },\n        .{ \"xin\", {} },\n        .{ \"कॉम\", {} },\n        .{ \"セール\", {} },\n        .{ \"佛山\", {} },\n        .{ \"慈善\", {} },\n        .{ \"集团\", {} },\n        .{ \"在线\", {} },\n        .{ \"点看\", {} },\n        .{ \"คอม\", {} },\n        .{ \"八卦\", {} },\n        .{ \"موقع\", {} },\n        .{ \"公益\", {} },\n        .{ \"公司\", {} },\n        .{ \"香格里拉\", {} },\n        .{ \"网站\", {} },\n        .{ \"移动\", {} },\n        .{ \"我爱你\", {} },\n        .{ \"москва\", {} },\n        .{ \"католик\", {} },\n        .{ \"онлайн\", {} },\n        .{ \"сайт\", {} },\n        .{ \"联通\", {} },\n        .{ \"קום\", {} },\n        .{ \"时尚\", {} },\n        .{ \"微博\", {} },\n        .{ \"淡马锡\", {} },\n        .{ \"ファッション\", {} },\n        .{ \"орг\", {} },\n        .{ \"नेट\", {} },\n        .{ \"ストア\", {} },\n        .{ \"アマゾン\", {} },\n        .{ \"삼성\", {} },\n        .{ \"商标\", {} },\n        .{ \"商店\", {} },\n        .{ \"商城\", {} },\n        .{ \"дети\", {} },\n        .{ \"ポイント\", {} },\n        .{ \"新闻\", {} },\n        .{ \"家電\", {} },\n        .{ \"كوم\", {} },\n        .{ \"中文网\", {} },\n        .{ \"中信\", {} },\n        .{ \"娱乐\", {} },\n        .{ \"谷歌\", {} },\n        .{ \"電訊盈科\", {} },\n        .{ \"购物\", {} },\n        .{ \"クラウド\", {} },\n        .{ \"通販\", {} },\n        .{ \"网店\", {} },\n        .{ \"संगठन\", {} },\n        .{ \"餐厅\", {} },\n        .{ \"网络\", {} },\n        .{ \"ком\", {} },\n        .{ \"亚马逊\", {} },\n        .{ \"食品\", {} },\n        .{ \"飞利浦\", {} },\n        .{ \"手机\", {} },\n        .{ \"ارامكو\", {} },\n        .{ \"العليان\", {} },\n        .{ \"بازار\", {} },\n        .{ \"ابوظبي\", {} },\n        .{ \"كاثوليك\", {} },\n        .{ \"همراه\", {} },\n        .{ \"닷컴\", {} },\n        .{ \"政府\", {} },\n        .{ \"شبكة\", {} },\n        .{ \"بيتك\", {} },\n        .{ \"عرب\", {} },\n        .{ \"机构\", {} },\n        .{ \"组织机构\", {} },\n        .{ \"健康\", {} },\n        .{ \"招聘\", {} },\n        .{ \"рус\", {} },\n        .{ \"大拿\", {} },\n        .{ \"みんな\", {} },\n        .{ \"グーグル\", {} },\n        .{ \"世界\", {} },\n        .{ \"書籍\", {} },\n        .{ \"网址\", {} },\n        .{ \"닷넷\", {} },\n        .{ \"コム\", {} },\n        .{ \"天主教\", {} },\n        .{ \"游戏\", {} },\n        .{ \"vermögensberater\", {} },\n        .{ \"vermögensberatung\", {} },\n        .{ \"企业\", {} },\n        .{ \"信息\", {} },\n        .{ \"嘉里大酒店\", {} },\n        .{ \"嘉里\", {} },\n        .{ \"广东\", {} },\n        .{ \"政务\", {} },\n        .{ \"xyz\", {} },\n        .{ \"yachts\", {} },\n        .{ \"yahoo\", {} },\n        .{ \"yamaxun\", {} },\n        .{ \"yandex\", {} },\n        .{ \"yodobashi\", {} },\n        .{ \"yoga\", {} },\n        .{ \"yokohama\", {} },\n        .{ \"you\", {} },\n        .{ \"youtube\", {} },\n        .{ \"yun\", {} },\n        .{ \"zappos\", {} },\n        .{ \"zara\", {} },\n        .{ \"zero\", {} },\n        .{ \"zip\", {} },\n        .{ \"zone\", {} },\n        .{ \"zuerich\", {} },\n        .{ \"co.krd\", {} },\n        .{ \"edu.krd\", {} },\n        .{ \"art.pl\", {} },\n        .{ \"gliwice.pl\", {} },\n        .{ \"krakow.pl\", {} },\n        .{ \"poznan.pl\", {} },\n        .{ \"wroc.pl\", {} },\n        .{ \"zakopane.pl\", {} },\n        .{ \"12chars.dev\", {} },\n        .{ \"12chars.it\", {} },\n        .{ \"12chars.pro\", {} },\n        .{ \"cc.ua\", {} },\n        .{ \"inf.ua\", {} },\n        .{ \"ltd.ua\", {} },\n        .{ \"611.to\", {} },\n        .{ \"a2hosted.com\", {} },\n        .{ \"cpserver.com\", {} },\n        .{ \"*.on-acorn.io\", {} },\n        .{ \"activetrail.biz\", {} },\n        .{ \"adaptable.app\", {} },\n        .{ \"myaddr.dev\", {} },\n        .{ \"myaddr.io\", {} },\n        .{ \"dyn.addr.tools\", {} },\n        .{ \"myaddr.tools\", {} },\n        .{ \"adobeaemcloud.com\", {} },\n        .{ \"*.dev.adobeaemcloud.com\", {} },\n        .{ \"aem.live\", {} },\n        .{ \"hlx.live\", {} },\n        .{ \"adobeaemcloud.net\", {} },\n        .{ \"aem.network\", {} },\n        .{ \"aem.page\", {} },\n        .{ \"hlx.page\", {} },\n        .{ \"aem.reviews\", {} },\n        .{ \"adobeio-static.net\", {} },\n        .{ \"adobeioruntime.net\", {} },\n        .{ \"africa.com\", {} },\n        .{ \"*.auiusercontent.com\", {} },\n        .{ \"beep.pl\", {} },\n        .{ \"aiven.app\", {} },\n        .{ \"aivencloud.com\", {} },\n        .{ \"akadns.net\", {} },\n        .{ \"akamai.net\", {} },\n        .{ \"akamai-staging.net\", {} },\n        .{ \"akamaiedge.net\", {} },\n        .{ \"akamaiedge-staging.net\", {} },\n        .{ \"akamaihd.net\", {} },\n        .{ \"akamaihd-staging.net\", {} },\n        .{ \"akamaiorigin.net\", {} },\n        .{ \"akamaiorigin-staging.net\", {} },\n        .{ \"akamaized.net\", {} },\n        .{ \"akamaized-staging.net\", {} },\n        .{ \"edgekey.net\", {} },\n        .{ \"edgekey-staging.net\", {} },\n        .{ \"edgesuite.net\", {} },\n        .{ \"edgesuite-staging.net\", {} },\n        .{ \"barsy.ca\", {} },\n        .{ \"*.compute.estate\", {} },\n        .{ \"*.alces.network\", {} },\n        .{ \"alibabacloudcs.com\", {} },\n        .{ \"kasserver.com\", {} },\n        .{ \"altervista.org\", {} },\n        .{ \"alwaysdata.net\", {} },\n        .{ \"myamaze.net\", {} },\n        .{ \"execute-api.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"execute-api.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"execute-api.af-south-1.amazonaws.com\", {} },\n        .{ \"execute-api.ap-east-1.amazonaws.com\", {} },\n        .{ \"execute-api.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"execute-api.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"execute-api.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"execute-api.ap-south-1.amazonaws.com\", {} },\n        .{ \"execute-api.ap-south-2.amazonaws.com\", {} },\n        .{ \"execute-api.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"execute-api.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"execute-api.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"execute-api.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"execute-api.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"execute-api.ca-central-1.amazonaws.com\", {} },\n        .{ \"execute-api.ca-west-1.amazonaws.com\", {} },\n        .{ \"execute-api.eu-central-1.amazonaws.com\", {} },\n        .{ \"execute-api.eu-central-2.amazonaws.com\", {} },\n        .{ \"execute-api.eu-north-1.amazonaws.com\", {} },\n        .{ \"execute-api.eu-south-1.amazonaws.com\", {} },\n        .{ \"execute-api.eu-south-2.amazonaws.com\", {} },\n        .{ \"execute-api.eu-west-1.amazonaws.com\", {} },\n        .{ \"execute-api.eu-west-2.amazonaws.com\", {} },\n        .{ \"execute-api.eu-west-3.amazonaws.com\", {} },\n        .{ \"execute-api.il-central-1.amazonaws.com\", {} },\n        .{ \"execute-api.me-central-1.amazonaws.com\", {} },\n        .{ \"execute-api.me-south-1.amazonaws.com\", {} },\n        .{ \"execute-api.sa-east-1.amazonaws.com\", {} },\n        .{ \"execute-api.us-east-1.amazonaws.com\", {} },\n        .{ \"execute-api.us-east-2.amazonaws.com\", {} },\n        .{ \"execute-api.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"execute-api.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"execute-api.us-west-1.amazonaws.com\", {} },\n        .{ \"execute-api.us-west-2.amazonaws.com\", {} },\n        .{ \"cloudfront.net\", {} },\n        .{ \"auth.af-south-1.amazoncognito.com\", {} },\n        .{ \"auth.ap-east-1.amazoncognito.com\", {} },\n        .{ \"auth.ap-northeast-1.amazoncognito.com\", {} },\n        .{ \"auth.ap-northeast-2.amazoncognito.com\", {} },\n        .{ \"auth.ap-northeast-3.amazoncognito.com\", {} },\n        .{ \"auth.ap-south-1.amazoncognito.com\", {} },\n        .{ \"auth.ap-south-2.amazoncognito.com\", {} },\n        .{ \"auth.ap-southeast-1.amazoncognito.com\", {} },\n        .{ \"auth.ap-southeast-2.amazoncognito.com\", {} },\n        .{ \"auth.ap-southeast-3.amazoncognito.com\", {} },\n        .{ \"auth.ap-southeast-4.amazoncognito.com\", {} },\n        .{ \"auth.ap-southeast-5.amazoncognito.com\", {} },\n        .{ \"auth.ap-southeast-7.amazoncognito.com\", {} },\n        .{ \"auth.ca-central-1.amazoncognito.com\", {} },\n        .{ \"auth.ca-west-1.amazoncognito.com\", {} },\n        .{ \"auth.eu-central-1.amazoncognito.com\", {} },\n        .{ \"auth.eu-central-2.amazoncognito.com\", {} },\n        .{ \"auth.eu-north-1.amazoncognito.com\", {} },\n        .{ \"auth.eu-south-1.amazoncognito.com\", {} },\n        .{ \"auth.eu-south-2.amazoncognito.com\", {} },\n        .{ \"auth.eu-west-1.amazoncognito.com\", {} },\n        .{ \"auth.eu-west-2.amazoncognito.com\", {} },\n        .{ \"auth.eu-west-3.amazoncognito.com\", {} },\n        .{ \"auth.il-central-1.amazoncognito.com\", {} },\n        .{ \"auth.me-central-1.amazoncognito.com\", {} },\n        .{ \"auth.me-south-1.amazoncognito.com\", {} },\n        .{ \"auth.mx-central-1.amazoncognito.com\", {} },\n        .{ \"auth.sa-east-1.amazoncognito.com\", {} },\n        .{ \"auth.us-east-1.amazoncognito.com\", {} },\n        .{ \"auth-fips.us-east-1.amazoncognito.com\", {} },\n        .{ \"auth.us-east-2.amazoncognito.com\", {} },\n        .{ \"auth-fips.us-east-2.amazoncognito.com\", {} },\n        .{ \"auth-fips.us-gov-east-1.amazoncognito.com\", {} },\n        .{ \"auth-fips.us-gov-west-1.amazoncognito.com\", {} },\n        .{ \"auth.us-west-1.amazoncognito.com\", {} },\n        .{ \"auth-fips.us-west-1.amazoncognito.com\", {} },\n        .{ \"auth.us-west-2.amazoncognito.com\", {} },\n        .{ \"auth-fips.us-west-2.amazoncognito.com\", {} },\n        .{ \"auth.cognito-idp.eusc-de-east-1.on.amazonwebservices.eu\", {} },\n        .{ \"*.compute.amazonaws.com.cn\", {} },\n        .{ \"*.compute.amazonaws.com\", {} },\n        .{ \"*.compute-1.amazonaws.com\", {} },\n        .{ \"us-east-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"emrnotebooks-prod.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"emrstudio-prod.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"emrappui-prod.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"emrnotebooks-prod.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"emrstudio-prod.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"emrappui-prod.af-south-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.af-south-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.af-south-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-east-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-east-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-east-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-south-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-south-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-south-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-south-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-south-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-south-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ca-central-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ca-central-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ca-central-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.ca-west-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.ca-west-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.ca-west-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-central-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-central-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-central-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-central-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-central-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-central-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-north-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-north-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-north-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-south-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-south-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-south-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-south-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-south-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-south-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-west-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-west-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-west-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-west-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-west-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-west-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.eu-west-3.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.eu-west-3.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.eu-west-3.amazonaws.com\", {} },\n        .{ \"emrappui-prod.il-central-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.il-central-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.il-central-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.me-central-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.me-central-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.me-central-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.me-south-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.me-south-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.me-south-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.sa-east-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.sa-east-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.sa-east-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.us-east-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.us-east-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.us-east-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.us-east-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.us-east-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.us-east-2.amazonaws.com\", {} },\n        .{ \"emrappui-prod.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.us-west-1.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.us-west-1.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.us-west-1.amazonaws.com\", {} },\n        .{ \"emrappui-prod.us-west-2.amazonaws.com\", {} },\n        .{ \"emrnotebooks-prod.us-west-2.amazonaws.com\", {} },\n        .{ \"emrstudio-prod.us-west-2.amazonaws.com\", {} },\n        .{ \"*.airflow.af-south-1.on.aws\", {} },\n        .{ \"*.airflow.ap-east-1.on.aws\", {} },\n        .{ \"*.airflow.ap-northeast-1.on.aws\", {} },\n        .{ \"*.airflow.ap-northeast-2.on.aws\", {} },\n        .{ \"*.airflow.ap-northeast-3.on.aws\", {} },\n        .{ \"*.airflow.ap-south-1.on.aws\", {} },\n        .{ \"*.airflow.ap-south-2.on.aws\", {} },\n        .{ \"*.airflow.ap-southeast-1.on.aws\", {} },\n        .{ \"*.airflow.ap-southeast-2.on.aws\", {} },\n        .{ \"*.airflow.ap-southeast-3.on.aws\", {} },\n        .{ \"*.airflow.ap-southeast-4.on.aws\", {} },\n        .{ \"*.airflow.ap-southeast-5.on.aws\", {} },\n        .{ \"*.airflow.ca-central-1.on.aws\", {} },\n        .{ \"*.airflow.ca-west-1.on.aws\", {} },\n        .{ \"*.airflow.eu-central-1.on.aws\", {} },\n        .{ \"*.airflow.eu-central-2.on.aws\", {} },\n        .{ \"*.airflow.eu-north-1.on.aws\", {} },\n        .{ \"*.airflow.eu-south-1.on.aws\", {} },\n        .{ \"*.airflow.eu-south-2.on.aws\", {} },\n        .{ \"*.airflow.eu-west-1.on.aws\", {} },\n        .{ \"*.airflow.eu-west-2.on.aws\", {} },\n        .{ \"*.airflow.eu-west-3.on.aws\", {} },\n        .{ \"*.airflow.il-central-1.on.aws\", {} },\n        .{ \"*.airflow.me-central-1.on.aws\", {} },\n        .{ \"*.airflow.me-south-1.on.aws\", {} },\n        .{ \"*.airflow.sa-east-1.on.aws\", {} },\n        .{ \"*.airflow.us-east-1.on.aws\", {} },\n        .{ \"*.airflow.us-east-2.on.aws\", {} },\n        .{ \"*.airflow.us-west-1.on.aws\", {} },\n        .{ \"*.airflow.us-west-2.on.aws\", {} },\n        .{ \"*.cn-north-1.airflow.amazonaws.com.cn\", {} },\n        .{ \"*.cn-northwest-1.airflow.amazonaws.com.cn\", {} },\n        .{ \"*.airflow.cn-north-1.on.amazonwebservices.com.cn\", {} },\n        .{ \"*.airflow.cn-northwest-1.on.amazonwebservices.com.cn\", {} },\n        .{ \"*.af-south-1.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-east-1.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-northeast-1.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-northeast-2.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-northeast-3.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-south-1.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-south-2.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-1.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-2.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-3.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-4.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-5.airflow.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-7.airflow.amazonaws.com\", {} },\n        .{ \"*.ca-central-1.airflow.amazonaws.com\", {} },\n        .{ \"*.ca-west-1.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-central-1.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-central-2.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-north-1.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-south-1.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-south-2.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-west-1.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-west-2.airflow.amazonaws.com\", {} },\n        .{ \"*.eu-west-3.airflow.amazonaws.com\", {} },\n        .{ \"*.il-central-1.airflow.amazonaws.com\", {} },\n        .{ \"*.me-central-1.airflow.amazonaws.com\", {} },\n        .{ \"*.me-south-1.airflow.amazonaws.com\", {} },\n        .{ \"*.sa-east-1.airflow.amazonaws.com\", {} },\n        .{ \"*.us-east-1.airflow.amazonaws.com\", {} },\n        .{ \"*.us-east-2.airflow.amazonaws.com\", {} },\n        .{ \"*.us-west-1.airflow.amazonaws.com\", {} },\n        .{ \"*.us-west-2.airflow.amazonaws.com\", {} },\n        .{ \"*.rds.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"*.rds.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"*.af-south-1.rds.amazonaws.com\", {} },\n        .{ \"*.ap-east-1.rds.amazonaws.com\", {} },\n        .{ \"*.ap-east-2.rds.amazonaws.com\", {} },\n        .{ \"*.ap-northeast-1.rds.amazonaws.com\", {} },\n        .{ \"*.ap-northeast-2.rds.amazonaws.com\", {} },\n        .{ \"*.ap-northeast-3.rds.amazonaws.com\", {} },\n        .{ \"*.ap-south-1.rds.amazonaws.com\", {} },\n        .{ \"*.ap-south-2.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-1.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-2.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-3.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-4.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-5.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-6.rds.amazonaws.com\", {} },\n        .{ \"*.ap-southeast-7.rds.amazonaws.com\", {} },\n        .{ \"*.ca-central-1.rds.amazonaws.com\", {} },\n        .{ \"*.ca-west-1.rds.amazonaws.com\", {} },\n        .{ \"*.eu-central-1.rds.amazonaws.com\", {} },\n        .{ \"*.eu-central-2.rds.amazonaws.com\", {} },\n        .{ \"*.eu-west-1.rds.amazonaws.com\", {} },\n        .{ \"*.eu-west-2.rds.amazonaws.com\", {} },\n        .{ \"*.eu-west-3.rds.amazonaws.com\", {} },\n        .{ \"*.il-central-1.rds.amazonaws.com\", {} },\n        .{ \"*.me-central-1.rds.amazonaws.com\", {} },\n        .{ \"*.me-south-1.rds.amazonaws.com\", {} },\n        .{ \"*.mx-central-1.rds.amazonaws.com\", {} },\n        .{ \"*.sa-east-1.rds.amazonaws.com\", {} },\n        .{ \"*.us-east-1.rds.amazonaws.com\", {} },\n        .{ \"*.us-east-2.rds.amazonaws.com\", {} },\n        .{ \"*.us-gov-east-1.rds.amazonaws.com\", {} },\n        .{ \"*.us-gov-west-1.rds.amazonaws.com\", {} },\n        .{ \"*.us-northeast-1.rds.amazonaws.com\", {} },\n        .{ \"*.us-west-1.rds.amazonaws.com\", {} },\n        .{ \"*.us-west-2.rds.amazonaws.com\", {} },\n        .{ \"s3.dualstack.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3-accesspoint.dualstack.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3-website.dualstack.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3-accesspoint.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3-deprecated.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3-object-lambda.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3-website.cn-north-1.amazonaws.com.cn\", {} },\n        .{ \"s3.dualstack.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"s3-accesspoint.dualstack.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"s3.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"s3-accesspoint.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"s3-object-lambda.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"s3-website.cn-northwest-1.amazonaws.com.cn\", {} },\n        .{ \"s3.dualstack.af-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.af-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.af-south-1.amazonaws.com\", {} },\n        .{ \"s3.af-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.af-south-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.af-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.af-south-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-east-1.amazonaws.com\", {} },\n        .{ \"s3.ap-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-east-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.ap-east-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-website.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3-website.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3-website.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.ap-south-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3-website.ap-south-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-website.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-website.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3-website.ap-southeast-3.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3-website.ap-southeast-4.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3-deprecated.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3-website.ap-southeast-5.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-fips.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.ca-central-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-fips.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.ca-west-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.eu-central-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3-website.eu-central-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-north-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-north-1.amazonaws.com\", {} },\n        .{ \"s3.eu-north-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-north-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-north-1.amazonaws.com\", {} },\n        .{ \"s3-website.eu-north-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.eu-south-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3-website.eu-south-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-deprecated.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.eu-west-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-west-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-west-2.amazonaws.com\", {} },\n        .{ \"s3.eu-west-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-west-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-west-2.amazonaws.com\", {} },\n        .{ \"s3-website.eu-west-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3-website.eu-west-3.amazonaws.com\", {} },\n        .{ \"s3.dualstack.il-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.il-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.il-central-1.amazonaws.com\", {} },\n        .{ \"s3.il-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.il-central-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.il-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.il-central-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.me-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.me-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.me-central-1.amazonaws.com\", {} },\n        .{ \"s3.me-central-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.me-central-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.me-central-1.amazonaws.com\", {} },\n        .{ \"s3-website.me-central-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.me-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.me-south-1.amazonaws.com\", {} },\n        .{ \"s3.me-south-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.me-south-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.me-south-1.amazonaws.com\", {} },\n        .{ \"s3-website.me-south-1.amazonaws.com\", {} },\n        .{ \"s3.amazonaws.com\", {} },\n        .{ \"s3-1.amazonaws.com\", {} },\n        .{ \"s3-ap-east-1.amazonaws.com\", {} },\n        .{ \"s3-ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-ap-northeast-2.amazonaws.com\", {} },\n        .{ \"s3-ap-northeast-3.amazonaws.com\", {} },\n        .{ \"s3-ap-south-1.amazonaws.com\", {} },\n        .{ \"s3-ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-ca-central-1.amazonaws.com\", {} },\n        .{ \"s3-eu-central-1.amazonaws.com\", {} },\n        .{ \"s3-eu-north-1.amazonaws.com\", {} },\n        .{ \"s3-eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-eu-west-2.amazonaws.com\", {} },\n        .{ \"s3-eu-west-3.amazonaws.com\", {} },\n        .{ \"s3-external-1.amazonaws.com\", {} },\n        .{ \"s3-fips-us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-fips-us-gov-west-1.amazonaws.com\", {} },\n        .{ \"mrap.accesspoint.s3-global.amazonaws.com\", {} },\n        .{ \"s3-me-south-1.amazonaws.com\", {} },\n        .{ \"s3-sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-us-east-2.amazonaws.com\", {} },\n        .{ \"s3-us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-us-west-1.amazonaws.com\", {} },\n        .{ \"s3-us-west-2.amazonaws.com\", {} },\n        .{ \"s3-website-ap-northeast-1.amazonaws.com\", {} },\n        .{ \"s3-website-ap-southeast-1.amazonaws.com\", {} },\n        .{ \"s3-website-ap-southeast-2.amazonaws.com\", {} },\n        .{ \"s3-website-eu-west-1.amazonaws.com\", {} },\n        .{ \"s3-website-sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-website-us-east-1.amazonaws.com\", {} },\n        .{ \"s3-website-us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-website-us-west-1.amazonaws.com\", {} },\n        .{ \"s3-website-us-west-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.sa-east-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.us-east-1.amazonaws.com\", {} },\n        .{ \"s3.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-deprecated.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-fips.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.us-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.us-east-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.us-east-2.amazonaws.com\", {} },\n        .{ \"s3.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-deprecated.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-fips.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.us-east-2.amazonaws.com\", {} },\n        .{ \"s3-website.us-east-2.amazonaws.com\", {} },\n        .{ \"s3.dualstack.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-fips.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3-website.us-gov-east-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-fips.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.us-gov-west-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.us-west-1.amazonaws.com\", {} },\n        .{ \"s3.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-fips.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.us-west-1.amazonaws.com\", {} },\n        .{ \"s3-website.us-west-1.amazonaws.com\", {} },\n        .{ \"s3.dualstack.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.dualstack.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.dualstack.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-fips.dualstack.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-website.dualstack.us-west-2.amazonaws.com\", {} },\n        .{ \"s3.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-accesspoint-fips.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-deprecated.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-fips.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-object-lambda.us-west-2.amazonaws.com\", {} },\n        .{ \"s3-website.us-west-2.amazonaws.com\", {} },\n        .{ \"labeling.ap-northeast-1.sagemaker.aws\", {} },\n        .{ \"labeling.ap-northeast-2.sagemaker.aws\", {} },\n        .{ \"labeling.ap-south-1.sagemaker.aws\", {} },\n        .{ \"labeling.ap-southeast-1.sagemaker.aws\", {} },\n        .{ \"labeling.ap-southeast-2.sagemaker.aws\", {} },\n        .{ \"labeling.ca-central-1.sagemaker.aws\", {} },\n        .{ \"labeling.eu-central-1.sagemaker.aws\", {} },\n        .{ \"labeling.eu-west-1.sagemaker.aws\", {} },\n        .{ \"labeling.eu-west-2.sagemaker.aws\", {} },\n        .{ \"labeling.us-east-1.sagemaker.aws\", {} },\n        .{ \"labeling.us-east-2.sagemaker.aws\", {} },\n        .{ \"labeling.us-west-2.sagemaker.aws\", {} },\n        .{ \"notebook.af-south-1.sagemaker.aws\", {} },\n        .{ \"notebook.ap-east-1.sagemaker.aws\", {} },\n        .{ \"notebook.ap-northeast-1.sagemaker.aws\", {} },\n        .{ \"notebook.ap-northeast-2.sagemaker.aws\", {} },\n        .{ \"notebook.ap-northeast-3.sagemaker.aws\", {} },\n        .{ \"notebook.ap-south-1.sagemaker.aws\", {} },\n        .{ \"notebook.ap-south-2.sagemaker.aws\", {} },\n        .{ \"notebook.ap-southeast-1.sagemaker.aws\", {} },\n        .{ \"notebook.ap-southeast-2.sagemaker.aws\", {} },\n        .{ \"notebook.ap-southeast-3.sagemaker.aws\", {} },\n        .{ \"notebook.ap-southeast-4.sagemaker.aws\", {} },\n        .{ \"notebook.ca-central-1.sagemaker.aws\", {} },\n        .{ \"notebook-fips.ca-central-1.sagemaker.aws\", {} },\n        .{ \"notebook.ca-west-1.sagemaker.aws\", {} },\n        .{ \"notebook-fips.ca-west-1.sagemaker.aws\", {} },\n        .{ \"notebook.eu-central-1.sagemaker.aws\", {} },\n        .{ \"notebook.eu-central-2.sagemaker.aws\", {} },\n        .{ \"notebook.eu-north-1.sagemaker.aws\", {} },\n        .{ \"notebook.eu-south-1.sagemaker.aws\", {} },\n        .{ \"notebook.eu-south-2.sagemaker.aws\", {} },\n        .{ \"notebook.eu-west-1.sagemaker.aws\", {} },\n        .{ \"notebook.eu-west-2.sagemaker.aws\", {} },\n        .{ \"notebook.eu-west-3.sagemaker.aws\", {} },\n        .{ \"notebook.il-central-1.sagemaker.aws\", {} },\n        .{ \"notebook.me-central-1.sagemaker.aws\", {} },\n        .{ \"notebook.me-south-1.sagemaker.aws\", {} },\n        .{ \"notebook.sa-east-1.sagemaker.aws\", {} },\n        .{ \"notebook.us-east-1.sagemaker.aws\", {} },\n        .{ \"notebook-fips.us-east-1.sagemaker.aws\", {} },\n        .{ \"notebook.us-east-2.sagemaker.aws\", {} },\n        .{ \"notebook-fips.us-east-2.sagemaker.aws\", {} },\n        .{ \"notebook.us-gov-east-1.sagemaker.aws\", {} },\n        .{ \"notebook-fips.us-gov-east-1.sagemaker.aws\", {} },\n        .{ \"notebook.us-gov-west-1.sagemaker.aws\", {} },\n        .{ \"notebook-fips.us-gov-west-1.sagemaker.aws\", {} },\n        .{ \"notebook.us-west-1.sagemaker.aws\", {} },\n        .{ \"notebook-fips.us-west-1.sagemaker.aws\", {} },\n        .{ \"notebook.us-west-2.sagemaker.aws\", {} },\n        .{ \"notebook-fips.us-west-2.sagemaker.aws\", {} },\n        .{ \"notebook.cn-north-1.sagemaker.com.cn\", {} },\n        .{ \"notebook.cn-northwest-1.sagemaker.com.cn\", {} },\n        .{ \"studio.af-south-1.sagemaker.aws\", {} },\n        .{ \"studio.ap-east-1.sagemaker.aws\", {} },\n        .{ \"studio.ap-northeast-1.sagemaker.aws\", {} },\n        .{ \"studio.ap-northeast-2.sagemaker.aws\", {} },\n        .{ \"studio.ap-northeast-3.sagemaker.aws\", {} },\n        .{ \"studio.ap-south-1.sagemaker.aws\", {} },\n        .{ \"studio.ap-southeast-1.sagemaker.aws\", {} },\n        .{ \"studio.ap-southeast-2.sagemaker.aws\", {} },\n        .{ \"studio.ap-southeast-3.sagemaker.aws\", {} },\n        .{ \"studio.ca-central-1.sagemaker.aws\", {} },\n        .{ \"studio.eu-central-1.sagemaker.aws\", {} },\n        .{ \"studio.eu-central-2.sagemaker.aws\", {} },\n        .{ \"studio.eu-north-1.sagemaker.aws\", {} },\n        .{ \"studio.eu-south-1.sagemaker.aws\", {} },\n        .{ \"studio.eu-south-2.sagemaker.aws\", {} },\n        .{ \"studio.eu-west-1.sagemaker.aws\", {} },\n        .{ \"studio.eu-west-2.sagemaker.aws\", {} },\n        .{ \"studio.eu-west-3.sagemaker.aws\", {} },\n        .{ \"studio.il-central-1.sagemaker.aws\", {} },\n        .{ \"studio.me-central-1.sagemaker.aws\", {} },\n        .{ \"studio.me-south-1.sagemaker.aws\", {} },\n        .{ \"studio.sa-east-1.sagemaker.aws\", {} },\n        .{ \"studio.us-east-1.sagemaker.aws\", {} },\n        .{ \"studio.us-east-2.sagemaker.aws\", {} },\n        .{ \"studio.us-gov-east-1.sagemaker.aws\", {} },\n        .{ \"studio-fips.us-gov-east-1.sagemaker.aws\", {} },\n        .{ \"studio.us-gov-west-1.sagemaker.aws\", {} },\n        .{ \"studio-fips.us-gov-west-1.sagemaker.aws\", {} },\n        .{ \"studio.us-west-1.sagemaker.aws\", {} },\n        .{ \"studio.us-west-2.sagemaker.aws\", {} },\n        .{ \"studio.cn-north-1.sagemaker.com.cn\", {} },\n        .{ \"studio.cn-northwest-1.sagemaker.com.cn\", {} },\n        .{ \"*.experiments.sagemaker.aws\", {} },\n        .{ \"analytics-gateway.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"analytics-gateway.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"analytics-gateway.ap-south-1.amazonaws.com\", {} },\n        .{ \"analytics-gateway.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"analytics-gateway.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"analytics-gateway.eu-central-1.amazonaws.com\", {} },\n        .{ \"analytics-gateway.eu-west-1.amazonaws.com\", {} },\n        .{ \"analytics-gateway.us-east-1.amazonaws.com\", {} },\n        .{ \"analytics-gateway.us-east-2.amazonaws.com\", {} },\n        .{ \"analytics-gateway.us-west-2.amazonaws.com\", {} },\n        .{ \"amplifyapp.com\", {} },\n        .{ \"*.awsapprunner.com\", {} },\n        .{ \"webview-assets.aws-cloud9.af-south-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.af-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.af-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-east-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-east-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-east-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-northeast-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-northeast-2.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-northeast-3.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-south-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-southeast-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ap-southeast-2.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.ca-central-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.ca-central-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.ca-central-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.eu-central-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.eu-central-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.eu-central-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.eu-north-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.eu-north-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.eu-north-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.eu-south-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.eu-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.eu-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.eu-west-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.eu-west-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.eu-west-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.eu-west-2.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.eu-west-2.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.eu-west-2.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.eu-west-3.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.eu-west-3.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.eu-west-3.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.il-central-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.il-central-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.me-south-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.me-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.me-south-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.sa-east-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.sa-east-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.sa-east-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.us-east-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.us-east-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.us-east-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.us-east-2.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.us-east-2.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.us-east-2.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.us-west-1.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.us-west-1.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.us-west-1.amazonaws.com\", {} },\n        .{ \"webview-assets.aws-cloud9.us-west-2.amazonaws.com\", {} },\n        .{ \"vfs.cloud9.us-west-2.amazonaws.com\", {} },\n        .{ \"webview-assets.cloud9.us-west-2.amazonaws.com\", {} },\n        .{ \"awsapps.com\", {} },\n        .{ \"cn-north-1.eb.amazonaws.com.cn\", {} },\n        .{ \"cn-northwest-1.eb.amazonaws.com.cn\", {} },\n        .{ \"elasticbeanstalk.com\", {} },\n        .{ \"af-south-1.elasticbeanstalk.com\", {} },\n        .{ \"ap-east-1.elasticbeanstalk.com\", {} },\n        .{ \"ap-northeast-1.elasticbeanstalk.com\", {} },\n        .{ \"ap-northeast-2.elasticbeanstalk.com\", {} },\n        .{ \"ap-northeast-3.elasticbeanstalk.com\", {} },\n        .{ \"ap-south-1.elasticbeanstalk.com\", {} },\n        .{ \"ap-southeast-1.elasticbeanstalk.com\", {} },\n        .{ \"ap-southeast-2.elasticbeanstalk.com\", {} },\n        .{ \"ap-southeast-3.elasticbeanstalk.com\", {} },\n        .{ \"ap-southeast-5.elasticbeanstalk.com\", {} },\n        .{ \"ap-southeast-7.elasticbeanstalk.com\", {} },\n        .{ \"ca-central-1.elasticbeanstalk.com\", {} },\n        .{ \"eu-central-1.elasticbeanstalk.com\", {} },\n        .{ \"eu-north-1.elasticbeanstalk.com\", {} },\n        .{ \"eu-south-1.elasticbeanstalk.com\", {} },\n        .{ \"eu-south-2.elasticbeanstalk.com\", {} },\n        .{ \"eu-west-1.elasticbeanstalk.com\", {} },\n        .{ \"eu-west-2.elasticbeanstalk.com\", {} },\n        .{ \"eu-west-3.elasticbeanstalk.com\", {} },\n        .{ \"il-central-1.elasticbeanstalk.com\", {} },\n        .{ \"me-central-1.elasticbeanstalk.com\", {} },\n        .{ \"me-south-1.elasticbeanstalk.com\", {} },\n        .{ \"sa-east-1.elasticbeanstalk.com\", {} },\n        .{ \"us-east-1.elasticbeanstalk.com\", {} },\n        .{ \"us-east-2.elasticbeanstalk.com\", {} },\n        .{ \"us-gov-east-1.elasticbeanstalk.com\", {} },\n        .{ \"us-gov-west-1.elasticbeanstalk.com\", {} },\n        .{ \"us-west-1.elasticbeanstalk.com\", {} },\n        .{ \"us-west-2.elasticbeanstalk.com\", {} },\n        .{ \"*.elb.amazonaws.com.cn\", {} },\n        .{ \"*.elb.amazonaws.com\", {} },\n        .{ \"awsglobalaccelerator.com\", {} },\n        .{ \"lambda-url.af-south-1.on.aws\", {} },\n        .{ \"lambda-url.ap-east-1.on.aws\", {} },\n        .{ \"lambda-url.ap-northeast-1.on.aws\", {} },\n        .{ \"lambda-url.ap-northeast-2.on.aws\", {} },\n        .{ \"lambda-url.ap-northeast-3.on.aws\", {} },\n        .{ \"lambda-url.ap-south-1.on.aws\", {} },\n        .{ \"lambda-url.ap-southeast-1.on.aws\", {} },\n        .{ \"lambda-url.ap-southeast-2.on.aws\", {} },\n        .{ \"lambda-url.ap-southeast-3.on.aws\", {} },\n        .{ \"lambda-url.ca-central-1.on.aws\", {} },\n        .{ \"lambda-url.eu-central-1.on.aws\", {} },\n        .{ \"lambda-url.eu-north-1.on.aws\", {} },\n        .{ \"lambda-url.eu-south-1.on.aws\", {} },\n        .{ \"lambda-url.eu-west-1.on.aws\", {} },\n        .{ \"lambda-url.eu-west-2.on.aws\", {} },\n        .{ \"lambda-url.eu-west-3.on.aws\", {} },\n        .{ \"lambda-url.me-south-1.on.aws\", {} },\n        .{ \"lambda-url.sa-east-1.on.aws\", {} },\n        .{ \"lambda-url.us-east-1.on.aws\", {} },\n        .{ \"lambda-url.us-east-2.on.aws\", {} },\n        .{ \"lambda-url.us-west-1.on.aws\", {} },\n        .{ \"lambda-url.us-west-2.on.aws\", {} },\n        .{ \"*.private.repost.aws\", {} },\n        .{ \"transfer-webapp.af-south-1.on.aws\", {} },\n        .{ \"transfer-webapp.ap-east-1.on.aws\", {} },\n        .{ \"transfer-webapp.ap-northeast-1.on.aws\", {} },\n        .{ \"transfer-webapp.ap-northeast-2.on.aws\", {} },\n        .{ \"transfer-webapp.ap-northeast-3.on.aws\", {} },\n        .{ \"transfer-webapp.ap-south-1.on.aws\", {} },\n        .{ \"transfer-webapp.ap-south-2.on.aws\", {} },\n        .{ \"transfer-webapp.ap-southeast-1.on.aws\", {} },\n        .{ \"transfer-webapp.ap-southeast-2.on.aws\", {} },\n        .{ \"transfer-webapp.ap-southeast-3.on.aws\", {} },\n        .{ \"transfer-webapp.ap-southeast-4.on.aws\", {} },\n        .{ \"transfer-webapp.ap-southeast-5.on.aws\", {} },\n        .{ \"transfer-webapp.ap-southeast-7.on.aws\", {} },\n        .{ \"transfer-webapp.ca-central-1.on.aws\", {} },\n        .{ \"transfer-webapp.ca-west-1.on.aws\", {} },\n        .{ \"transfer-webapp.eu-central-1.on.aws\", {} },\n        .{ \"transfer-webapp.eu-central-2.on.aws\", {} },\n        .{ \"transfer-webapp.eu-north-1.on.aws\", {} },\n        .{ \"transfer-webapp.eu-south-1.on.aws\", {} },\n        .{ \"transfer-webapp.eu-south-2.on.aws\", {} },\n        .{ \"transfer-webapp.eu-west-1.on.aws\", {} },\n        .{ \"transfer-webapp.eu-west-2.on.aws\", {} },\n        .{ \"transfer-webapp.eu-west-3.on.aws\", {} },\n        .{ \"transfer-webapp.il-central-1.on.aws\", {} },\n        .{ \"transfer-webapp.me-central-1.on.aws\", {} },\n        .{ \"transfer-webapp.me-south-1.on.aws\", {} },\n        .{ \"transfer-webapp.mx-central-1.on.aws\", {} },\n        .{ \"transfer-webapp.sa-east-1.on.aws\", {} },\n        .{ \"transfer-webapp.us-east-1.on.aws\", {} },\n        .{ \"transfer-webapp.us-east-2.on.aws\", {} },\n        .{ \"transfer-webapp.us-gov-east-1.on.aws\", {} },\n        .{ \"transfer-webapp-fips.us-gov-east-1.on.aws\", {} },\n        .{ \"transfer-webapp.us-gov-west-1.on.aws\", {} },\n        .{ \"transfer-webapp-fips.us-gov-west-1.on.aws\", {} },\n        .{ \"transfer-webapp.us-west-1.on.aws\", {} },\n        .{ \"transfer-webapp.us-west-2.on.aws\", {} },\n        .{ \"transfer-webapp.cn-north-1.on.amazonwebservices.com.cn\", {} },\n        .{ \"transfer-webapp.cn-northwest-1.on.amazonwebservices.com.cn\", {} },\n        .{ \"eero.online\", {} },\n        .{ \"eero-stage.online\", {} },\n        .{ \"antagonist.cloud\", {} },\n        .{ \"apigee.io\", {} },\n        .{ \"panel.dev\", {} },\n        .{ \"siiites.com\", {} },\n        .{ \"int.apple\", {} },\n        .{ \"*.cloud.int.apple\", {} },\n        .{ \"*.r.cloud.int.apple\", {} },\n        .{ \"*.ap-north-1.r.cloud.int.apple\", {} },\n        .{ \"*.ap-south-1.r.cloud.int.apple\", {} },\n        .{ \"*.ap-south-2.r.cloud.int.apple\", {} },\n        .{ \"*.eu-central-1.r.cloud.int.apple\", {} },\n        .{ \"*.eu-north-1.r.cloud.int.apple\", {} },\n        .{ \"*.us-central-1.r.cloud.int.apple\", {} },\n        .{ \"*.us-central-2.r.cloud.int.apple\", {} },\n        .{ \"*.us-east-1.r.cloud.int.apple\", {} },\n        .{ \"*.us-east-2.r.cloud.int.apple\", {} },\n        .{ \"*.us-west-1.r.cloud.int.apple\", {} },\n        .{ \"*.us-west-2.r.cloud.int.apple\", {} },\n        .{ \"*.us-west-3.r.cloud.int.apple\", {} },\n        .{ \"appspacehosted.com\", {} },\n        .{ \"appspaceusercontent.com\", {} },\n        .{ \"appudo.net\", {} },\n        .{ \"appwrite.global\", {} },\n        .{ \"appwrite.network\", {} },\n        .{ \"*.appwrite.run\", {} },\n        .{ \"on-aptible.com\", {} },\n        .{ \"f5.si\", {} },\n        .{ \"arvanedge.ir\", {} },\n        .{ \"user.aseinet.ne.jp\", {} },\n        .{ \"gv.vc\", {} },\n        .{ \"d.gv.vc\", {} },\n        .{ \"user.party.eus\", {} },\n        .{ \"pimienta.org\", {} },\n        .{ \"poivron.org\", {} },\n        .{ \"potager.org\", {} },\n        .{ \"sweetpepper.org\", {} },\n        .{ \"myasustor.com\", {} },\n        .{ \"cdn.prod.atlassian-dev.net\", {} },\n        .{ \"myfritz.link\", {} },\n        .{ \"myfritz.net\", {} },\n        .{ \"*.awdev.ca\", {} },\n        .{ \"*.advisor.ws\", {} },\n        .{ \"ecommerce-shop.pl\", {} },\n        .{ \"b-data.io\", {} },\n        .{ \"balena-devices.com\", {} },\n        .{ \"base.ec\", {} },\n        .{ \"official.ec\", {} },\n        .{ \"buyshop.jp\", {} },\n        .{ \"fashionstore.jp\", {} },\n        .{ \"handcrafted.jp\", {} },\n        .{ \"kawaiishop.jp\", {} },\n        .{ \"supersale.jp\", {} },\n        .{ \"theshop.jp\", {} },\n        .{ \"shopselect.net\", {} },\n        .{ \"base.shop\", {} },\n        .{ \"beagleboard.io\", {} },\n        .{ \"bearblog.dev\", {} },\n        .{ \"*.beget.app\", {} },\n        .{ \"pages.gay\", {} },\n        .{ \"bnr.la\", {} },\n        .{ \"bitbucket.io\", {} },\n        .{ \"blackbaudcdn.net\", {} },\n        .{ \"of.je\", {} },\n        .{ \"square.site\", {} },\n        .{ \"bluebite.io\", {} },\n        .{ \"boomla.net\", {} },\n        .{ \"boutir.com\", {} },\n        .{ \"boxfuse.io\", {} },\n        .{ \"square7.ch\", {} },\n        .{ \"bplaced.com\", {} },\n        .{ \"bplaced.de\", {} },\n        .{ \"square7.de\", {} },\n        .{ \"bplaced.net\", {} },\n        .{ \"square7.net\", {} },\n        .{ \"brave.app\", {} },\n        .{ \"*.s.brave.app\", {} },\n        .{ \"brave.dev\", {} },\n        .{ \"*.s.brave.dev\", {} },\n        .{ \"brave.io\", {} },\n        .{ \"*.s.brave.io\", {} },\n        .{ \"shop.brendly.ba\", {} },\n        .{ \"shop.brendly.hr\", {} },\n        .{ \"shop.brendly.rs\", {} },\n        .{ \"browsersafetymark.io\", {} },\n        .{ \"radio.am\", {} },\n        .{ \"radio.fm\", {} },\n        .{ \"cdn.bubble.io\", {} },\n        .{ \"bubbleapps.io\", {} },\n        .{ \"*.bwcloud-os-instance.de\", {} },\n        .{ \"uk0.bigv.io\", {} },\n        .{ \"dh.bytemark.co.uk\", {} },\n        .{ \"vm.bytemark.co.uk\", {} },\n        .{ \"cafjs.com\", {} },\n        .{ \"canva-apps.cn\", {} },\n        .{ \"my.canvasite.cn\", {} },\n        .{ \"canva-apps.com\", {} },\n        .{ \"canva-hosted-embed.com\", {} },\n        .{ \"canvacode.com\", {} },\n        .{ \"rice-labs.com\", {} },\n        .{ \"canva.run\", {} },\n        .{ \"my.canva.site\", {} },\n        .{ \"drr.ac\", {} },\n        .{ \"uwu.ai\", {} },\n        .{ \"carrd.co\", {} },\n        .{ \"crd.co\", {} },\n        .{ \"ju.mp\", {} },\n        .{ \"api.gov.uk\", {} },\n        .{ \"cdn77-storage.com\", {} },\n        .{ \"rsc.contentproxy9.cz\", {} },\n        .{ \"r.cdn77.net\", {} },\n        .{ \"cdn77-ssl.net\", {} },\n        .{ \"c.cdn77.org\", {} },\n        .{ \"rsc.cdn77.org\", {} },\n        .{ \"ssl.origin.cdn77-secure.org\", {} },\n        .{ \"za.bz\", {} },\n        .{ \"br.com\", {} },\n        .{ \"cn.com\", {} },\n        .{ \"de.com\", {} },\n        .{ \"eu.com\", {} },\n        .{ \"jpn.com\", {} },\n        .{ \"mex.com\", {} },\n        .{ \"ru.com\", {} },\n        .{ \"sa.com\", {} },\n        .{ \"uk.com\", {} },\n        .{ \"us.com\", {} },\n        .{ \"za.com\", {} },\n        .{ \"com.de\", {} },\n        .{ \"gb.net\", {} },\n        .{ \"hu.net\", {} },\n        .{ \"jp.net\", {} },\n        .{ \"se.net\", {} },\n        .{ \"uk.net\", {} },\n        .{ \"ae.org\", {} },\n        .{ \"com.se\", {} },\n        .{ \"cx.ua\", {} },\n        .{ \"discourse.diy\", {} },\n        .{ \"discourse.group\", {} },\n        .{ \"discourse.team\", {} },\n        .{ \"clerk.app\", {} },\n        .{ \"clerkstage.app\", {} },\n        .{ \"*.lcl.dev\", {} },\n        .{ \"*.lclstage.dev\", {} },\n        .{ \"*.stg.dev\", {} },\n        .{ \"*.stgstage.dev\", {} },\n        .{ \"cleverapps.cc\", {} },\n        .{ \"*.services.clever-cloud.com\", {} },\n        .{ \"cleverapps.io\", {} },\n        .{ \"cleverapps.tech\", {} },\n        .{ \"clickrising.net\", {} },\n        .{ \"cloudns.asia\", {} },\n        .{ \"cloudns.be\", {} },\n        .{ \"cloud-ip.biz\", {} },\n        .{ \"cloudns.biz\", {} },\n        .{ \"cloud-ip.cc\", {} },\n        .{ \"cloudns.cc\", {} },\n        .{ \"cloudns.ch\", {} },\n        .{ \"cloudns.cl\", {} },\n        .{ \"cloudns.club\", {} },\n        .{ \"abrdns.com\", {} },\n        .{ \"dnsabr.com\", {} },\n        .{ \"ip-ddns.com\", {} },\n        .{ \"cloudns.cx\", {} },\n        .{ \"cloudns.eu\", {} },\n        .{ \"cloudns.in\", {} },\n        .{ \"cloudns.info\", {} },\n        .{ \"ddns-ip.net\", {} },\n        .{ \"dns-cloud.net\", {} },\n        .{ \"dns-dynamic.net\", {} },\n        .{ \"cloudns.nz\", {} },\n        .{ \"cloudns.org\", {} },\n        .{ \"ip-dynamic.org\", {} },\n        .{ \"cloudns.ph\", {} },\n        .{ \"cloudns.pro\", {} },\n        .{ \"cloudns.pw\", {} },\n        .{ \"cloudns.us\", {} },\n        .{ \"c66.me\", {} },\n        .{ \"cloud66.ws\", {} },\n        .{ \"jdevcloud.com\", {} },\n        .{ \"wpdevcloud.com\", {} },\n        .{ \"cloudaccess.host\", {} },\n        .{ \"freesite.host\", {} },\n        .{ \"cloudaccess.net\", {} },\n        .{ \"cloudbeesusercontent.io\", {} },\n        .{ \"*.cloudera.site\", {} },\n        .{ \"cloudflare.app\", {} },\n        .{ \"cf-ipfs.com\", {} },\n        .{ \"cloudflare-ipfs.com\", {} },\n        .{ \"trycloudflare.com\", {} },\n        .{ \"pages.dev\", {} },\n        .{ \"r2.dev\", {} },\n        .{ \"workers.dev\", {} },\n        .{ \"cloudflare.net\", {} },\n        .{ \"cdn.cloudflare.net\", {} },\n        .{ \"cdn.cloudflareanycast.net\", {} },\n        .{ \"cdn.cloudflarecn.net\", {} },\n        .{ \"cdn.cloudflareglobal.net\", {} },\n        .{ \"cust.cloudscale.ch\", {} },\n        .{ \"objects.lpg.cloudscale.ch\", {} },\n        .{ \"objects.rma.cloudscale.ch\", {} },\n        .{ \"lpg.objectstorage.ch\", {} },\n        .{ \"rma.objectstorage.ch\", {} },\n        .{ \"wnext.app\", {} },\n        .{ \"cnpy.gdn\", {} },\n        .{ \"*.otap.co\", {} },\n        .{ \"co.ca\", {} },\n        .{ \"co.com\", {} },\n        .{ \"codeberg.page\", {} },\n        .{ \"csb.app\", {} },\n        .{ \"preview.csb.app\", {} },\n        .{ \"co.nl\", {} },\n        .{ \"co.no\", {} },\n        .{ \"*.devinapps.com\", {} },\n        .{ \"webhosting.be\", {} },\n        .{ \"prvw.eu\", {} },\n        .{ \"hosting-cluster.nl\", {} },\n        .{ \"ctfcloud.net\", {} },\n        .{ \"convex.app\", {} },\n        .{ \"convex.cloud\", {} },\n        .{ \"convex.site\", {} },\n        .{ \"ac.ru\", {} },\n        .{ \"edu.ru\", {} },\n        .{ \"gov.ru\", {} },\n        .{ \"int.ru\", {} },\n        .{ \"mil.ru\", {} },\n        .{ \"corespeed.app\", {} },\n        .{ \"dyn.cosidns.de\", {} },\n        .{ \"dnsupdater.de\", {} },\n        .{ \"dynamisches-dns.de\", {} },\n        .{ \"internet-dns.de\", {} },\n        .{ \"l-o-g-i-n.de\", {} },\n        .{ \"dynamic-dns.info\", {} },\n        .{ \"feste-ip.net\", {} },\n        .{ \"knx-server.net\", {} },\n        .{ \"static-access.net\", {} },\n        .{ \"craft.me\", {} },\n        .{ \"realm.cz\", {} },\n        .{ \"on.crisp.email\", {} },\n        .{ \"*.cryptonomic.net\", {} },\n        .{ \"cfolks.pl\", {} },\n        .{ \"cyon.link\", {} },\n        .{ \"cyon.site\", {} },\n        .{ \"biz.dk\", {} },\n        .{ \"co.dk\", {} },\n        .{ \"firm.dk\", {} },\n        .{ \"reg.dk\", {} },\n        .{ \"store.dk\", {} },\n        .{ \"dyndns.dappnode.io\", {} },\n        .{ \"builtwithdark.com\", {} },\n        .{ \"darklang.io\", {} },\n        .{ \"demo.datadetect.com\", {} },\n        .{ \"instance.datadetect.com\", {} },\n        .{ \"edgestack.me\", {} },\n        .{ \"dattolocal.com\", {} },\n        .{ \"dattorelay.com\", {} },\n        .{ \"dattoweb.com\", {} },\n        .{ \"mydatto.com\", {} },\n        .{ \"dattolocal.net\", {} },\n        .{ \"mydatto.net\", {} },\n        .{ \"ddnss.de\", {} },\n        .{ \"dyn.ddnss.de\", {} },\n        .{ \"dyndns.ddnss.de\", {} },\n        .{ \"dyn-ip24.de\", {} },\n        .{ \"dyndns1.de\", {} },\n        .{ \"home-webserver.de\", {} },\n        .{ \"dyn.home-webserver.de\", {} },\n        .{ \"myhome-server.de\", {} },\n        .{ \"ddnss.org\", {} },\n        .{ \"debian.net\", {} },\n        .{ \"definima.io\", {} },\n        .{ \"definima.net\", {} },\n        .{ \"deno.dev\", {} },\n        .{ \"deno-staging.dev\", {} },\n        .{ \"deno.net\", {} },\n        .{ \"dedyn.io\", {} },\n        .{ \"deta.app\", {} },\n        .{ \"deta.dev\", {} },\n        .{ \"deuxfleurs.eu\", {} },\n        .{ \"deuxfleurs.page\", {} },\n        .{ \"*.at.ply.gg\", {} },\n        .{ \"d6.ply.gg\", {} },\n        .{ \"joinmc.link\", {} },\n        .{ \"playit.plus\", {} },\n        .{ \"*.at.playit.plus\", {} },\n        .{ \"with.playit.plus\", {} },\n        .{ \"icp0.io\", {} },\n        .{ \"*.raw.icp0.io\", {} },\n        .{ \"icp1.io\", {} },\n        .{ \"*.raw.icp1.io\", {} },\n        .{ \"*.icp.net\", {} },\n        .{ \"caffeine.site\", {} },\n        .{ \"caffeine.xyz\", {} },\n        .{ \"dfirma.pl\", {} },\n        .{ \"dkonto.pl\", {} },\n        .{ \"you2.pl\", {} },\n        .{ \"ondigitalocean.app\", {} },\n        .{ \"*.digitaloceanspaces.com\", {} },\n        .{ \"qzz.io\", {} },\n        .{ \"us.kg\", {} },\n        .{ \"xx.kg\", {} },\n        .{ \"dpdns.org\", {} },\n        .{ \"discordsays.com\", {} },\n        .{ \"discordsez.com\", {} },\n        .{ \"jozi.biz\", {} },\n        .{ \"ccwu.cc\", {} },\n        .{ \"cc.cd\", {} },\n        .{ \"us.ci\", {} },\n        .{ \"de5.net\", {} },\n        .{ \"dnshome.de\", {} },\n        .{ \"online.th\", {} },\n        .{ \"shop.th\", {} },\n        .{ \"co.scot\", {} },\n        .{ \"me.scot\", {} },\n        .{ \"org.scot\", {} },\n        .{ \"drayddns.com\", {} },\n        .{ \"shoparena.pl\", {} },\n        .{ \"dreamhosters.com\", {} },\n        .{ \"durumis.com\", {} },\n        .{ \"duckdns.org\", {} },\n        .{ \"dy.fi\", {} },\n        .{ \"tunk.org\", {} },\n        .{ \"dyndns.biz\", {} },\n        .{ \"for-better.biz\", {} },\n        .{ \"for-more.biz\", {} },\n        .{ \"for-some.biz\", {} },\n        .{ \"for-the.biz\", {} },\n        .{ \"selfip.biz\", {} },\n        .{ \"webhop.biz\", {} },\n        .{ \"ftpaccess.cc\", {} },\n        .{ \"game-server.cc\", {} },\n        .{ \"myphotos.cc\", {} },\n        .{ \"scrapping.cc\", {} },\n        .{ \"blogdns.com\", {} },\n        .{ \"cechire.com\", {} },\n        .{ \"dnsalias.com\", {} },\n        .{ \"dnsdojo.com\", {} },\n        .{ \"doesntexist.com\", {} },\n        .{ \"dontexist.com\", {} },\n        .{ \"doomdns.com\", {} },\n        .{ \"dyn-o-saur.com\", {} },\n        .{ \"dynalias.com\", {} },\n        .{ \"dyndns-at-home.com\", {} },\n        .{ \"dyndns-at-work.com\", {} },\n        .{ \"dyndns-blog.com\", {} },\n        .{ \"dyndns-free.com\", {} },\n        .{ \"dyndns-home.com\", {} },\n        .{ \"dyndns-ip.com\", {} },\n        .{ \"dyndns-mail.com\", {} },\n        .{ \"dyndns-office.com\", {} },\n        .{ \"dyndns-pics.com\", {} },\n        .{ \"dyndns-remote.com\", {} },\n        .{ \"dyndns-server.com\", {} },\n        .{ \"dyndns-web.com\", {} },\n        .{ \"dyndns-wiki.com\", {} },\n        .{ \"dyndns-work.com\", {} },\n        .{ \"est-a-la-maison.com\", {} },\n        .{ \"est-a-la-masion.com\", {} },\n        .{ \"est-le-patron.com\", {} },\n        .{ \"est-mon-blogueur.com\", {} },\n        .{ \"from-ak.com\", {} },\n        .{ \"from-al.com\", {} },\n        .{ \"from-ar.com\", {} },\n        .{ \"from-ca.com\", {} },\n        .{ \"from-ct.com\", {} },\n        .{ \"from-dc.com\", {} },\n        .{ \"from-de.com\", {} },\n        .{ \"from-fl.com\", {} },\n        .{ \"from-ga.com\", {} },\n        .{ \"from-hi.com\", {} },\n        .{ \"from-ia.com\", {} },\n        .{ \"from-id.com\", {} },\n        .{ \"from-il.com\", {} },\n        .{ \"from-in.com\", {} },\n        .{ \"from-ks.com\", {} },\n        .{ \"from-ky.com\", {} },\n        .{ \"from-ma.com\", {} },\n        .{ \"from-md.com\", {} },\n        .{ \"from-mi.com\", {} },\n        .{ \"from-mn.com\", {} },\n        .{ \"from-mo.com\", {} },\n        .{ \"from-ms.com\", {} },\n        .{ \"from-mt.com\", {} },\n        .{ \"from-nc.com\", {} },\n        .{ \"from-nd.com\", {} },\n        .{ \"from-ne.com\", {} },\n        .{ \"from-nh.com\", {} },\n        .{ \"from-nj.com\", {} },\n        .{ \"from-nm.com\", {} },\n        .{ \"from-nv.com\", {} },\n        .{ \"from-oh.com\", {} },\n        .{ \"from-ok.com\", {} },\n        .{ \"from-or.com\", {} },\n        .{ \"from-pa.com\", {} },\n        .{ \"from-pr.com\", {} },\n        .{ \"from-ri.com\", {} },\n        .{ \"from-sc.com\", {} },\n        .{ \"from-sd.com\", {} },\n        .{ \"from-tn.com\", {} },\n        .{ \"from-tx.com\", {} },\n        .{ \"from-ut.com\", {} },\n        .{ \"from-va.com\", {} },\n        .{ \"from-vt.com\", {} },\n        .{ \"from-wa.com\", {} },\n        .{ \"from-wi.com\", {} },\n        .{ \"from-wv.com\", {} },\n        .{ \"from-wy.com\", {} },\n        .{ \"getmyip.com\", {} },\n        .{ \"gotdns.com\", {} },\n        .{ \"hobby-site.com\", {} },\n        .{ \"homelinux.com\", {} },\n        .{ \"homeunix.com\", {} },\n        .{ \"iamallama.com\", {} },\n        .{ \"is-a-anarchist.com\", {} },\n        .{ \"is-a-blogger.com\", {} },\n        .{ \"is-a-bookkeeper.com\", {} },\n        .{ \"is-a-bulls-fan.com\", {} },\n        .{ \"is-a-caterer.com\", {} },\n        .{ \"is-a-chef.com\", {} },\n        .{ \"is-a-conservative.com\", {} },\n        .{ \"is-a-cpa.com\", {} },\n        .{ \"is-a-cubicle-slave.com\", {} },\n        .{ \"is-a-democrat.com\", {} },\n        .{ \"is-a-designer.com\", {} },\n        .{ \"is-a-doctor.com\", {} },\n        .{ \"is-a-financialadvisor.com\", {} },\n        .{ \"is-a-geek.com\", {} },\n        .{ \"is-a-green.com\", {} },\n        .{ \"is-a-guru.com\", {} },\n        .{ \"is-a-hard-worker.com\", {} },\n        .{ \"is-a-hunter.com\", {} },\n        .{ \"is-a-landscaper.com\", {} },\n        .{ \"is-a-lawyer.com\", {} },\n        .{ \"is-a-liberal.com\", {} },\n        .{ \"is-a-libertarian.com\", {} },\n        .{ \"is-a-llama.com\", {} },\n        .{ \"is-a-musician.com\", {} },\n        .{ \"is-a-nascarfan.com\", {} },\n        .{ \"is-a-nurse.com\", {} },\n        .{ \"is-a-painter.com\", {} },\n        .{ \"is-a-personaltrainer.com\", {} },\n        .{ \"is-a-photographer.com\", {} },\n        .{ \"is-a-player.com\", {} },\n        .{ \"is-a-republican.com\", {} },\n        .{ \"is-a-rockstar.com\", {} },\n        .{ \"is-a-socialist.com\", {} },\n        .{ \"is-a-student.com\", {} },\n        .{ \"is-a-teacher.com\", {} },\n        .{ \"is-a-techie.com\", {} },\n        .{ \"is-a-therapist.com\", {} },\n        .{ \"is-an-accountant.com\", {} },\n        .{ \"is-an-actor.com\", {} },\n        .{ \"is-an-actress.com\", {} },\n        .{ \"is-an-anarchist.com\", {} },\n        .{ \"is-an-artist.com\", {} },\n        .{ \"is-an-engineer.com\", {} },\n        .{ \"is-an-entertainer.com\", {} },\n        .{ \"is-certified.com\", {} },\n        .{ \"is-gone.com\", {} },\n        .{ \"is-into-anime.com\", {} },\n        .{ \"is-into-cars.com\", {} },\n        .{ \"is-into-cartoons.com\", {} },\n        .{ \"is-into-games.com\", {} },\n        .{ \"is-leet.com\", {} },\n        .{ \"is-not-certified.com\", {} },\n        .{ \"is-slick.com\", {} },\n        .{ \"is-uberleet.com\", {} },\n        .{ \"is-with-theband.com\", {} },\n        .{ \"isa-geek.com\", {} },\n        .{ \"isa-hockeynut.com\", {} },\n        .{ \"issmarterthanyou.com\", {} },\n        .{ \"likes-pie.com\", {} },\n        .{ \"likescandy.com\", {} },\n        .{ \"neat-url.com\", {} },\n        .{ \"saves-the-whales.com\", {} },\n        .{ \"selfip.com\", {} },\n        .{ \"sells-for-less.com\", {} },\n        .{ \"sells-for-u.com\", {} },\n        .{ \"servebbs.com\", {} },\n        .{ \"simple-url.com\", {} },\n        .{ \"space-to-rent.com\", {} },\n        .{ \"teaches-yoga.com\", {} },\n        .{ \"writesthisblog.com\", {} },\n        .{ \"ath.cx\", {} },\n        .{ \"fuettertdasnetz.de\", {} },\n        .{ \"isteingeek.de\", {} },\n        .{ \"istmein.de\", {} },\n        .{ \"lebtimnetz.de\", {} },\n        .{ \"leitungsen.de\", {} },\n        .{ \"traeumtgerade.de\", {} },\n        .{ \"barrel-of-knowledge.info\", {} },\n        .{ \"barrell-of-knowledge.info\", {} },\n        .{ \"dyndns.info\", {} },\n        .{ \"for-our.info\", {} },\n        .{ \"groks-the.info\", {} },\n        .{ \"groks-this.info\", {} },\n        .{ \"here-for-more.info\", {} },\n        .{ \"knowsitall.info\", {} },\n        .{ \"selfip.info\", {} },\n        .{ \"webhop.info\", {} },\n        .{ \"forgot.her.name\", {} },\n        .{ \"forgot.his.name\", {} },\n        .{ \"at-band-camp.net\", {} },\n        .{ \"blogdns.net\", {} },\n        .{ \"broke-it.net\", {} },\n        .{ \"buyshouses.net\", {} },\n        .{ \"dnsalias.net\", {} },\n        .{ \"dnsdojo.net\", {} },\n        .{ \"does-it.net\", {} },\n        .{ \"dontexist.net\", {} },\n        .{ \"dynalias.net\", {} },\n        .{ \"dynathome.net\", {} },\n        .{ \"endofinternet.net\", {} },\n        .{ \"from-az.net\", {} },\n        .{ \"from-co.net\", {} },\n        .{ \"from-la.net\", {} },\n        .{ \"from-ny.net\", {} },\n        .{ \"gets-it.net\", {} },\n        .{ \"ham-radio-op.net\", {} },\n        .{ \"homeftp.net\", {} },\n        .{ \"homeip.net\", {} },\n        .{ \"homelinux.net\", {} },\n        .{ \"homeunix.net\", {} },\n        .{ \"in-the-band.net\", {} },\n        .{ \"is-a-chef.net\", {} },\n        .{ \"is-a-geek.net\", {} },\n        .{ \"isa-geek.net\", {} },\n        .{ \"kicks-ass.net\", {} },\n        .{ \"office-on-the.net\", {} },\n        .{ \"podzone.net\", {} },\n        .{ \"scrapper-site.net\", {} },\n        .{ \"selfip.net\", {} },\n        .{ \"sells-it.net\", {} },\n        .{ \"servebbs.net\", {} },\n        .{ \"serveftp.net\", {} },\n        .{ \"thruhere.net\", {} },\n        .{ \"webhop.net\", {} },\n        .{ \"merseine.nu\", {} },\n        .{ \"mine.nu\", {} },\n        .{ \"shacknet.nu\", {} },\n        .{ \"blogdns.org\", {} },\n        .{ \"blogsite.org\", {} },\n        .{ \"boldlygoingnowhere.org\", {} },\n        .{ \"dnsalias.org\", {} },\n        .{ \"dnsdojo.org\", {} },\n        .{ \"doesntexist.org\", {} },\n        .{ \"dontexist.org\", {} },\n        .{ \"doomdns.org\", {} },\n        .{ \"dvrdns.org\", {} },\n        .{ \"dynalias.org\", {} },\n        .{ \"dyndns.org\", {} },\n        .{ \"go.dyndns.org\", {} },\n        .{ \"home.dyndns.org\", {} },\n        .{ \"endofinternet.org\", {} },\n        .{ \"endoftheinternet.org\", {} },\n        .{ \"from-me.org\", {} },\n        .{ \"game-host.org\", {} },\n        .{ \"gotdns.org\", {} },\n        .{ \"hobby-site.org\", {} },\n        .{ \"homedns.org\", {} },\n        .{ \"homeftp.org\", {} },\n        .{ \"homelinux.org\", {} },\n        .{ \"homeunix.org\", {} },\n        .{ \"is-a-bruinsfan.org\", {} },\n        .{ \"is-a-candidate.org\", {} },\n        .{ \"is-a-celticsfan.org\", {} },\n        .{ \"is-a-chef.org\", {} },\n        .{ \"is-a-geek.org\", {} },\n        .{ \"is-a-knight.org\", {} },\n        .{ \"is-a-linux-user.org\", {} },\n        .{ \"is-a-patsfan.org\", {} },\n        .{ \"is-a-soxfan.org\", {} },\n        .{ \"is-found.org\", {} },\n        .{ \"is-lost.org\", {} },\n        .{ \"is-saved.org\", {} },\n        .{ \"is-very-bad.org\", {} },\n        .{ \"is-very-evil.org\", {} },\n        .{ \"is-very-good.org\", {} },\n        .{ \"is-very-nice.org\", {} },\n        .{ \"is-very-sweet.org\", {} },\n        .{ \"isa-geek.org\", {} },\n        .{ \"kicks-ass.org\", {} },\n        .{ \"misconfused.org\", {} },\n        .{ \"podzone.org\", {} },\n        .{ \"readmyblog.org\", {} },\n        .{ \"selfip.org\", {} },\n        .{ \"sellsyourhome.org\", {} },\n        .{ \"servebbs.org\", {} },\n        .{ \"serveftp.org\", {} },\n        .{ \"servegame.org\", {} },\n        .{ \"stuff-4-sale.org\", {} },\n        .{ \"webhop.org\", {} },\n        .{ \"better-than.tv\", {} },\n        .{ \"dyndns.tv\", {} },\n        .{ \"on-the-web.tv\", {} },\n        .{ \"worse-than.tv\", {} },\n        .{ \"is-by.us\", {} },\n        .{ \"land-4-sale.us\", {} },\n        .{ \"stuff-4-sale.us\", {} },\n        .{ \"dyndns.ws\", {} },\n        .{ \"mypets.ws\", {} },\n        .{ \"ddnsfree.com\", {} },\n        .{ \"ddnsgeek.com\", {} },\n        .{ \"giize.com\", {} },\n        .{ \"gleeze.com\", {} },\n        .{ \"kozow.com\", {} },\n        .{ \"loseyourip.com\", {} },\n        .{ \"ooguy.com\", {} },\n        .{ \"theworkpc.com\", {} },\n        .{ \"casacam.net\", {} },\n        .{ \"dynu.net\", {} },\n        .{ \"accesscam.org\", {} },\n        .{ \"camdvr.org\", {} },\n        .{ \"freeddns.org\", {} },\n        .{ \"mywire.org\", {} },\n        .{ \"webredirect.org\", {} },\n        .{ \"myddns.rocks\", {} },\n        .{ \"dynv6.net\", {} },\n        .{ \"e4.cz\", {} },\n        .{ \"easypanel.app\", {} },\n        .{ \"easypanel.host\", {} },\n        .{ \"*.ewp.live\", {} },\n        .{ \"twmail.cc\", {} },\n        .{ \"twmail.net\", {} },\n        .{ \"twmail.org\", {} },\n        .{ \"mymailer.com.tw\", {} },\n        .{ \"url.tw\", {} },\n        .{ \"at.emf.camp\", {} },\n        .{ \"rt.ht\", {} },\n        .{ \"elementor.cloud\", {} },\n        .{ \"elementor.cool\", {} },\n        .{ \"emergent.cloud\", {} },\n        .{ \"preview.emergentagent.com\", {} },\n        .{ \"emergent.host\", {} },\n        .{ \"mytuleap.com\", {} },\n        .{ \"tuleap-partners.com\", {} },\n        .{ \"encr.app\", {} },\n        .{ \"frontend.encr.app\", {} },\n        .{ \"encoreapi.com\", {} },\n        .{ \"lp.dev\", {} },\n        .{ \"api.lp.dev\", {} },\n        .{ \"objects.lp.dev\", {} },\n        .{ \"eu.encoway.cloud\", {} },\n        .{ \"eu.org\", {} },\n        .{ \"al.eu.org\", {} },\n        .{ \"asso.eu.org\", {} },\n        .{ \"at.eu.org\", {} },\n        .{ \"au.eu.org\", {} },\n        .{ \"be.eu.org\", {} },\n        .{ \"bg.eu.org\", {} },\n        .{ \"ca.eu.org\", {} },\n        .{ \"cd.eu.org\", {} },\n        .{ \"ch.eu.org\", {} },\n        .{ \"cn.eu.org\", {} },\n        .{ \"cy.eu.org\", {} },\n        .{ \"cz.eu.org\", {} },\n        .{ \"de.eu.org\", {} },\n        .{ \"dk.eu.org\", {} },\n        .{ \"edu.eu.org\", {} },\n        .{ \"ee.eu.org\", {} },\n        .{ \"es.eu.org\", {} },\n        .{ \"fi.eu.org\", {} },\n        .{ \"fr.eu.org\", {} },\n        .{ \"gr.eu.org\", {} },\n        .{ \"hr.eu.org\", {} },\n        .{ \"hu.eu.org\", {} },\n        .{ \"ie.eu.org\", {} },\n        .{ \"il.eu.org\", {} },\n        .{ \"in.eu.org\", {} },\n        .{ \"int.eu.org\", {} },\n        .{ \"is.eu.org\", {} },\n        .{ \"it.eu.org\", {} },\n        .{ \"jp.eu.org\", {} },\n        .{ \"kr.eu.org\", {} },\n        .{ \"lt.eu.org\", {} },\n        .{ \"lu.eu.org\", {} },\n        .{ \"lv.eu.org\", {} },\n        .{ \"me.eu.org\", {} },\n        .{ \"mk.eu.org\", {} },\n        .{ \"mt.eu.org\", {} },\n        .{ \"my.eu.org\", {} },\n        .{ \"net.eu.org\", {} },\n        .{ \"ng.eu.org\", {} },\n        .{ \"nl.eu.org\", {} },\n        .{ \"no.eu.org\", {} },\n        .{ \"nz.eu.org\", {} },\n        .{ \"pl.eu.org\", {} },\n        .{ \"pt.eu.org\", {} },\n        .{ \"ro.eu.org\", {} },\n        .{ \"ru.eu.org\", {} },\n        .{ \"se.eu.org\", {} },\n        .{ \"si.eu.org\", {} },\n        .{ \"sk.eu.org\", {} },\n        .{ \"tr.eu.org\", {} },\n        .{ \"uk.eu.org\", {} },\n        .{ \"us.eu.org\", {} },\n        .{ \"eurodir.ru\", {} },\n        .{ \"eu-1.evennode.com\", {} },\n        .{ \"eu-2.evennode.com\", {} },\n        .{ \"eu-3.evennode.com\", {} },\n        .{ \"eu-4.evennode.com\", {} },\n        .{ \"us-1.evennode.com\", {} },\n        .{ \"us-2.evennode.com\", {} },\n        .{ \"us-3.evennode.com\", {} },\n        .{ \"us-4.evennode.com\", {} },\n        .{ \"relay.evervault.app\", {} },\n        .{ \"relay.evervault.dev\", {} },\n        .{ \"expo.app\", {} },\n        .{ \"staging.expo.app\", {} },\n        .{ \"onfabrica.com\", {} },\n        .{ \"ru.net\", {} },\n        .{ \"adygeya.ru\", {} },\n        .{ \"bashkiria.ru\", {} },\n        .{ \"bir.ru\", {} },\n        .{ \"cbg.ru\", {} },\n        .{ \"com.ru\", {} },\n        .{ \"dagestan.ru\", {} },\n        .{ \"grozny.ru\", {} },\n        .{ \"kalmykia.ru\", {} },\n        .{ \"kustanai.ru\", {} },\n        .{ \"marine.ru\", {} },\n        .{ \"mordovia.ru\", {} },\n        .{ \"msk.ru\", {} },\n        .{ \"mytis.ru\", {} },\n        .{ \"nalchik.ru\", {} },\n        .{ \"nov.ru\", {} },\n        .{ \"pyatigorsk.ru\", {} },\n        .{ \"spb.ru\", {} },\n        .{ \"vladikavkaz.ru\", {} },\n        .{ \"vladimir.ru\", {} },\n        .{ \"abkhazia.su\", {} },\n        .{ \"adygeya.su\", {} },\n        .{ \"aktyubinsk.su\", {} },\n        .{ \"arkhangelsk.su\", {} },\n        .{ \"armenia.su\", {} },\n        .{ \"ashgabad.su\", {} },\n        .{ \"azerbaijan.su\", {} },\n        .{ \"balashov.su\", {} },\n        .{ \"bashkiria.su\", {} },\n        .{ \"bryansk.su\", {} },\n        .{ \"bukhara.su\", {} },\n        .{ \"chimkent.su\", {} },\n        .{ \"dagestan.su\", {} },\n        .{ \"east-kazakhstan.su\", {} },\n        .{ \"exnet.su\", {} },\n        .{ \"georgia.su\", {} },\n        .{ \"grozny.su\", {} },\n        .{ \"ivanovo.su\", {} },\n        .{ \"jambyl.su\", {} },\n        .{ \"kalmykia.su\", {} },\n        .{ \"kaluga.su\", {} },\n        .{ \"karacol.su\", {} },\n        .{ \"karaganda.su\", {} },\n        .{ \"karelia.su\", {} },\n        .{ \"khakassia.su\", {} },\n        .{ \"krasnodar.su\", {} },\n        .{ \"kurgan.su\", {} },\n        .{ \"kustanai.su\", {} },\n        .{ \"lenug.su\", {} },\n        .{ \"mangyshlak.su\", {} },\n        .{ \"mordovia.su\", {} },\n        .{ \"msk.su\", {} },\n        .{ \"murmansk.su\", {} },\n        .{ \"nalchik.su\", {} },\n        .{ \"navoi.su\", {} },\n        .{ \"north-kazakhstan.su\", {} },\n        .{ \"nov.su\", {} },\n        .{ \"obninsk.su\", {} },\n        .{ \"penza.su\", {} },\n        .{ \"pokrovsk.su\", {} },\n        .{ \"sochi.su\", {} },\n        .{ \"spb.su\", {} },\n        .{ \"tashkent.su\", {} },\n        .{ \"termez.su\", {} },\n        .{ \"togliatti.su\", {} },\n        .{ \"troitsk.su\", {} },\n        .{ \"tselinograd.su\", {} },\n        .{ \"tula.su\", {} },\n        .{ \"tuva.su\", {} },\n        .{ \"vladikavkaz.su\", {} },\n        .{ \"vladimir.su\", {} },\n        .{ \"vologda.su\", {} },\n        .{ \"channelsdvr.net\", {} },\n        .{ \"u.channelsdvr.net\", {} },\n        .{ \"edgecompute.app\", {} },\n        .{ \"fastly-edge.com\", {} },\n        .{ \"fastly-terrarium.com\", {} },\n        .{ \"freetls.fastly.net\", {} },\n        .{ \"map.fastly.net\", {} },\n        .{ \"a.prod.fastly.net\", {} },\n        .{ \"global.prod.fastly.net\", {} },\n        .{ \"a.ssl.fastly.net\", {} },\n        .{ \"b.ssl.fastly.net\", {} },\n        .{ \"global.ssl.fastly.net\", {} },\n        .{ \"fastlylb.net\", {} },\n        .{ \"map.fastlylb.net\", {} },\n        .{ \"*.user.fm\", {} },\n        .{ \"fastvps-server.com\", {} },\n        .{ \"fastvps.host\", {} },\n        .{ \"myfast.host\", {} },\n        .{ \"fastvps.site\", {} },\n        .{ \"myfast.space\", {} },\n        .{ \"conn.uk\", {} },\n        .{ \"copro.uk\", {} },\n        .{ \"hosp.uk\", {} },\n        .{ \"fedorainfracloud.org\", {} },\n        .{ \"fedorapeople.org\", {} },\n        .{ \"cloud.fedoraproject.org\", {} },\n        .{ \"app.os.fedoraproject.org\", {} },\n        .{ \"app.os.stg.fedoraproject.org\", {} },\n        .{ \"mydobiss.com\", {} },\n        .{ \"fh-muenster.io\", {} },\n        .{ \"figma.site\", {} },\n        .{ \"figma-gov.site\", {} },\n        .{ \"preview.site\", {} },\n        .{ \"filegear.me\", {} },\n        .{ \"firebaseapp.com\", {} },\n        .{ \"fldrv.com\", {} },\n        .{ \"on-fleek.app\", {} },\n        .{ \"flutterflow.app\", {} },\n        .{ \"sprites.app\", {} },\n        .{ \"fly.dev\", {} },\n        .{ \"e2b.app\", {} },\n        .{ \"framer.ai\", {} },\n        .{ \"framer.app\", {} },\n        .{ \"framercanvas.com\", {} },\n        .{ \"framer.media\", {} },\n        .{ \"framer.photos\", {} },\n        .{ \"framer.website\", {} },\n        .{ \"framer.wiki\", {} },\n        .{ \"*.0e.vc\", {} },\n        .{ \"freebox-os.com\", {} },\n        .{ \"freeboxos.com\", {} },\n        .{ \"fbx-os.fr\", {} },\n        .{ \"fbxos.fr\", {} },\n        .{ \"freebox-os.fr\", {} },\n        .{ \"freeboxos.fr\", {} },\n        .{ \"freedesktop.org\", {} },\n        .{ \"freemyip.com\", {} },\n        .{ \"*.frusky.de\", {} },\n        .{ \"wien.funkfeuer.at\", {} },\n        .{ \"daemon.asia\", {} },\n        .{ \"dix.asia\", {} },\n        .{ \"mydns.bz\", {} },\n        .{ \"0am.jp\", {} },\n        .{ \"0g0.jp\", {} },\n        .{ \"0j0.jp\", {} },\n        .{ \"0t0.jp\", {} },\n        .{ \"mydns.jp\", {} },\n        .{ \"pgw.jp\", {} },\n        .{ \"wjg.jp\", {} },\n        .{ \"keyword-on.net\", {} },\n        .{ \"live-on.net\", {} },\n        .{ \"server-on.net\", {} },\n        .{ \"mydns.tw\", {} },\n        .{ \"mydns.vc\", {} },\n        .{ \"*.futurecms.at\", {} },\n        .{ \"*.ex.futurecms.at\", {} },\n        .{ \"*.in.futurecms.at\", {} },\n        .{ \"futurehosting.at\", {} },\n        .{ \"futuremailing.at\", {} },\n        .{ \"*.ex.ortsinfo.at\", {} },\n        .{ \"*.kunden.ortsinfo.at\", {} },\n        .{ \"*.statics.cloud\", {} },\n        .{ \"gadget.app\", {} },\n        .{ \"gadget.host\", {} },\n        .{ \"aliases121.com\", {} },\n        .{ \"campaign.gov.uk\", {} },\n        .{ \"service.gov.uk\", {} },\n        .{ \"independent-commission.uk\", {} },\n        .{ \"independent-inquest.uk\", {} },\n        .{ \"independent-inquiry.uk\", {} },\n        .{ \"independent-panel.uk\", {} },\n        .{ \"independent-review.uk\", {} },\n        .{ \"public-inquiry.uk\", {} },\n        .{ \"royal-commission.uk\", {} },\n        .{ \"gehirn.ne.jp\", {} },\n        .{ \"usercontent.jp\", {} },\n        .{ \"gentapps.com\", {} },\n        .{ \"gentlentapis.com\", {} },\n        .{ \"cdn-edges.net\", {} },\n        .{ \"gsj.bz\", {} },\n        .{ \"gitbook.io\", {} },\n        .{ \"github.app\", {} },\n        .{ \"githubusercontent.com\", {} },\n        .{ \"githubpreview.dev\", {} },\n        .{ \"github.io\", {} },\n        .{ \"gitlab.io\", {} },\n        .{ \"gitapp.si\", {} },\n        .{ \"gitpage.si\", {} },\n        .{ \"nog.community\", {} },\n        .{ \"co.ro\", {} },\n        .{ \"shop.ro\", {} },\n        .{ \"lolipop.io\", {} },\n        .{ \"angry.jp\", {} },\n        .{ \"babyblue.jp\", {} },\n        .{ \"babymilk.jp\", {} },\n        .{ \"backdrop.jp\", {} },\n        .{ \"bambina.jp\", {} },\n        .{ \"bitter.jp\", {} },\n        .{ \"blush.jp\", {} },\n        .{ \"boo.jp\", {} },\n        .{ \"boy.jp\", {} },\n        .{ \"boyfriend.jp\", {} },\n        .{ \"but.jp\", {} },\n        .{ \"candypop.jp\", {} },\n        .{ \"capoo.jp\", {} },\n        .{ \"catfood.jp\", {} },\n        .{ \"cheap.jp\", {} },\n        .{ \"chicappa.jp\", {} },\n        .{ \"chillout.jp\", {} },\n        .{ \"chips.jp\", {} },\n        .{ \"chowder.jp\", {} },\n        .{ \"chu.jp\", {} },\n        .{ \"ciao.jp\", {} },\n        .{ \"cocotte.jp\", {} },\n        .{ \"coolblog.jp\", {} },\n        .{ \"cranky.jp\", {} },\n        .{ \"cutegirl.jp\", {} },\n        .{ \"daa.jp\", {} },\n        .{ \"deca.jp\", {} },\n        .{ \"deci.jp\", {} },\n        .{ \"digick.jp\", {} },\n        .{ \"egoism.jp\", {} },\n        .{ \"fakefur.jp\", {} },\n        .{ \"fem.jp\", {} },\n        .{ \"flier.jp\", {} },\n        .{ \"floppy.jp\", {} },\n        .{ \"fool.jp\", {} },\n        .{ \"frenchkiss.jp\", {} },\n        .{ \"girlfriend.jp\", {} },\n        .{ \"girly.jp\", {} },\n        .{ \"gloomy.jp\", {} },\n        .{ \"gonna.jp\", {} },\n        .{ \"greater.jp\", {} },\n        .{ \"hacca.jp\", {} },\n        .{ \"heavy.jp\", {} },\n        .{ \"her.jp\", {} },\n        .{ \"hiho.jp\", {} },\n        .{ \"hippy.jp\", {} },\n        .{ \"holy.jp\", {} },\n        .{ \"hungry.jp\", {} },\n        .{ \"icurus.jp\", {} },\n        .{ \"itigo.jp\", {} },\n        .{ \"jellybean.jp\", {} },\n        .{ \"kikirara.jp\", {} },\n        .{ \"kill.jp\", {} },\n        .{ \"kilo.jp\", {} },\n        .{ \"kuron.jp\", {} },\n        .{ \"littlestar.jp\", {} },\n        .{ \"lolipopmc.jp\", {} },\n        .{ \"lolitapunk.jp\", {} },\n        .{ \"lomo.jp\", {} },\n        .{ \"lovepop.jp\", {} },\n        .{ \"lovesick.jp\", {} },\n        .{ \"main.jp\", {} },\n        .{ \"mods.jp\", {} },\n        .{ \"mond.jp\", {} },\n        .{ \"mongolian.jp\", {} },\n        .{ \"moo.jp\", {} },\n        .{ \"namaste.jp\", {} },\n        .{ \"nikita.jp\", {} },\n        .{ \"nobushi.jp\", {} },\n        .{ \"noor.jp\", {} },\n        .{ \"oops.jp\", {} },\n        .{ \"parallel.jp\", {} },\n        .{ \"parasite.jp\", {} },\n        .{ \"pecori.jp\", {} },\n        .{ \"peewee.jp\", {} },\n        .{ \"penne.jp\", {} },\n        .{ \"pepper.jp\", {} },\n        .{ \"perma.jp\", {} },\n        .{ \"pigboat.jp\", {} },\n        .{ \"pinoko.jp\", {} },\n        .{ \"punyu.jp\", {} },\n        .{ \"pupu.jp\", {} },\n        .{ \"pussycat.jp\", {} },\n        .{ \"pya.jp\", {} },\n        .{ \"raindrop.jp\", {} },\n        .{ \"readymade.jp\", {} },\n        .{ \"sadist.jp\", {} },\n        .{ \"schoolbus.jp\", {} },\n        .{ \"secret.jp\", {} },\n        .{ \"staba.jp\", {} },\n        .{ \"stripper.jp\", {} },\n        .{ \"sub.jp\", {} },\n        .{ \"sunnyday.jp\", {} },\n        .{ \"thick.jp\", {} },\n        .{ \"tonkotsu.jp\", {} },\n        .{ \"under.jp\", {} },\n        .{ \"upper.jp\", {} },\n        .{ \"velvet.jp\", {} },\n        .{ \"verse.jp\", {} },\n        .{ \"versus.jp\", {} },\n        .{ \"vivian.jp\", {} },\n        .{ \"watson.jp\", {} },\n        .{ \"weblike.jp\", {} },\n        .{ \"whitesnow.jp\", {} },\n        .{ \"zombie.jp\", {} },\n        .{ \"heteml.net\", {} },\n        .{ \"graphic.design\", {} },\n        .{ \"goip.de\", {} },\n        .{ \"*.hosted.app\", {} },\n        .{ \"*.run.app\", {} },\n        .{ \"*.mtls.run.app\", {} },\n        .{ \"web.app\", {} },\n        .{ \"*.0emm.com\", {} },\n        .{ \"appspot.com\", {} },\n        .{ \"*.r.appspot.com\", {} },\n        .{ \"blogspot.com\", {} },\n        .{ \"codespot.com\", {} },\n        .{ \"googleapis.com\", {} },\n        .{ \"googlecode.com\", {} },\n        .{ \"pagespeedmobilizer.com\", {} },\n        .{ \"withgoogle.com\", {} },\n        .{ \"withyoutube.com\", {} },\n        .{ \"*.gateway.dev\", {} },\n        .{ \"cloud.goog\", {} },\n        .{ \"translate.goog\", {} },\n        .{ \"*.usercontent.goog\", {} },\n        .{ \"cloudfunctions.net\", {} },\n        .{ \"goupile.fr\", {} },\n        .{ \"pymnt.uk\", {} },\n        .{ \"cloudapps.digital\", {} },\n        .{ \"london.cloudapps.digital\", {} },\n        .{ \"gov.nl\", {} },\n        .{ \"grafana-dev.net\", {} },\n        .{ \"grayjayleagues.com\", {} },\n        .{ \"grebedoc.dev\", {} },\n        .{ \"günstigbestellen.de\", {} },\n        .{ \"günstigliefern.de\", {} },\n        .{ \"gv.uy\", {} },\n        .{ \"hackclub.app\", {} },\n        .{ \"häkkinen.fi\", {} },\n        .{ \"hashbang.sh\", {} },\n        .{ \"hasura.app\", {} },\n        .{ \"hasura-app.io\", {} },\n        .{ \"hatenablog.com\", {} },\n        .{ \"hatenadiary.com\", {} },\n        .{ \"hateblo.jp\", {} },\n        .{ \"hatenablog.jp\", {} },\n        .{ \"hatenadiary.jp\", {} },\n        .{ \"hatenadiary.org\", {} },\n        .{ \"pages.it.hs-heilbronn.de\", {} },\n        .{ \"pages-research.it.hs-heilbronn.de\", {} },\n        .{ \"heiyu.space\", {} },\n        .{ \"helioho.st\", {} },\n        .{ \"heliohost.us\", {} },\n        .{ \"hepforge.org\", {} },\n        .{ \"onhercules.app\", {} },\n        .{ \"hercules-app.com\", {} },\n        .{ \"hercules-dev.com\", {} },\n        .{ \"herokuapp.com\", {} },\n        .{ \"heyflow.page\", {} },\n        .{ \"heyflow.site\", {} },\n        .{ \"ravendb.cloud\", {} },\n        .{ \"ravendb.community\", {} },\n        .{ \"development.run\", {} },\n        .{ \"ravendb.run\", {} },\n        .{ \"hidns.co\", {} },\n        .{ \"hidns.vip\", {} },\n        .{ \"homesklep.pl\", {} },\n        .{ \"*.kin.one\", {} },\n        .{ \"*.id.pub\", {} },\n        .{ \"*.kin.pub\", {} },\n        .{ \"hoplix.shop\", {} },\n        .{ \"orx.biz\", {} },\n        .{ \"biz.ng\", {} },\n        .{ \"co.biz.ng\", {} },\n        .{ \"dl.biz.ng\", {} },\n        .{ \"go.biz.ng\", {} },\n        .{ \"lg.biz.ng\", {} },\n        .{ \"on.biz.ng\", {} },\n        .{ \"col.ng\", {} },\n        .{ \"firm.ng\", {} },\n        .{ \"gen.ng\", {} },\n        .{ \"ltd.ng\", {} },\n        .{ \"ngo.ng\", {} },\n        .{ \"plc.ng\", {} },\n        .{ \"hostyhosting.io\", {} },\n        .{ \"hf.space\", {} },\n        .{ \"static.hf.space\", {} },\n        .{ \"hypernode.io\", {} },\n        .{ \"iobb.net\", {} },\n        .{ \"co.cz\", {} },\n        .{ \"*.moonscale.io\", {} },\n        .{ \"moonscale.net\", {} },\n        .{ \"gr.com\", {} },\n        .{ \"iki.fi\", {} },\n        .{ \"ibxos.it\", {} },\n        .{ \"iliadboxos.it\", {} },\n        .{ \"imagine-proxy.work\", {} },\n        .{ \"smushcdn.com\", {} },\n        .{ \"wphostedmail.com\", {} },\n        .{ \"wpmucdn.com\", {} },\n        .{ \"tempurl.host\", {} },\n        .{ \"wpmudev.host\", {} },\n        .{ \"dyn-berlin.de\", {} },\n        .{ \"in-berlin.de\", {} },\n        .{ \"in-brb.de\", {} },\n        .{ \"in-butter.de\", {} },\n        .{ \"in-dsl.de\", {} },\n        .{ \"in-vpn.de\", {} },\n        .{ \"in-dsl.net\", {} },\n        .{ \"in-vpn.net\", {} },\n        .{ \"in-dsl.org\", {} },\n        .{ \"in-vpn.org\", {} },\n        .{ \"oninferno.net\", {} },\n        .{ \"biz.at\", {} },\n        .{ \"info.at\", {} },\n        .{ \"info.cx\", {} },\n        .{ \"ac.leg.br\", {} },\n        .{ \"al.leg.br\", {} },\n        .{ \"am.leg.br\", {} },\n        .{ \"ap.leg.br\", {} },\n        .{ \"ba.leg.br\", {} },\n        .{ \"ce.leg.br\", {} },\n        .{ \"df.leg.br\", {} },\n        .{ \"es.leg.br\", {} },\n        .{ \"go.leg.br\", {} },\n        .{ \"ma.leg.br\", {} },\n        .{ \"mg.leg.br\", {} },\n        .{ \"ms.leg.br\", {} },\n        .{ \"mt.leg.br\", {} },\n        .{ \"pa.leg.br\", {} },\n        .{ \"pb.leg.br\", {} },\n        .{ \"pe.leg.br\", {} },\n        .{ \"pi.leg.br\", {} },\n        .{ \"pr.leg.br\", {} },\n        .{ \"rj.leg.br\", {} },\n        .{ \"rn.leg.br\", {} },\n        .{ \"ro.leg.br\", {} },\n        .{ \"rr.leg.br\", {} },\n        .{ \"rs.leg.br\", {} },\n        .{ \"sc.leg.br\", {} },\n        .{ \"se.leg.br\", {} },\n        .{ \"sp.leg.br\", {} },\n        .{ \"to.leg.br\", {} },\n        .{ \"pixolino.com\", {} },\n        .{ \"na4u.ru\", {} },\n        .{ \"botdash.app\", {} },\n        .{ \"botdash.dev\", {} },\n        .{ \"botdash.gg\", {} },\n        .{ \"botdash.net\", {} },\n        .{ \"botda.sh\", {} },\n        .{ \"botdash.xyz\", {} },\n        .{ \"apps-1and1.com\", {} },\n        .{ \"live-website.com\", {} },\n        .{ \"webspace-host.com\", {} },\n        .{ \"apps-1and1.net\", {} },\n        .{ \"websitebuilder.online\", {} },\n        .{ \"app-ionos.space\", {} },\n        .{ \"iopsys.se\", {} },\n        .{ \"*.inbrowser.dev\", {} },\n        .{ \"*.dweb.link\", {} },\n        .{ \"*.inbrowser.link\", {} },\n        .{ \"ipifony.net\", {} },\n        .{ \"ir.md\", {} },\n        .{ \"is-a-good.dev\", {} },\n        .{ \"iservschule.de\", {} },\n        .{ \"mein-iserv.de\", {} },\n        .{ \"schuldock.de\", {} },\n        .{ \"schulplattform.de\", {} },\n        .{ \"schulserver.de\", {} },\n        .{ \"test-iserv.de\", {} },\n        .{ \"iserv.dev\", {} },\n        .{ \"iserv.host\", {} },\n        .{ \"ispmanager.name\", {} },\n        .{ \"mel.cloudlets.com.au\", {} },\n        .{ \"cloud.interhostsolutions.be\", {} },\n        .{ \"alp1.ae.flow.ch\", {} },\n        .{ \"appengine.flow.ch\", {} },\n        .{ \"es-1.axarnet.cloud\", {} },\n        .{ \"diadem.cloud\", {} },\n        .{ \"vip.jelastic.cloud\", {} },\n        .{ \"jele.cloud\", {} },\n        .{ \"it1.eur.aruba.jenv-aruba.cloud\", {} },\n        .{ \"it1.jenv-aruba.cloud\", {} },\n        .{ \"keliweb.cloud\", {} },\n        .{ \"cs.keliweb.cloud\", {} },\n        .{ \"oxa.cloud\", {} },\n        .{ \"tn.oxa.cloud\", {} },\n        .{ \"uk.oxa.cloud\", {} },\n        .{ \"primetel.cloud\", {} },\n        .{ \"uk.primetel.cloud\", {} },\n        .{ \"ca.reclaim.cloud\", {} },\n        .{ \"uk.reclaim.cloud\", {} },\n        .{ \"us.reclaim.cloud\", {} },\n        .{ \"ch.trendhosting.cloud\", {} },\n        .{ \"de.trendhosting.cloud\", {} },\n        .{ \"jele.club\", {} },\n        .{ \"dopaas.com\", {} },\n        .{ \"paas.hosted-by-previder.com\", {} },\n        .{ \"rag-cloud.hosteur.com\", {} },\n        .{ \"rag-cloud-ch.hosteur.com\", {} },\n        .{ \"jcloud.ik-server.com\", {} },\n        .{ \"jcloud-ver-jpc.ik-server.com\", {} },\n        .{ \"demo.jelastic.com\", {} },\n        .{ \"paas.massivegrid.com\", {} },\n        .{ \"jed.wafaicloud.com\", {} },\n        .{ \"ryd.wafaicloud.com\", {} },\n        .{ \"j.scaleforce.com.cy\", {} },\n        .{ \"jelastic.dogado.eu\", {} },\n        .{ \"fi.cloudplatform.fi\", {} },\n        .{ \"demo.datacenter.fi\", {} },\n        .{ \"paas.datacenter.fi\", {} },\n        .{ \"jele.host\", {} },\n        .{ \"mircloud.host\", {} },\n        .{ \"paas.beebyte.io\", {} },\n        .{ \"sekd1.beebyteapp.io\", {} },\n        .{ \"jele.io\", {} },\n        .{ \"jc.neen.it\", {} },\n        .{ \"jcloud.kz\", {} },\n        .{ \"cloudjiffy.net\", {} },\n        .{ \"fra1-de.cloudjiffy.net\", {} },\n        .{ \"west1-us.cloudjiffy.net\", {} },\n        .{ \"jls-sto1.elastx.net\", {} },\n        .{ \"jls-sto2.elastx.net\", {} },\n        .{ \"jls-sto3.elastx.net\", {} },\n        .{ \"fr-1.paas.massivegrid.net\", {} },\n        .{ \"lon-1.paas.massivegrid.net\", {} },\n        .{ \"lon-2.paas.massivegrid.net\", {} },\n        .{ \"ny-1.paas.massivegrid.net\", {} },\n        .{ \"ny-2.paas.massivegrid.net\", {} },\n        .{ \"sg-1.paas.massivegrid.net\", {} },\n        .{ \"jelastic.saveincloud.net\", {} },\n        .{ \"nordeste-idc.saveincloud.net\", {} },\n        .{ \"j.scaleforce.net\", {} },\n        .{ \"sdscloud.pl\", {} },\n        .{ \"unicloud.pl\", {} },\n        .{ \"mircloud.ru\", {} },\n        .{ \"enscaled.sg\", {} },\n        .{ \"jele.site\", {} },\n        .{ \"jelastic.team\", {} },\n        .{ \"orangecloud.tn\", {} },\n        .{ \"j.layershift.co.uk\", {} },\n        .{ \"phx.enscaled.us\", {} },\n        .{ \"mircloud.us\", {} },\n        .{ \"myjino.ru\", {} },\n        .{ \"*.hosting.myjino.ru\", {} },\n        .{ \"*.landing.myjino.ru\", {} },\n        .{ \"*.spectrum.myjino.ru\", {} },\n        .{ \"*.vps.myjino.ru\", {} },\n        .{ \"jote.cloud\", {} },\n        .{ \"jotelulu.cloud\", {} },\n        .{ \"eu1-plenit.com\", {} },\n        .{ \"la1-plenit.com\", {} },\n        .{ \"us1-plenit.com\", {} },\n        .{ \"webadorsite.com\", {} },\n        .{ \"jouwweb.site\", {} },\n        .{ \"*.cns.joyent.com\", {} },\n        .{ \"*.triton.zone\", {} },\n        .{ \"js.org\", {} },\n        .{ \"kaas.gg\", {} },\n        .{ \"khplay.nl\", {} },\n        .{ \"kapsi.fi\", {} },\n        .{ \"ezproxy.kuleuven.be\", {} },\n        .{ \"kuleuven.cloud\", {} },\n        .{ \"ae.kg\", {} },\n        .{ \"keymachine.de\", {} },\n        .{ \"kiloapps.ai\", {} },\n        .{ \"kiloapps.io\", {} },\n        .{ \"kinghost.net\", {} },\n        .{ \"uni5.net\", {} },\n        .{ \"knightpoint.systems\", {} },\n        .{ \"koobin.events\", {} },\n        .{ \"webthings.io\", {} },\n        .{ \"krellian.net\", {} },\n        .{ \"oya.to\", {} },\n        .{ \"co.de\", {} },\n        .{ \"shiptoday.app\", {} },\n        .{ \"shiptoday.build\", {} },\n        .{ \"laravel.cloud\", {} },\n        .{ \"on-forge.com\", {} },\n        .{ \"on-vapor.com\", {} },\n        .{ \"git-repos.de\", {} },\n        .{ \"lcube-server.de\", {} },\n        .{ \"svn-repos.de\", {} },\n        .{ \"leadpages.co\", {} },\n        .{ \"lpages.co\", {} },\n        .{ \"lpusercontent.com\", {} },\n        .{ \"leapcell.app\", {} },\n        .{ \"leapcell.dev\", {} },\n        .{ \"leapcell.online\", {} },\n        .{ \"liara.run\", {} },\n        .{ \"iran.liara.run\", {} },\n        .{ \"libp2p.direct\", {} },\n        .{ \"runcontainers.dev\", {} },\n        .{ \"co.business\", {} },\n        .{ \"co.education\", {} },\n        .{ \"co.events\", {} },\n        .{ \"co.financial\", {} },\n        .{ \"co.network\", {} },\n        .{ \"co.place\", {} },\n        .{ \"co.technology\", {} },\n        .{ \"linkyard-cloud.ch\", {} },\n        .{ \"linkyard.cloud\", {} },\n        .{ \"members.linode.com\", {} },\n        .{ \"*.nodebalancer.linode.com\", {} },\n        .{ \"*.linodeobjects.com\", {} },\n        .{ \"ip.linodeusercontent.com\", {} },\n        .{ \"we.bs\", {} },\n        .{ \"filegear-sg.me\", {} },\n        .{ \"ggff.net\", {} },\n        .{ \"*.user.localcert.dev\", {} },\n        .{ \"localtonet.com\", {} },\n        .{ \"*.localto.net\", {} },\n        .{ \"lodz.pl\", {} },\n        .{ \"pabianice.pl\", {} },\n        .{ \"plock.pl\", {} },\n        .{ \"sieradz.pl\", {} },\n        .{ \"skierniewice.pl\", {} },\n        .{ \"zgierz.pl\", {} },\n        .{ \"loginline.app\", {} },\n        .{ \"loginline.dev\", {} },\n        .{ \"loginline.io\", {} },\n        .{ \"loginline.services\", {} },\n        .{ \"loginline.site\", {} },\n        .{ \"lohmus.me\", {} },\n        .{ \"lovable.app\", {} },\n        .{ \"lovableproject.com\", {} },\n        .{ \"lovable.run\", {} },\n        .{ \"lovable.sh\", {} },\n        .{ \"krasnik.pl\", {} },\n        .{ \"leczna.pl\", {} },\n        .{ \"lubartow.pl\", {} },\n        .{ \"lublin.pl\", {} },\n        .{ \"poniatowa.pl\", {} },\n        .{ \"swidnik.pl\", {} },\n        .{ \"glug.org.uk\", {} },\n        .{ \"lug.org.uk\", {} },\n        .{ \"lugs.org.uk\", {} },\n        .{ \"barsy.bg\", {} },\n        .{ \"barsy.club\", {} },\n        .{ \"barsycenter.com\", {} },\n        .{ \"barsyonline.com\", {} },\n        .{ \"barsy.de\", {} },\n        .{ \"barsy.dev\", {} },\n        .{ \"barsy.eu\", {} },\n        .{ \"barsy.gr\", {} },\n        .{ \"barsy.in\", {} },\n        .{ \"barsy.info\", {} },\n        .{ \"barsy.io\", {} },\n        .{ \"barsy.me\", {} },\n        .{ \"barsy.menu\", {} },\n        .{ \"barsyonline.menu\", {} },\n        .{ \"barsy.mobi\", {} },\n        .{ \"barsy.net\", {} },\n        .{ \"barsy.online\", {} },\n        .{ \"barsy.org\", {} },\n        .{ \"barsy.pro\", {} },\n        .{ \"barsy.pub\", {} },\n        .{ \"barsy.ro\", {} },\n        .{ \"barsy.rs\", {} },\n        .{ \"barsy.shop\", {} },\n        .{ \"barsyonline.shop\", {} },\n        .{ \"barsy.site\", {} },\n        .{ \"barsy.store\", {} },\n        .{ \"barsy.support\", {} },\n        .{ \"barsy.uk\", {} },\n        .{ \"barsy.co.uk\", {} },\n        .{ \"barsyonline.co.uk\", {} },\n        .{ \"*.lutrausercontent.com\", {} },\n        .{ \"luyani.app\", {} },\n        .{ \"luyani.net\", {} },\n        .{ \"*.magentosite.cloud\", {} },\n        .{ \"magicpatterns.app\", {} },\n        .{ \"magicpatternsapp.com\", {} },\n        .{ \"hb.cldmail.ru\", {} },\n        .{ \"matlab.cloud\", {} },\n        .{ \"modelscape.com\", {} },\n        .{ \"mwcloudnonprod.com\", {} },\n        .{ \"polyspace.com\", {} },\n        .{ \"mayfirst.info\", {} },\n        .{ \"mayfirst.org\", {} },\n        .{ \"mazeplay.com\", {} },\n        .{ \"mcdir.me\", {} },\n        .{ \"mcdir.ru\", {} },\n        .{ \"vps.mcdir.ru\", {} },\n        .{ \"mcpre.ru\", {} },\n        .{ \"mediatech.by\", {} },\n        .{ \"mediatech.dev\", {} },\n        .{ \"hra.health\", {} },\n        .{ \"medusajs.app\", {} },\n        .{ \"miniserver.com\", {} },\n        .{ \"memset.net\", {} },\n        .{ \"messerli.app\", {} },\n        .{ \"atmeta.com\", {} },\n        .{ \"apps.fbsbx.com\", {} },\n        .{ \"*.cloud.metacentrum.cz\", {} },\n        .{ \"custom.metacentrum.cz\", {} },\n        .{ \"flt.cloud.muni.cz\", {} },\n        .{ \"usr.cloud.muni.cz\", {} },\n        .{ \"meteorapp.com\", {} },\n        .{ \"eu.meteorapp.com\", {} },\n        .{ \"co.pl\", {} },\n        .{ \"*.azurecontainer.io\", {} },\n        .{ \"azure-api.net\", {} },\n        .{ \"azure-mobile.net\", {} },\n        .{ \"azureedge.net\", {} },\n        .{ \"azurefd.net\", {} },\n        .{ \"azurestaticapps.net\", {} },\n        .{ \"1.azurestaticapps.net\", {} },\n        .{ \"2.azurestaticapps.net\", {} },\n        .{ \"3.azurestaticapps.net\", {} },\n        .{ \"4.azurestaticapps.net\", {} },\n        .{ \"5.azurestaticapps.net\", {} },\n        .{ \"6.azurestaticapps.net\", {} },\n        .{ \"7.azurestaticapps.net\", {} },\n        .{ \"centralus.azurestaticapps.net\", {} },\n        .{ \"eastasia.azurestaticapps.net\", {} },\n        .{ \"eastus2.azurestaticapps.net\", {} },\n        .{ \"westeurope.azurestaticapps.net\", {} },\n        .{ \"westus2.azurestaticapps.net\", {} },\n        .{ \"azurewebsites.net\", {} },\n        .{ \"cloudapp.net\", {} },\n        .{ \"trafficmanager.net\", {} },\n        .{ \"servicebus.usgovcloudapi.net\", {} },\n        .{ \"usgovcloudapp.net\", {} },\n        .{ \"blob.core.windows.net\", {} },\n        .{ \"servicebus.windows.net\", {} },\n        .{ \"azure-api.us\", {} },\n        .{ \"azurewebsites.us\", {} },\n        .{ \"routingthecloud.com\", {} },\n        .{ \"sn.mynetname.net\", {} },\n        .{ \"routingthecloud.net\", {} },\n        .{ \"routingthecloud.org\", {} },\n        .{ \"same-app.com\", {} },\n        .{ \"same-preview.com\", {} },\n        .{ \"csx.cc\", {} },\n        .{ \"miren.app\", {} },\n        .{ \"miren.systems\", {} },\n        .{ \"mydbserver.com\", {} },\n        .{ \"webspaceconfig.de\", {} },\n        .{ \"mittwald.info\", {} },\n        .{ \"mittwaldserver.info\", {} },\n        .{ \"typo3server.info\", {} },\n        .{ \"project.space\", {} },\n        .{ \"mocha.app\", {} },\n        .{ \"mochausercontent.com\", {} },\n        .{ \"mocha-sandbox.dev\", {} },\n        .{ \"modx.dev\", {} },\n        .{ \"bmoattachments.org\", {} },\n        .{ \"net.ru\", {} },\n        .{ \"org.ru\", {} },\n        .{ \"pp.ru\", {} },\n        .{ \"hostedpi.com\", {} },\n        .{ \"caracal.mythic-beasts.com\", {} },\n        .{ \"customer.mythic-beasts.com\", {} },\n        .{ \"fentiger.mythic-beasts.com\", {} },\n        .{ \"lynx.mythic-beasts.com\", {} },\n        .{ \"ocelot.mythic-beasts.com\", {} },\n        .{ \"oncilla.mythic-beasts.com\", {} },\n        .{ \"onza.mythic-beasts.com\", {} },\n        .{ \"sphinx.mythic-beasts.com\", {} },\n        .{ \"vs.mythic-beasts.com\", {} },\n        .{ \"x.mythic-beasts.com\", {} },\n        .{ \"yali.mythic-beasts.com\", {} },\n        .{ \"cust.retrosnub.co.uk\", {} },\n        .{ \"ui.nabu.casa\", {} },\n        .{ \"needle.run\", {} },\n        .{ \"co.site\", {} },\n        .{ \"cloud.nospamproxy.com\", {} },\n        .{ \"o365.cloud.nospamproxy.com\", {} },\n        .{ \"netlib.re\", {} },\n        .{ \"netlify.app\", {} },\n        .{ \"4u.com\", {} },\n        .{ \"nfshost.com\", {} },\n        .{ \"ipfs.nftstorage.link\", {} },\n        .{ \"ngo.us\", {} },\n        .{ \"ngrok.app\", {} },\n        .{ \"ngrok-free.app\", {} },\n        .{ \"ngrok.dev\", {} },\n        .{ \"ngrok-free.dev\", {} },\n        .{ \"ngrok.io\", {} },\n        .{ \"ap.ngrok.io\", {} },\n        .{ \"au.ngrok.io\", {} },\n        .{ \"eu.ngrok.io\", {} },\n        .{ \"in.ngrok.io\", {} },\n        .{ \"jp.ngrok.io\", {} },\n        .{ \"sa.ngrok.io\", {} },\n        .{ \"us.ngrok.io\", {} },\n        .{ \"ngrok.pizza\", {} },\n        .{ \"ngrok.pro\", {} },\n        .{ \"torun.pl\", {} },\n        .{ \"nh-serv.co.uk\", {} },\n        .{ \"nimsite.uk\", {} },\n        .{ \"mmafan.biz\", {} },\n        .{ \"myftp.biz\", {} },\n        .{ \"no-ip.biz\", {} },\n        .{ \"no-ip.ca\", {} },\n        .{ \"fantasyleague.cc\", {} },\n        .{ \"gotdns.ch\", {} },\n        .{ \"3utilities.com\", {} },\n        .{ \"blogsyte.com\", {} },\n        .{ \"ciscofreak.com\", {} },\n        .{ \"damnserver.com\", {} },\n        .{ \"ddnsking.com\", {} },\n        .{ \"ditchyourip.com\", {} },\n        .{ \"dnsiskinky.com\", {} },\n        .{ \"dynns.com\", {} },\n        .{ \"geekgalaxy.com\", {} },\n        .{ \"health-carereform.com\", {} },\n        .{ \"homesecuritymac.com\", {} },\n        .{ \"homesecuritypc.com\", {} },\n        .{ \"myactivedirectory.com\", {} },\n        .{ \"mysecuritycamera.com\", {} },\n        .{ \"myvnc.com\", {} },\n        .{ \"net-freaks.com\", {} },\n        .{ \"onthewifi.com\", {} },\n        .{ \"point2this.com\", {} },\n        .{ \"quicksytes.com\", {} },\n        .{ \"securitytactics.com\", {} },\n        .{ \"servebeer.com\", {} },\n        .{ \"servecounterstrike.com\", {} },\n        .{ \"serveexchange.com\", {} },\n        .{ \"serveftp.com\", {} },\n        .{ \"servegame.com\", {} },\n        .{ \"servehalflife.com\", {} },\n        .{ \"servehttp.com\", {} },\n        .{ \"servehumour.com\", {} },\n        .{ \"serveirc.com\", {} },\n        .{ \"servemp3.com\", {} },\n        .{ \"servep2p.com\", {} },\n        .{ \"servepics.com\", {} },\n        .{ \"servequake.com\", {} },\n        .{ \"servesarcasm.com\", {} },\n        .{ \"stufftoread.com\", {} },\n        .{ \"unusualperson.com\", {} },\n        .{ \"workisboring.com\", {} },\n        .{ \"dvrcam.info\", {} },\n        .{ \"ilovecollege.info\", {} },\n        .{ \"no-ip.info\", {} },\n        .{ \"brasilia.me\", {} },\n        .{ \"ddns.me\", {} },\n        .{ \"dnsfor.me\", {} },\n        .{ \"hopto.me\", {} },\n        .{ \"loginto.me\", {} },\n        .{ \"noip.me\", {} },\n        .{ \"webhop.me\", {} },\n        .{ \"bounceme.net\", {} },\n        .{ \"ddns.net\", {} },\n        .{ \"eating-organic.net\", {} },\n        .{ \"mydissent.net\", {} },\n        .{ \"myeffect.net\", {} },\n        .{ \"mymediapc.net\", {} },\n        .{ \"mypsx.net\", {} },\n        .{ \"mysecuritycamera.net\", {} },\n        .{ \"nhlfan.net\", {} },\n        .{ \"no-ip.net\", {} },\n        .{ \"pgafan.net\", {} },\n        .{ \"privatizehealthinsurance.net\", {} },\n        .{ \"redirectme.net\", {} },\n        .{ \"serveblog.net\", {} },\n        .{ \"serveminecraft.net\", {} },\n        .{ \"sytes.net\", {} },\n        .{ \"cable-modem.org\", {} },\n        .{ \"collegefan.org\", {} },\n        .{ \"couchpotatofries.org\", {} },\n        .{ \"hopto.org\", {} },\n        .{ \"mlbfan.org\", {} },\n        .{ \"myftp.org\", {} },\n        .{ \"mysecuritycamera.org\", {} },\n        .{ \"nflfan.org\", {} },\n        .{ \"no-ip.org\", {} },\n        .{ \"read-books.org\", {} },\n        .{ \"ufcfan.org\", {} },\n        .{ \"zapto.org\", {} },\n        .{ \"no-ip.co.uk\", {} },\n        .{ \"golffan.us\", {} },\n        .{ \"noip.us\", {} },\n        .{ \"pointto.us\", {} },\n        .{ \"stage.nodeart.io\", {} },\n        .{ \"*.developer.app\", {} },\n        .{ \"noop.app\", {} },\n        .{ \"*.northflank.app\", {} },\n        .{ \"*.build.run\", {} },\n        .{ \"*.code.run\", {} },\n        .{ \"*.database.run\", {} },\n        .{ \"*.migration.run\", {} },\n        .{ \"noticeable.news\", {} },\n        .{ \"notion.site\", {} },\n        .{ \"dnsking.ch\", {} },\n        .{ \"mypi.co\", {} },\n        .{ \"myiphost.com\", {} },\n        .{ \"forumz.info\", {} },\n        .{ \"soundcast.me\", {} },\n        .{ \"tcp4.me\", {} },\n        .{ \"dnsup.net\", {} },\n        .{ \"hicam.net\", {} },\n        .{ \"now-dns.net\", {} },\n        .{ \"ownip.net\", {} },\n        .{ \"vpndns.net\", {} },\n        .{ \"dynserv.org\", {} },\n        .{ \"now-dns.org\", {} },\n        .{ \"x443.pw\", {} },\n        .{ \"ntdll.top\", {} },\n        .{ \"freeddns.us\", {} },\n        .{ \"nsupdate.info\", {} },\n        .{ \"nerdpol.ovh\", {} },\n        .{ \"prvcy.page\", {} },\n        .{ \"observablehq.cloud\", {} },\n        .{ \"static.observableusercontent.com\", {} },\n        .{ \"omg.lol\", {} },\n        .{ \"cloudycluster.net\", {} },\n        .{ \"omniwe.site\", {} },\n        .{ \"123webseite.at\", {} },\n        .{ \"123website.be\", {} },\n        .{ \"simplesite.com.br\", {} },\n        .{ \"123website.ch\", {} },\n        .{ \"simplesite.com\", {} },\n        .{ \"123webseite.de\", {} },\n        .{ \"123hjemmeside.dk\", {} },\n        .{ \"123miweb.es\", {} },\n        .{ \"123kotisivu.fi\", {} },\n        .{ \"123siteweb.fr\", {} },\n        .{ \"simplesite.gr\", {} },\n        .{ \"123homepage.it\", {} },\n        .{ \"123website.lu\", {} },\n        .{ \"123website.nl\", {} },\n        .{ \"123hjemmeside.no\", {} },\n        .{ \"service.one\", {} },\n        .{ \"website.one\", {} },\n        .{ \"simplesite.pl\", {} },\n        .{ \"123paginaweb.pt\", {} },\n        .{ \"123minsida.se\", {} },\n        .{ \"onid.ca\", {} },\n        .{ \"is-a-fullstack.dev\", {} },\n        .{ \"is-cool.dev\", {} },\n        .{ \"is-not-a.dev\", {} },\n        .{ \"localplayer.dev\", {} },\n        .{ \"is-local.org\", {} },\n        .{ \"opensocial.site\", {} },\n        .{ \"*.oaiusercontent.com\", {} },\n        .{ \"opencraft.hosting\", {} },\n        .{ \"16-b.it\", {} },\n        .{ \"32-b.it\", {} },\n        .{ \"64-b.it\", {} },\n        .{ \"orsites.com\", {} },\n        .{ \"operaunite.com\", {} },\n        .{ \"*.customer-oci.com\", {} },\n        .{ \"*.oci.customer-oci.com\", {} },\n        .{ \"*.ocp.customer-oci.com\", {} },\n        .{ \"*.ocs.customer-oci.com\", {} },\n        .{ \"*.oraclecloudapps.com\", {} },\n        .{ \"*.oraclegovcloudapps.com\", {} },\n        .{ \"*.oraclegovcloudapps.uk\", {} },\n        .{ \"tech.orange\", {} },\n        .{ \"can.re\", {} },\n        .{ \"authgear-staging.com\", {} },\n        .{ \"authgearapps.com\", {} },\n        .{ \"outsystemscloud.com\", {} },\n        .{ \"*.hosting.ovh.net\", {} },\n        .{ \"*.webpaas.ovh.net\", {} },\n        .{ \"ownprovider.com\", {} },\n        .{ \"own.pm\", {} },\n        .{ \"*.owo.codes\", {} },\n        .{ \"ox.rs\", {} },\n        .{ \"oy.lc\", {} },\n        .{ \"pgfog.com\", {} },\n        .{ \"pagexl.com\", {} },\n        .{ \"gotpantheon.com\", {} },\n        .{ \"pantheonsite.io\", {} },\n        .{ \"*.paywhirl.com\", {} },\n        .{ \"*.xmit.co\", {} },\n        .{ \"xmit.dev\", {} },\n        .{ \"madethis.site\", {} },\n        .{ \"srv.us\", {} },\n        .{ \"gh.srv.us\", {} },\n        .{ \"gl.srv.us\", {} },\n        .{ \"mypep.link\", {} },\n        .{ \"perspecta.cloud\", {} },\n        .{ \"forgeblocks.com\", {} },\n        .{ \"id.forgerock.io\", {} },\n        .{ \"support.site\", {} },\n        .{ \"on-web.fr\", {} },\n        .{ \"*.upsun.app\", {} },\n        .{ \"upsunapp.com\", {} },\n        .{ \"ent.platform.sh\", {} },\n        .{ \"eu.platform.sh\", {} },\n        .{ \"us.platform.sh\", {} },\n        .{ \"*.platformsh.site\", {} },\n        .{ \"*.tst.site\", {} },\n        .{ \"pley.games\", {} },\n        .{ \"onporter.run\", {} },\n        .{ \"co.bn\", {} },\n        .{ \"postman-echo.com\", {} },\n        .{ \"pstmn.io\", {} },\n        .{ \"mock.pstmn.io\", {} },\n        .{ \"httpbin.org\", {} },\n        .{ \"prequalifyme.today\", {} },\n        .{ \"xen.prgmr.com\", {} },\n        .{ \"priv.at\", {} },\n        .{ \"c01.kr\", {} },\n        .{ \"eliv-api.kr\", {} },\n        .{ \"eliv-cdn.kr\", {} },\n        .{ \"eliv-dns.kr\", {} },\n        .{ \"mmv.kr\", {} },\n        .{ \"vki.kr\", {} },\n        .{ \"dev.project-study.com\", {} },\n        .{ \"protonet.io\", {} },\n        .{ \"platter-app.dev\", {} },\n        .{ \"e.id\", {} },\n        .{ \"chirurgiens-dentistes-en-france.fr\", {} },\n        .{ \"byen.site\", {} },\n        .{ \"nyc.mn\", {} },\n        .{ \"*.cn.st\", {} },\n        .{ \"pubtls.org\", {} },\n        .{ \"pythonanywhere.com\", {} },\n        .{ \"eu.pythonanywhere.com\", {} },\n        .{ \"qa2.com\", {} },\n        .{ \"qcx.io\", {} },\n        .{ \"*.sys.qcx.io\", {} },\n        .{ \"myqnapcloud.cn\", {} },\n        .{ \"alpha-myqnapcloud.com\", {} },\n        .{ \"dev-myqnapcloud.com\", {} },\n        .{ \"mycloudnas.com\", {} },\n        .{ \"mynascloud.com\", {} },\n        .{ \"myqnapcloud.com\", {} },\n        .{ \"qoto.io\", {} },\n        .{ \"qualifioapp.com\", {} },\n        .{ \"ladesk.com\", {} },\n        .{ \"*.qualyhqpartner.com\", {} },\n        .{ \"*.qualyhqportal.com\", {} },\n        .{ \"qbuser.com\", {} },\n        .{ \"*.quipelements.com\", {} },\n        .{ \"vapor.cloud\", {} },\n        .{ \"vaporcloud.io\", {} },\n        .{ \"rackmaze.com\", {} },\n        .{ \"rackmaze.net\", {} },\n        .{ \"cloudsite.builders\", {} },\n        .{ \"myradweb.net\", {} },\n        .{ \"servername.us\", {} },\n        .{ \"web.in\", {} },\n        .{ \"in.net\", {} },\n        .{ \"myrdbx.io\", {} },\n        .{ \"site.rb-hosting.io\", {} },\n        .{ \"up.railway.app\", {} },\n        .{ \"*.on-rancher.cloud\", {} },\n        .{ \"*.on-k3s.io\", {} },\n        .{ \"*.on-rio.io\", {} },\n        .{ \"ravpage.co.il\", {} },\n        .{ \"readthedocs-hosted.com\", {} },\n        .{ \"readthedocs.io\", {} },\n        .{ \"rhcloud.com\", {} },\n        .{ \"instances.spawn.cc\", {} },\n        .{ \"*.clusters.rdpa.co\", {} },\n        .{ \"*.srvrless.rdpa.co\", {} },\n        .{ \"onrender.com\", {} },\n        .{ \"app.render.com\", {} },\n        .{ \"replit.app\", {} },\n        .{ \"id.replit.app\", {} },\n        .{ \"firewalledreplit.co\", {} },\n        .{ \"id.firewalledreplit.co\", {} },\n        .{ \"repl.co\", {} },\n        .{ \"id.repl.co\", {} },\n        .{ \"replit.dev\", {} },\n        .{ \"archer.replit.dev\", {} },\n        .{ \"bones.replit.dev\", {} },\n        .{ \"canary.replit.dev\", {} },\n        .{ \"global.replit.dev\", {} },\n        .{ \"hacker.replit.dev\", {} },\n        .{ \"id.replit.dev\", {} },\n        .{ \"janeway.replit.dev\", {} },\n        .{ \"kim.replit.dev\", {} },\n        .{ \"kira.replit.dev\", {} },\n        .{ \"kirk.replit.dev\", {} },\n        .{ \"odo.replit.dev\", {} },\n        .{ \"paris.replit.dev\", {} },\n        .{ \"picard.replit.dev\", {} },\n        .{ \"pike.replit.dev\", {} },\n        .{ \"prerelease.replit.dev\", {} },\n        .{ \"reed.replit.dev\", {} },\n        .{ \"riker.replit.dev\", {} },\n        .{ \"sisko.replit.dev\", {} },\n        .{ \"spock.replit.dev\", {} },\n        .{ \"staging.replit.dev\", {} },\n        .{ \"sulu.replit.dev\", {} },\n        .{ \"tarpit.replit.dev\", {} },\n        .{ \"teams.replit.dev\", {} },\n        .{ \"tucker.replit.dev\", {} },\n        .{ \"wesley.replit.dev\", {} },\n        .{ \"worf.replit.dev\", {} },\n        .{ \"repl.run\", {} },\n        .{ \"resindevice.io\", {} },\n        .{ \"devices.resinstaging.io\", {} },\n        .{ \"hzc.io\", {} },\n        .{ \"adimo.co.uk\", {} },\n        .{ \"itcouldbewor.se\", {} },\n        .{ \"aus.basketball\", {} },\n        .{ \"nz.basketball\", {} },\n        .{ \"subsc-pay.com\", {} },\n        .{ \"subsc-pay.net\", {} },\n        .{ \"git-pages.rit.edu\", {} },\n        .{ \"rocky.page\", {} },\n        .{ \"rub.de\", {} },\n        .{ \"ruhr-uni-bochum.de\", {} },\n        .{ \"io.noc.ruhr-uni-bochum.de\", {} },\n        .{ \"биз.рус\", {} },\n        .{ \"ком.рус\", {} },\n        .{ \"крым.рус\", {} },\n        .{ \"мир.рус\", {} },\n        .{ \"мск.рус\", {} },\n        .{ \"орг.рус\", {} },\n        .{ \"самара.рус\", {} },\n        .{ \"сочи.рус\", {} },\n        .{ \"спб.рус\", {} },\n        .{ \"я.рус\", {} },\n        .{ \"ras.ru\", {} },\n        .{ \"nyat.app\", {} },\n        .{ \"180r.com\", {} },\n        .{ \"dojin.com\", {} },\n        .{ \"sakuratan.com\", {} },\n        .{ \"sakuraweb.com\", {} },\n        .{ \"x0.com\", {} },\n        .{ \"2-d.jp\", {} },\n        .{ \"bona.jp\", {} },\n        .{ \"crap.jp\", {} },\n        .{ \"daynight.jp\", {} },\n        .{ \"eek.jp\", {} },\n        .{ \"flop.jp\", {} },\n        .{ \"halfmoon.jp\", {} },\n        .{ \"jeez.jp\", {} },\n        .{ \"matrix.jp\", {} },\n        .{ \"mimoza.jp\", {} },\n        .{ \"ivory.ne.jp\", {} },\n        .{ \"mail-box.ne.jp\", {} },\n        .{ \"mints.ne.jp\", {} },\n        .{ \"mokuren.ne.jp\", {} },\n        .{ \"opal.ne.jp\", {} },\n        .{ \"sakura.ne.jp\", {} },\n        .{ \"sumomo.ne.jp\", {} },\n        .{ \"topaz.ne.jp\", {} },\n        .{ \"netgamers.jp\", {} },\n        .{ \"nyanta.jp\", {} },\n        .{ \"o0o0.jp\", {} },\n        .{ \"rdy.jp\", {} },\n        .{ \"rgr.jp\", {} },\n        .{ \"rulez.jp\", {} },\n        .{ \"s3.isk01.sakurastorage.jp\", {} },\n        .{ \"s3.isk02.sakurastorage.jp\", {} },\n        .{ \"saloon.jp\", {} },\n        .{ \"sblo.jp\", {} },\n        .{ \"skr.jp\", {} },\n        .{ \"tank.jp\", {} },\n        .{ \"uh-oh.jp\", {} },\n        .{ \"undo.jp\", {} },\n        .{ \"rs.webaccel.jp\", {} },\n        .{ \"user.webaccel.jp\", {} },\n        .{ \"websozai.jp\", {} },\n        .{ \"xii.jp\", {} },\n        .{ \"squares.net\", {} },\n        .{ \"jpn.org\", {} },\n        .{ \"kirara.st\", {} },\n        .{ \"x0.to\", {} },\n        .{ \"from.tv\", {} },\n        .{ \"sakura.tv\", {} },\n        .{ \"*.builder.code.com\", {} },\n        .{ \"*.dev-builder.code.com\", {} },\n        .{ \"*.stg-builder.code.com\", {} },\n        .{ \"*.001.test.code-builder-stg.platform.salesforce.com\", {} },\n        .{ \"*.d.crm.dev\", {} },\n        .{ \"*.w.crm.dev\", {} },\n        .{ \"*.wa.crm.dev\", {} },\n        .{ \"*.wb.crm.dev\", {} },\n        .{ \"*.wc.crm.dev\", {} },\n        .{ \"*.wd.crm.dev\", {} },\n        .{ \"*.we.crm.dev\", {} },\n        .{ \"*.wf.crm.dev\", {} },\n        .{ \"sandcats.io\", {} },\n        .{ \"sav.case\", {} },\n        .{ \"logoip.com\", {} },\n        .{ \"logoip.de\", {} },\n        .{ \"fr-par-1.baremetal.scw.cloud\", {} },\n        .{ \"fr-par-2.baremetal.scw.cloud\", {} },\n        .{ \"nl-ams-1.baremetal.scw.cloud\", {} },\n        .{ \"cockpit.fr-par.scw.cloud\", {} },\n        .{ \"ddl.fr-par.scw.cloud\", {} },\n        .{ \"dtwh.fr-par.scw.cloud\", {} },\n        .{ \"fnc.fr-par.scw.cloud\", {} },\n        .{ \"functions.fnc.fr-par.scw.cloud\", {} },\n        .{ \"ifr.fr-par.scw.cloud\", {} },\n        .{ \"k8s.fr-par.scw.cloud\", {} },\n        .{ \"nodes.k8s.fr-par.scw.cloud\", {} },\n        .{ \"kafk.fr-par.scw.cloud\", {} },\n        .{ \"mgdb.fr-par.scw.cloud\", {} },\n        .{ \"rdb.fr-par.scw.cloud\", {} },\n        .{ \"s3.fr-par.scw.cloud\", {} },\n        .{ \"s3-website.fr-par.scw.cloud\", {} },\n        .{ \"scbl.fr-par.scw.cloud\", {} },\n        .{ \"whm.fr-par.scw.cloud\", {} },\n        .{ \"priv.instances.scw.cloud\", {} },\n        .{ \"pub.instances.scw.cloud\", {} },\n        .{ \"k8s.scw.cloud\", {} },\n        .{ \"cockpit.nl-ams.scw.cloud\", {} },\n        .{ \"ddl.nl-ams.scw.cloud\", {} },\n        .{ \"dtwh.nl-ams.scw.cloud\", {} },\n        .{ \"ifr.nl-ams.scw.cloud\", {} },\n        .{ \"k8s.nl-ams.scw.cloud\", {} },\n        .{ \"nodes.k8s.nl-ams.scw.cloud\", {} },\n        .{ \"kafk.nl-ams.scw.cloud\", {} },\n        .{ \"mgdb.nl-ams.scw.cloud\", {} },\n        .{ \"rdb.nl-ams.scw.cloud\", {} },\n        .{ \"s3.nl-ams.scw.cloud\", {} },\n        .{ \"s3-website.nl-ams.scw.cloud\", {} },\n        .{ \"scbl.nl-ams.scw.cloud\", {} },\n        .{ \"whm.nl-ams.scw.cloud\", {} },\n        .{ \"cockpit.pl-waw.scw.cloud\", {} },\n        .{ \"ddl.pl-waw.scw.cloud\", {} },\n        .{ \"dtwh.pl-waw.scw.cloud\", {} },\n        .{ \"ifr.pl-waw.scw.cloud\", {} },\n        .{ \"k8s.pl-waw.scw.cloud\", {} },\n        .{ \"nodes.k8s.pl-waw.scw.cloud\", {} },\n        .{ \"kafk.pl-waw.scw.cloud\", {} },\n        .{ \"mgdb.pl-waw.scw.cloud\", {} },\n        .{ \"rdb.pl-waw.scw.cloud\", {} },\n        .{ \"s3.pl-waw.scw.cloud\", {} },\n        .{ \"s3-website.pl-waw.scw.cloud\", {} },\n        .{ \"scbl.pl-waw.scw.cloud\", {} },\n        .{ \"scalebook.scw.cloud\", {} },\n        .{ \"smartlabeling.scw.cloud\", {} },\n        .{ \"dedibox.fr\", {} },\n        .{ \"schokokeks.net\", {} },\n        .{ \"gov.scot\", {} },\n        .{ \"service.gov.scot\", {} },\n        .{ \"scrysec.com\", {} },\n        .{ \"client.scrypted.io\", {} },\n        .{ \"firewall-gateway.com\", {} },\n        .{ \"firewall-gateway.de\", {} },\n        .{ \"my-gateway.de\", {} },\n        .{ \"my-router.de\", {} },\n        .{ \"spdns.de\", {} },\n        .{ \"spdns.eu\", {} },\n        .{ \"firewall-gateway.net\", {} },\n        .{ \"my-firewall.org\", {} },\n        .{ \"myfirewall.org\", {} },\n        .{ \"spdns.org\", {} },\n        .{ \"seidat.net\", {} },\n        .{ \"sellfy.store\", {} },\n        .{ \"minisite.ms\", {} },\n        .{ \"senseering.net\", {} },\n        .{ \"servebolt.cloud\", {} },\n        .{ \"biz.ua\", {} },\n        .{ \"co.ua\", {} },\n        .{ \"pp.ua\", {} },\n        .{ \"as.sh.cn\", {} },\n        .{ \"sheezy.games\", {} },\n        .{ \"myshopblocks.com\", {} },\n        .{ \"myshopify.com\", {} },\n        .{ \"shopitsite.com\", {} },\n        .{ \"shopware.shop\", {} },\n        .{ \"shopware.store\", {} },\n        .{ \"mo-siemens.io\", {} },\n        .{ \"1kapp.com\", {} },\n        .{ \"appchizi.com\", {} },\n        .{ \"applinzi.com\", {} },\n        .{ \"sinaapp.com\", {} },\n        .{ \"vipsinaapp.com\", {} },\n        .{ \"siteleaf.net\", {} },\n        .{ \"small-web.org\", {} },\n        .{ \"aeroport.fr\", {} },\n        .{ \"avocat.fr\", {} },\n        .{ \"chambagri.fr\", {} },\n        .{ \"chirurgiens-dentistes.fr\", {} },\n        .{ \"experts-comptables.fr\", {} },\n        .{ \"medecin.fr\", {} },\n        .{ \"notaires.fr\", {} },\n        .{ \"pharmacien.fr\", {} },\n        .{ \"port.fr\", {} },\n        .{ \"veterinaire.fr\", {} },\n        .{ \"vp4.me\", {} },\n        .{ \"*.snowflake.app\", {} },\n        .{ \"*.privatelink.snowflake.app\", {} },\n        .{ \"streamlit.app\", {} },\n        .{ \"streamlitapp.com\", {} },\n        .{ \"try-snowplow.com\", {} },\n        .{ \"mafelo.net\", {} },\n        .{ \"playstation-cloud.com\", {} },\n        .{ \"srht.site\", {} },\n        .{ \"apps.lair.io\", {} },\n        .{ \"*.stolos.io\", {} },\n        .{ \"4.at\", {} },\n        .{ \"my.at\", {} },\n        .{ \"my.de\", {} },\n        .{ \"*.nxa.eu\", {} },\n        .{ \"nx.gw\", {} },\n        .{ \"spawnbase.app\", {} },\n        .{ \"customer.speedpartner.de\", {} },\n        .{ \"myspreadshop.at\", {} },\n        .{ \"myspreadshop.com.au\", {} },\n        .{ \"myspreadshop.be\", {} },\n        .{ \"myspreadshop.ca\", {} },\n        .{ \"myspreadshop.ch\", {} },\n        .{ \"myspreadshop.com\", {} },\n        .{ \"myspreadshop.de\", {} },\n        .{ \"myspreadshop.dk\", {} },\n        .{ \"myspreadshop.es\", {} },\n        .{ \"myspreadshop.fi\", {} },\n        .{ \"myspreadshop.fr\", {} },\n        .{ \"myspreadshop.ie\", {} },\n        .{ \"myspreadshop.it\", {} },\n        .{ \"myspreadshop.net\", {} },\n        .{ \"myspreadshop.nl\", {} },\n        .{ \"myspreadshop.no\", {} },\n        .{ \"myspreadshop.pl\", {} },\n        .{ \"myspreadshop.se\", {} },\n        .{ \"myspreadshop.co.uk\", {} },\n        .{ \"w-corp-staticblitz.com\", {} },\n        .{ \"w-credentialless-staticblitz.com\", {} },\n        .{ \"w-staticblitz.com\", {} },\n        .{ \"bolt.host\", {} },\n        .{ \"stackhero-network.com\", {} },\n        .{ \"runs.onstackit.cloud\", {} },\n        .{ \"stackit.gg\", {} },\n        .{ \"stackit.rocks\", {} },\n        .{ \"stackit.run\", {} },\n        .{ \"stackit.zone\", {} },\n        .{ \"indevs.in\", {} },\n        .{ \"musician.io\", {} },\n        .{ \"novecore.site\", {} },\n        .{ \"api.stdlib.com\", {} },\n        .{ \"statichost.page\", {} },\n        .{ \"feedback.ac\", {} },\n        .{ \"forms.ac\", {} },\n        .{ \"assessments.cx\", {} },\n        .{ \"calculators.cx\", {} },\n        .{ \"funnels.cx\", {} },\n        .{ \"paynow.cx\", {} },\n        .{ \"quizzes.cx\", {} },\n        .{ \"researched.cx\", {} },\n        .{ \"tests.cx\", {} },\n        .{ \"surveys.so\", {} },\n        .{ \"ipfs.storacha.link\", {} },\n        .{ \"ipfs.w3s.link\", {} },\n        .{ \"storebase.store\", {} },\n        .{ \"storj.farm\", {} },\n        .{ \"strapiapp.com\", {} },\n        .{ \"media.strapiapp.com\", {} },\n        .{ \"vps-host.net\", {} },\n        .{ \"atl.jelastic.vps-host.net\", {} },\n        .{ \"njs.jelastic.vps-host.net\", {} },\n        .{ \"ric.jelastic.vps-host.net\", {} },\n        .{ \"streak-link.com\", {} },\n        .{ \"streaklinks.com\", {} },\n        .{ \"streakusercontent.com\", {} },\n        .{ \"soc.srcf.net\", {} },\n        .{ \"user.srcf.net\", {} },\n        .{ \"utwente.io\", {} },\n        .{ \"temp-dns.com\", {} },\n        .{ \"supabase.co\", {} },\n        .{ \"realtime.supabase.co\", {} },\n        .{ \"storage.supabase.co\", {} },\n        .{ \"supabase.in\", {} },\n        .{ \"supabase.net\", {} },\n        .{ \"syncloud.it\", {} },\n        .{ \"dscloud.biz\", {} },\n        .{ \"direct.quickconnect.cn\", {} },\n        .{ \"dsmynas.com\", {} },\n        .{ \"familyds.com\", {} },\n        .{ \"diskstation.me\", {} },\n        .{ \"dscloud.me\", {} },\n        .{ \"i234.me\", {} },\n        .{ \"myds.me\", {} },\n        .{ \"synology.me\", {} },\n        .{ \"dscloud.mobi\", {} },\n        .{ \"dsmynas.net\", {} },\n        .{ \"familyds.net\", {} },\n        .{ \"dsmynas.org\", {} },\n        .{ \"familyds.org\", {} },\n        .{ \"direct.quickconnect.to\", {} },\n        .{ \"vpnplus.to\", {} },\n        .{ \"mytabit.com\", {} },\n        .{ \"mytabit.co.il\", {} },\n        .{ \"tabitorder.co.il\", {} },\n        .{ \"taifun-dns.de\", {} },\n        .{ \"erp.dev\", {} },\n        .{ \"web.erp.dev\", {} },\n        .{ \"ts.net\", {} },\n        .{ \"*.c.ts.net\", {} },\n        .{ \"gda.pl\", {} },\n        .{ \"gdansk.pl\", {} },\n        .{ \"gdynia.pl\", {} },\n        .{ \"med.pl\", {} },\n        .{ \"sopot.pl\", {} },\n        .{ \"taveusercontent.com\", {} },\n        .{ \"p.tawk.email\", {} },\n        .{ \"p.tawkto.email\", {} },\n        .{ \"tche.br\", {} },\n        .{ \"site.tb-hosting.com\", {} },\n        .{ \"directwp.eu\", {} },\n        .{ \"ec.cc\", {} },\n        .{ \"eu.cc\", {} },\n        .{ \"gu.cc\", {} },\n        .{ \"uk.cc\", {} },\n        .{ \"us.cc\", {} },\n        .{ \"edugit.io\", {} },\n        .{ \"s3.teckids.org\", {} },\n        .{ \"telebit.app\", {} },\n        .{ \"telebit.io\", {} },\n        .{ \"*.telebit.xyz\", {} },\n        .{ \"teleport.sh\", {} },\n        .{ \"*.firenet.ch\", {} },\n        .{ \"*.svc.firenet.ch\", {} },\n        .{ \"reservd.com\", {} },\n        .{ \"thingdustdata.com\", {} },\n        .{ \"cust.dev.thingdust.io\", {} },\n        .{ \"reservd.dev.thingdust.io\", {} },\n        .{ \"cust.disrec.thingdust.io\", {} },\n        .{ \"reservd.disrec.thingdust.io\", {} },\n        .{ \"cust.prod.thingdust.io\", {} },\n        .{ \"cust.testing.thingdust.io\", {} },\n        .{ \"reservd.testing.thingdust.io\", {} },\n        .{ \"tickets.io\", {} },\n        .{ \"arvo.network\", {} },\n        .{ \"azimuth.network\", {} },\n        .{ \"tlon.network\", {} },\n        .{ \"torproject.net\", {} },\n        .{ \"pages.torproject.net\", {} },\n        .{ \"townnews-staging.com\", {} },\n        .{ \"12hp.at\", {} },\n        .{ \"2ix.at\", {} },\n        .{ \"4lima.at\", {} },\n        .{ \"lima-city.at\", {} },\n        .{ \"12hp.ch\", {} },\n        .{ \"2ix.ch\", {} },\n        .{ \"4lima.ch\", {} },\n        .{ \"lima-city.ch\", {} },\n        .{ \"trafficplex.cloud\", {} },\n        .{ \"de.cool\", {} },\n        .{ \"12hp.de\", {} },\n        .{ \"2ix.de\", {} },\n        .{ \"4lima.de\", {} },\n        .{ \"lima-city.de\", {} },\n        .{ \"1337.pictures\", {} },\n        .{ \"clan.rip\", {} },\n        .{ \"lima-city.rocks\", {} },\n        .{ \"webspace.rocks\", {} },\n        .{ \"lima.zone\", {} },\n        .{ \"*.transurl.be\", {} },\n        .{ \"*.transurl.eu\", {} },\n        .{ \"site.transip.me\", {} },\n        .{ \"*.transurl.nl\", {} },\n        .{ \"tunnelmole.net\", {} },\n        .{ \"tuxfamily.org\", {} },\n        .{ \"typedream.app\", {} },\n        .{ \"pro.typeform.com\", {} },\n        .{ \"uber.space\", {} },\n        .{ \"hk.com\", {} },\n        .{ \"inc.hk\", {} },\n        .{ \"ltd.hk\", {} },\n        .{ \"hk.org\", {} },\n        .{ \"it.com\", {} },\n        .{ \"umso.co\", {} },\n        .{ \"unison-services.cloud\", {} },\n        .{ \"virtual-user.de\", {} },\n        .{ \"virtualuser.de\", {} },\n        .{ \"obj.ag\", {} },\n        .{ \"name.pm\", {} },\n        .{ \"sch.tf\", {} },\n        .{ \"biz.wf\", {} },\n        .{ \"sch.wf\", {} },\n        .{ \"org.yt\", {} },\n        .{ \"rs.ba\", {} },\n        .{ \"bielsko.pl\", {} },\n        .{ \"urown.cloud\", {} },\n        .{ \"dnsupdate.info\", {} },\n        .{ \"us.org\", {} },\n        .{ \"v.ua\", {} },\n        .{ \"val.run\", {} },\n        .{ \"web.val.run\", {} },\n        .{ \"vercel.app\", {} },\n        .{ \"v0.build\", {} },\n        .{ \"vercel.dev\", {} },\n        .{ \"vusercontent.net\", {} },\n        .{ \"vercel.run\", {} },\n        .{ \"now.sh\", {} },\n        .{ \"2038.io\", {} },\n        .{ \"v-info.info\", {} },\n        .{ \"vistablog.ir\", {} },\n        .{ \"deus-canvas.com\", {} },\n        .{ \"voorloper.cloud\", {} },\n        .{ \"*.vultrobjects.com\", {} },\n        .{ \"wafflecell.com\", {} },\n        .{ \"wal.app\", {} },\n        .{ \"wasmer.app\", {} },\n        .{ \"webflow.io\", {} },\n        .{ \"webflowtest.io\", {} },\n        .{ \"*.webhare.dev\", {} },\n        .{ \"bookonline.app\", {} },\n        .{ \"hotelwithflight.com\", {} },\n        .{ \"reserve-online.com\", {} },\n        .{ \"reserve-online.net\", {} },\n        .{ \"cprapid.com\", {} },\n        .{ \"pleskns.com\", {} },\n        .{ \"wp2.host\", {} },\n        .{ \"pdns.page\", {} },\n        .{ \"plesk.page\", {} },\n        .{ \"cpanel.site\", {} },\n        .{ \"wpsquared.site\", {} },\n        .{ \"*.wadl.top\", {} },\n        .{ \"remotewd.com\", {} },\n        .{ \"box.ca\", {} },\n        .{ \"pages.wiardweb.com\", {} },\n        .{ \"toolforge.org\", {} },\n        .{ \"wmcloud.org\", {} },\n        .{ \"beta.wmcloud.org\", {} },\n        .{ \"wmflabs.org\", {} },\n        .{ \"vps.hrsn.au\", {} },\n        .{ \"hrsn.dev\", {} },\n        .{ \"is-a.dev\", {} },\n        .{ \"localcert.net\", {} },\n        .{ \"windsurf.app\", {} },\n        .{ \"windsurf.build\", {} },\n        .{ \"panel.gg\", {} },\n        .{ \"daemon.panel.gg\", {} },\n        .{ \"base44.app\", {} },\n        .{ \"base44-sandbox.com\", {} },\n        .{ \"wixsite.com\", {} },\n        .{ \"wixstudio.com\", {} },\n        .{ \"editorx.io\", {} },\n        .{ \"wixstudio.io\", {} },\n        .{ \"wix.run\", {} },\n        .{ \"messwithdns.com\", {} },\n        .{ \"woltlab-demo.com\", {} },\n        .{ \"myforum.community\", {} },\n        .{ \"community-pro.de\", {} },\n        .{ \"diskussionsbereich.de\", {} },\n        .{ \"community-pro.net\", {} },\n        .{ \"meinforum.net\", {} },\n        .{ \"affinitylottery.org.uk\", {} },\n        .{ \"raffleentry.org.uk\", {} },\n        .{ \"weeklylottery.org.uk\", {} },\n        .{ \"wpenginepowered.com\", {} },\n        .{ \"js.wpenginepowered.com\", {} },\n        .{ \"*.xenonconnect.de\", {} },\n        .{ \"half.host\", {} },\n        .{ \"xnbay.com\", {} },\n        .{ \"u2.xnbay.com\", {} },\n        .{ \"u2-local.xnbay.com\", {} },\n        .{ \"cistron.nl\", {} },\n        .{ \"demon.nl\", {} },\n        .{ \"xs4all.space\", {} },\n        .{ \"xtooldevice.com\", {} },\n        .{ \"yandexcloud.net\", {} },\n        .{ \"storage.yandexcloud.net\", {} },\n        .{ \"website.yandexcloud.net\", {} },\n        .{ \"sourcecraft.site\", {} },\n        .{ \"official.academy\", {} },\n        .{ \"yolasite.com\", {} },\n        .{ \"ynh.fr\", {} },\n        .{ \"nohost.me\", {} },\n        .{ \"noho.st\", {} },\n        .{ \"za.net\", {} },\n        .{ \"za.org\", {} },\n        .{ \"zap.cloud\", {} },\n        .{ \"zeabur.app\", {} },\n        .{ \"*.zerops.app\", {} },\n        .{ \"bss.design\", {} },\n        .{ \"basicserver.io\", {} },\n        .{ \"virtualserver.io\", {} },\n        .{ \"enterprisecloud.nu\", {} },\n        .{ \"zone.id\", {} },\n        .{ \"nett.to\", {} },\n        .{ \"zabc.net\", {} },\n    };\n"
  },
  {
    "path": "src/data/public_suffix_list_gen.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc main() {\n\tresp, err := http.Get(\"https://publicsuffix.org/list/public_suffix_list.dat\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar domains []string\n\n\tscanner := bufio.NewScanner(resp.Body)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif len(line) == 0 || strings.HasPrefix(line, \"//\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tdomains = append(domains, line)\n\t}\n\n\tlookup :=\n\t\t\"const std = @import(\\\"std\\\");\\n\" +\n\t\t\t\"const builtin = @import(\\\"builtin\\\");\\n\\n\" +\n\t\t\t\"pub fn lookup(value: []const u8) bool {\\n\" +\n\t\t\t\"    return public_suffix_list.has(value);\\n\" +\n\t\t\t\"}\\n\"\n\tfmt.Println(lookup)\n\n\tfmt.Println(\"const public_suffix_list = std.StaticStringMap(void).initComptime(entries);\\n\")\n\tfmt.Println(\"const entries: []const struct { []const u8, void } =\")\n\tfmt.Println(\"    if (builtin.is_test) &.{\")\n\tfmt.Println(\"        .{ \\\"api.gov.uk\\\", {} },\")\n\tfmt.Println(\"        .{ \\\"gov.uk\\\", {} },\")\n\tfmt.Println(\"    } else &.{\")\n\tfor _, domain := range domains {\n\t\tfmt.Printf(`        .{ \"%s\", {} },`, domain)\n\t\tfmt.Println()\n\t}\n\tfmt.Println(\"    };\")\n}\n"
  },
  {
    "path": "src/datetime.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst builtin = @import(\"builtin\");\nconst posix = std.posix;\n\nconst Allocator = std.mem.Allocator;\n\npub const Date = struct {\n    year: i16,\n    month: u8,\n    day: u8,\n\n    pub const Format = enum {\n        iso8601,\n        rfc3339,\n    };\n\n    pub fn init(year: i16, month: u8, day: u8) !Date {\n        if (!Date.valid(year, month, day)) {\n            return error.InvalidDate;\n        }\n\n        return .{\n            .year = year,\n            .month = month,\n            .day = day,\n        };\n    }\n\n    pub fn valid(year: i16, month: u8, day: u8) bool {\n        if (month == 0 or month > 12) {\n            return false;\n        }\n\n        if (day == 0) {\n            return false;\n        }\n\n        const month_days = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };\n        const max_days = if (month == 2 and (@rem(year, 400) == 0 or (@rem(year, 100) != 0 and @rem(year, 4) == 0))) 29 else month_days[month - 1];\n        if (day > max_days) {\n            return false;\n        }\n\n        return true;\n    }\n\n    pub fn parse(input: []const u8, fmt: Format) !Date {\n        var parser = Parser.init(input);\n\n        const date = switch (fmt) {\n            .rfc3339 => try parser.rfc3339Date(),\n            .iso8601 => try parser.iso8601Date(),\n        };\n\n        if (parser.unconsumed() != 0) {\n            return error.InvalidDate;\n        }\n\n        return date;\n    }\n\n    pub fn order(a: Date, b: Date) std.math.Order {\n        const year_order = std.math.order(a.year, b.year);\n        if (year_order != .eq) return year_order;\n\n        const month_order = std.math.order(a.month, b.month);\n        if (month_order != .eq) return month_order;\n\n        return std.math.order(a.day, b.day);\n    }\n\n    pub fn format(self: Date, writer: *std.Io.Writer) !void {\n        var buf: [11]u8 = undefined;\n        const n = writeDate(&buf, self);\n        try writer.writeAll(buf[0..n]);\n    }\n\n    pub fn jsonStringify(self: Date, out: anytype) !void {\n        // Our goal here isn't to validate the date. It's to write what we have\n        // in a YYYY-MM-DD format. If the data in Date isn't valid, that's not\n        // our problem and we don't guarantee any reasonable output in such cases.\n\n        // std.fmt.formatInt is difficult to work with. The padding with signs\n        // doesn't work and it'll always put a + sign given a signed integer with padding\n        // So, for year, we always feed it an unsigned number (which avoids both issues)\n        // and prepend the - if we need it.s\n        var buf: [13]u8 = undefined;\n        const n = writeDate(buf[1..12], self);\n        buf[0] = '\"';\n        buf[n + 1] = '\"';\n        try out.print(\"{s}\", .{buf[0 .. n + 2]});\n    }\n\n    pub fn jsonParse(allocator: Allocator, source: anytype, options: anytype) !Date {\n        _ = options;\n\n        switch (try source.nextAlloc(allocator, .alloc_if_needed)) {\n            inline .string, .allocated_string => |str| return Date.parse(str, .rfc3339) catch return error.InvalidCharacter,\n            else => return error.UnexpectedToken,\n        }\n    }\n};\n\npub const Time = struct {\n    hour: u8,\n    min: u8,\n    sec: u8,\n    micros: u32,\n\n    pub const Format = enum {\n        rfc3339,\n    };\n\n    pub fn init(hour: u8, min: u8, sec: u8, micros: u32) !Time {\n        if (!Time.valid(hour, min, sec, micros)) {\n            return error.InvalidTime;\n        }\n\n        return .{\n            .hour = hour,\n            .min = min,\n            .sec = sec,\n            .micros = micros,\n        };\n    }\n\n    pub fn valid(hour: u8, min: u8, sec: u8, micros: u32) bool {\n        if (hour > 23) {\n            return false;\n        }\n\n        if (min > 59) {\n            return false;\n        }\n\n        if (sec > 59) {\n            return false;\n        }\n\n        if (micros > 999999) {\n            return false;\n        }\n\n        return true;\n    }\n\n    pub fn parse(input: []const u8, fmt: Format) !Time {\n        var parser = Parser.init(input);\n        const time = switch (fmt) {\n            .rfc3339 => try parser.time(true),\n        };\n\n        if (parser.unconsumed() != 0) {\n            return error.InvalidTime;\n        }\n        return time;\n    }\n\n    pub fn order(a: Time, b: Time) std.math.Order {\n        const hour_order = std.math.order(a.hour, b.hour);\n        if (hour_order != .eq) return hour_order;\n\n        const min_order = std.math.order(a.min, b.min);\n        if (min_order != .eq) return min_order;\n\n        const sec_order = std.math.order(a.sec, b.sec);\n        if (sec_order != .eq) return sec_order;\n\n        return std.math.order(a.micros, b.micros);\n    }\n\n    pub fn format(self: Time, writer: *std.Io.Writer) !void {\n        var buf: [15]u8 = undefined;\n        const n = writeTime(&buf, self);\n        try writer.writeAll(buf[0..n]);\n    }\n\n    pub fn jsonStringify(self: Time, out: anytype) !void {\n        // Our goal here isn't to validate the time. It's to write what we have\n        // in a hh:mm:ss.sss format. If the data in Time isn't valid, that's not\n        // our problem and we don't guarantee any reasonable output in such cases.\n        var buf: [17]u8 = undefined;\n        const n = writeTime(buf[1..16], self);\n        buf[0] = '\"';\n        buf[n + 1] = '\"';\n        try out.print(\"{s}\", .{buf[0 .. n + 2]});\n    }\n\n    pub fn jsonParse(allocator: Allocator, source: anytype, options: anytype) !Time {\n        _ = options;\n\n        switch (try source.nextAlloc(allocator, .alloc_if_needed)) {\n            inline .string, .allocated_string => |str| return Time.parse(str, .rfc3339) catch return error.InvalidCharacter,\n            else => return error.UnexpectedToken,\n        }\n    }\n};\n\npub const DateTime = struct {\n    micros: i64,\n\n    const MICROSECONDS_IN_A_DAY = 86_400_000_000;\n    const MICROSECONDS_IN_AN_HOUR = 3_600_000_000;\n    const MICROSECONDS_IN_A_MIN = 60_000_000;\n    const MICROSECONDS_IN_A_SEC = 1_000_000;\n\n    pub const Format = enum {\n        rfc822,\n        rfc3339,\n    };\n\n    pub const TimestampPrecision = enum {\n        seconds,\n        milliseconds,\n        microseconds,\n    };\n\n    pub const TimeUnit = enum {\n        days,\n        hours,\n        minutes,\n        seconds,\n        milliseconds,\n        microseconds,\n    };\n\n    // https://blog.reverberate.org/2020/05/12/optimizing-date-algorithms.html\n    pub fn initUTC(year: i16, month: u8, day: u8, hour: u8, min: u8, sec: u8, micros: u32) !DateTime {\n        if (Date.valid(year, month, day) == false) {\n            return error.InvalidDate;\n        }\n\n        if (Time.valid(hour, min, sec, micros) == false) {\n            return error.InvalidTime;\n        }\n\n        const year_base = 4800;\n        const month_adj = @as(i32, @intCast(month)) - 3; // March-based month\n        const carry: u8 = if (month_adj < 0) 1 else 0;\n        const adjust: u8 = if (carry == 1) 12 else 0;\n        const year_adj: i64 = year + year_base - carry;\n        const month_days = @divTrunc(((month_adj + adjust) * 62719 + 769), 2048);\n        const leap_days = @divTrunc(year_adj, 4) - @divTrunc(year_adj, 100) + @divTrunc(year_adj, 400);\n\n        const date_micros: i64 = (year_adj * 365 + leap_days + month_days + (day - 1) - 2472632) * MICROSECONDS_IN_A_DAY;\n        const time_micros = (@as(i64, @intCast(hour)) * MICROSECONDS_IN_AN_HOUR) + (@as(i64, @intCast(min)) * MICROSECONDS_IN_A_MIN) + (@as(i64, @intCast(sec)) * MICROSECONDS_IN_A_SEC) + micros;\n\n        return fromUnix(date_micros + time_micros, .microseconds);\n    }\n\n    pub fn fromUnix(value: i64, precision: TimestampPrecision) !DateTime {\n        switch (precision) {\n            .seconds => {\n                if (value < -210863520000 or value > 253402300799) {\n                    return error.OutsideJulianPeriod;\n                }\n                return .{ .micros = value * 1_000_000 };\n            },\n            .milliseconds => {\n                if (value < -210863520000000 or value > 253402300799999) {\n                    return error.OutsideJulianPeriod;\n                }\n                return .{ .micros = value * 1_000 };\n            },\n            .microseconds => {\n                if (value < -210863520000000000 or value > 253402300799999999) {\n                    return error.OutsideJulianPeriod;\n                }\n                return .{ .micros = value };\n            },\n        }\n    }\n\n    pub fn now() DateTime {\n        return .{\n            .micros = std.time.microTimestamp(),\n        };\n    }\n\n    pub fn parse(input: []const u8, fmt: Format) !DateTime {\n        switch (fmt) {\n            .rfc822 => return parseRFC822(input),\n            .rfc3339 => return parseRFC3339(input),\n        }\n    }\n\n    pub fn parseRFC822(input: []const u8) !DateTime {\n        if (input.len < 10) {\n            return error.InvalidDateTime;\n        }\n        var parser = Parser.init(input);\n        if (input[3] == ',' and input[4] == ' ') {\n            _ = std.meta.stringToEnum(enum { Mon, Tue, Wed, Thu, Fri, Sat, Sun }, input[0..3]) orelse return error.InvalidDate;\n            // skip over the \"DoW, \"\n            parser.pos = 5;\n        }\n\n        const day = parser.paddedInt(u8, 2) orelse return error.InvalidDate;\n        if (parser.consumeIf(' ') == false) {\n            return error.InvalidDate;\n        }\n\n        const month = std.meta.stringToEnum(enum { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }, parser.consumeN(3) orelse return error.InvalidDate) orelse return error.InvalidDate;\n\n        if (parser.consumeIf(' ') == false) {\n            return error.InvalidDate;\n        }\n\n        const year = parser.paddedInt(i16, 4) orelse blk: {\n            const short_year = parser.paddedInt(u8, 2) orelse return error.InvalidDate;\n            break :blk if (short_year > 68) 1900 + @as(i16, short_year) else 2000 + @as(i16, short_year);\n        };\n\n        if (parser.consumeIf(' ') == false) {\n            return error.InvalidDateTime;\n        }\n        const tm = try parser.time(false);\n\n        if (parser.consumeIf(' ') == false) {\n            return error.InvalidTime;\n        }\n\n        _ = std.meta.stringToEnum(enum { UT, GMT, Z }, parser.rest()) orelse return error.UnsupportedTimeZone;\n\n        return initUTC(year, @intFromEnum(month) + 1, day, tm.hour, tm.min, tm.sec, tm.micros);\n    }\n\n    pub fn parseRFC3339(input: []const u8) !DateTime {\n        var parser = Parser.init(input);\n\n        const dt = try parser.rfc3339Date();\n\n        const year = dt.year;\n        if (year < -4712 or year > 9999) {\n            return error.OutsideJulianPeriod;\n        }\n\n        // Per the spec, it can be argued thatt 't' and even ' ' should be allowed,\n        // but certainly not encouraged.\n        if (parser.consumeIf('T') == false) {\n            return error.InvalidDateTime;\n        }\n\n        const tm = try parser.time(true);\n\n        switch (parser.unconsumed()) {\n            0 => return error.InvalidDateTime,\n            1 => if (parser.consumeIf('Z') == false) {\n                return error.InvalidDateTime;\n            },\n            6 => {\n                const suffix = parser.rest();\n                if (suffix[0] != '+' and suffix[0] != '-') {\n                    return error.InvalidDateTime;\n                }\n                if (std.mem.eql(u8, suffix[1..], \"00:00\") == false) {\n                    return error.NonUTCNotSupported;\n                }\n            },\n            else => return error.InvalidDateTime,\n        }\n\n        return initUTC(dt.year, dt.month, dt.day, tm.hour, tm.min, tm.sec, tm.micros);\n    }\n\n    pub fn add(dt: DateTime, value: i64, unit: TimeUnit) !DateTime {\n        const micros = dt.micros;\n        switch (unit) {\n            .days => return fromUnix(micros + value * MICROSECONDS_IN_A_DAY, .microseconds),\n            .hours => return fromUnix(micros + value * MICROSECONDS_IN_AN_HOUR, .microseconds),\n            .minutes => return fromUnix(micros + value * MICROSECONDS_IN_A_MIN, .microseconds),\n            .seconds => return fromUnix(micros + value * MICROSECONDS_IN_A_SEC, .microseconds),\n            .milliseconds => return fromUnix(micros + value * 1_000, .microseconds),\n            .microseconds => return fromUnix(micros + value, .microseconds),\n        }\n    }\n\n    pub fn sub(a: DateTime, b: DateTime, precision: TimestampPrecision) i64 {\n        return a.unix(precision) - b.unix(precision);\n    }\n\n    // https://git.musl-libc.org/cgit/musl/tree/src/time/__secs_to_tm.c?h=v0.9.15\n    pub fn date(dt: DateTime) Date {\n        // 2000-03-01 (mod 400 year, immediately after feb29\n        const leap_epoch = 946684800 + 86400 * (31 + 29);\n        const days_per_400y = 365 * 400 + 97;\n        const days_per_100y = 365 * 100 + 24;\n        const days_per_4y = 365 * 4 + 1;\n\n        // march-based\n        const month_days = [_]u8{ 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29 };\n\n        const secs = @divTrunc(dt.micros, 1_000_000) - leap_epoch;\n\n        var days = @divTrunc(secs, 86400);\n        if (@rem(secs, 86400) < 0) {\n            days -= 1;\n        }\n\n        var qc_cycles = @divTrunc(days, days_per_400y);\n        var rem_days = @rem(days, days_per_400y);\n        if (rem_days < 0) {\n            rem_days += days_per_400y;\n            qc_cycles -= 1;\n        }\n\n        var c_cycles = @divTrunc(rem_days, days_per_100y);\n        if (c_cycles == 4) {\n            c_cycles -= 1;\n        }\n        rem_days -= c_cycles * days_per_100y;\n\n        var q_cycles = @divTrunc(rem_days, days_per_4y);\n        if (q_cycles == 25) {\n            q_cycles -= 1;\n        }\n        rem_days -= q_cycles * days_per_4y;\n\n        var rem_years = @divTrunc(rem_days, 365);\n        if (rem_years == 4) {\n            rem_years -= 1;\n        }\n        rem_days -= rem_years * 365;\n\n        var year = rem_years + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles + 2000;\n\n        var month: u8 = 0;\n        while (month_days[month] <= rem_days) : (month += 1) {\n            rem_days -= month_days[month];\n        }\n\n        month += 2;\n        if (month >= 12) {\n            year += 1;\n            month -= 12;\n        }\n\n        return .{\n            .year = @intCast(year),\n            .month = month + 1,\n            .day = @intCast(rem_days + 1),\n        };\n    }\n\n    pub fn time(dt: DateTime) Time {\n        const micros = @mod(dt.micros, MICROSECONDS_IN_A_DAY);\n\n        return .{\n            .hour = @intCast(@divTrunc(micros, MICROSECONDS_IN_AN_HOUR)),\n            .min = @intCast(@divTrunc(@rem(micros, MICROSECONDS_IN_AN_HOUR), MICROSECONDS_IN_A_MIN)),\n            .sec = @intCast(@divTrunc(@rem(micros, MICROSECONDS_IN_A_MIN), MICROSECONDS_IN_A_SEC)),\n            .micros = @intCast(@rem(micros, MICROSECONDS_IN_A_SEC)),\n        };\n    }\n\n    pub fn unix(self: DateTime, precision: TimestampPrecision) i64 {\n        const micros = self.micros;\n        return switch (precision) {\n            .seconds => @divTrunc(micros, 1_000_000),\n            .milliseconds => @divTrunc(micros, 1_000),\n            .microseconds => micros,\n        };\n    }\n\n    pub fn order(a: DateTime, b: DateTime) std.math.Order {\n        return std.math.order(a.micros, b.micros);\n    }\n\n    pub fn format(self: DateTime, writer: *std.Io.Writer) !void {\n        var buf: [28]u8 = undefined;\n        const n = self.bufWrite(&buf);\n        try writer.writeAll(buf[0..n]);\n    }\n\n    pub fn jsonStringify(self: DateTime, out: anytype) !void {\n        var buf: [30]u8 = undefined;\n        buf[0] = '\"';\n        const n = self.bufWrite(buf[1..]);\n        buf[n + 1] = '\"';\n        try out.print(\"{s}\", .{buf[0 .. n + 2]});\n    }\n\n    pub fn jsonParse(allocator: Allocator, source: anytype, options: anytype) !DateTime {\n        _ = options;\n\n        switch (try source.nextAlloc(allocator, .alloc_if_needed)) {\n            inline .string, .allocated_string => |str| return parseRFC3339(str) catch return error.InvalidCharacter,\n            else => return error.UnexpectedToken,\n        }\n    }\n\n    fn bufWrite(self: DateTime, buf: []u8) usize {\n        const date_n = writeDate(buf, self.date());\n\n        buf[date_n] = 'T';\n\n        const time_start = date_n + 1;\n        const time_n = writeTime(buf[time_start..], self.time());\n\n        const time_stop = time_start + time_n;\n        buf[time_stop] = 'Z';\n\n        return time_stop + 1;\n    }\n};\n\n// true if we should use clock_gettime()\nconst is_posix = switch (builtin.os.tag) {\n    .windows, .uefi, .wasi => false,\n    else => true,\n};\n\npub const TimestampMode = enum {\n    clock,\n    monotonic,\n};\npub fn timestamp(comptime mode: TimestampMode) u64 {\n    if (comptime is_posix == false or mode == .clock) {\n        return @intCast(std.time.timestamp());\n    }\n    const ts = timespec();\n    return @intCast(ts.sec);\n}\n\npub fn milliTimestamp(comptime mode: TimestampMode) u64 {\n    if (comptime is_posix == false or mode == .clock) {\n        return @intCast(std.time.milliTimestamp());\n    }\n    const ts = timespec();\n    return @as(u64, @intCast(ts.sec)) * 1000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000_000)));\n}\n\npub fn timespec() posix.timespec {\n    if (comptime is_posix == false) {\n        @compileError(\"`timespec` should not be called when `is_posix` is false\");\n    }\n\n    const clock_id = switch (@import(\"builtin\").os.tag) {\n        .freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST,\n        .macos, .ios, .tvos, .watchos, .visionos => posix.CLOCK.UPTIME_RAW, // continues counting while suspended\n        .linux => posix.CLOCK.BOOTTIME, // continues counting while suspended\n        else => posix.CLOCK.MONOTONIC,\n    };\n    // unreac\n    return posix.clock_gettime(clock_id) catch unreachable;\n}\n\nfn writeDate(into: []u8, date: Date) u8 {\n    var buf: []u8 = undefined;\n    // cast year to a u16 so it doesn't insert a sign\n    // we don't want the + sign, ever\n    // and we don't even want it to insert the - sign, because it screws up\n    // the padding (we need to do it ourselfs)\n    const year = date.year;\n    if (year < 0) {\n        _ = std.fmt.printInt(into[1..], @as(u16, @intCast(year * -1)), 10, .lower, .{ .width = 4, .fill = '0' });\n        into[0] = '-';\n        buf = into[5..];\n    } else {\n        _ = std.fmt.printInt(into, @as(u16, @intCast(year)), 10, .lower, .{ .width = 4, .fill = '0' });\n        buf = into[4..];\n    }\n\n    buf[0] = '-';\n    buf[1..3].* = paddingTwoDigits(date.month);\n    buf[3] = '-';\n    buf[4..6].* = paddingTwoDigits(date.day);\n\n    // return the length of the string. 10 for positive year, 11 for negative\n    return if (year < 0) 11 else 10;\n}\n\nfn writeTime(into: []u8, time: Time) u8 {\n    into[0..2].* = paddingTwoDigits(time.hour);\n    into[2] = ':';\n    into[3..5].* = paddingTwoDigits(time.min);\n    into[5] = ':';\n    into[6..8].* = paddingTwoDigits(time.sec);\n\n    const micros = time.micros;\n    if (micros == 0) {\n        return 8;\n    }\n\n    if (@rem(micros, 1000) == 0) {\n        into[8] = '.';\n        _ = std.fmt.printInt(into[9..12], micros / 1000, 10, .lower, .{ .width = 3, .fill = '0' });\n        return 12;\n    }\n\n    into[8] = '.';\n    _ = std.fmt.printInt(into[9..15], micros, 10, .lower, .{ .width = 6, .fill = '0' });\n    return 15;\n}\n\nfn paddingTwoDigits(value: usize) [2]u8 {\n    lp.assert(value < 61, \"datetime.paddingTwoDigits\", .{ .value = value });\n    const digits = \"0001020304050607080910111213141516171819\" ++\n        \"2021222324252627282930313233343536373839\" ++\n        \"4041424344454647484950515253545556575859\" ++\n        \"60\";\n    return digits[value * 2 ..][0..2].*;\n}\n\nconst Parser = struct {\n    input: []const u8,\n    pos: usize,\n\n    fn init(input: []const u8) Parser {\n        return .{\n            .pos = 0,\n            .input = input,\n        };\n    }\n\n    fn unconsumed(self: *const Parser) usize {\n        return self.input.len - self.pos;\n    }\n\n    fn rest(self: *const Parser) []const u8 {\n        return self.input[self.pos..];\n    }\n\n    // unsafe, assumes caller has checked remaining first\n    fn peek(self: *const Parser) u8 {\n        return self.input[self.pos];\n    }\n\n    // unsafe, assumes caller has checked remaining first\n    fn consumeIf(self: *Parser, c: u8) bool {\n        const pos = self.pos;\n        if (self.input[pos] != c) {\n            return false;\n        }\n        self.pos = pos + 1;\n        return true;\n    }\n\n    fn consumeN(self: *Parser, n: usize) ?[]const u8 {\n        const pos = self.pos;\n        const end = pos + n;\n        if (end > self.input.len) {\n            return null;\n        }\n\n        defer self.pos = end;\n        return self.input[pos..end];\n    }\n\n    fn nanoseconds(self: *Parser) ?usize {\n        const start = self.pos;\n        const input = self.input[start..];\n\n        var len = input.len;\n        if (len == 0) {\n            return null;\n        }\n\n        var value: usize = 0;\n        for (input, 0..) |b, i| {\n            const n = b -% '0'; // wrapping subtraction\n            if (n > 9) {\n                len = i;\n                break;\n            }\n            value = value * 10 + n;\n        }\n\n        if (len > 9) {\n            return null;\n        }\n\n        self.pos = start + len;\n        return value * std.math.pow(usize, 10, 9 - len);\n    }\n\n    fn paddedInt(self: *Parser, comptime T: type, size: u8) ?T {\n        const pos = self.pos;\n        const end = pos + size;\n        const input = self.input;\n\n        if (end > input.len) {\n            return null;\n        }\n\n        var value: T = 0;\n        for (input[pos..end]) |b| {\n            const n = b -% '0'; // wrapping subtraction\n            if (n > 9) return null;\n            value = value * 10 + n;\n        }\n        self.pos = end;\n        return value;\n    }\n\n    fn time(self: *Parser, allow_nano: bool) !Time {\n        const len = self.unconsumed();\n        if (len < 5) {\n            return error.InvalidTime;\n        }\n\n        const hour = self.paddedInt(u8, 2) orelse return error.InvalidTime;\n        if (self.consumeIf(':') == false) {\n            return error.InvalidTime;\n        }\n\n        const min = self.paddedInt(u8, 2) orelse return error.InvalidTime;\n        if (len == 5 or self.consumeIf(':') == false) {\n            return Time.init(hour, min, 0, 0);\n        }\n\n        const sec = self.paddedInt(u8, 2) orelse return error.InvalidTime;\n        if (allow_nano == false or len == 8 or self.consumeIf('.') == false) {\n            return Time.init(hour, min, sec, 0);\n        }\n\n        const nanos = self.nanoseconds() orelse return error.InvalidTime;\n        return Time.init(hour, min, sec, @intCast(nanos / 1000));\n    }\n\n    fn iso8601Date(self: *Parser) !Date {\n        const len = self.unconsumed();\n        if (len < 8) {\n            return error.InvalidDate;\n        }\n\n        const negative = self.consumeIf('-');\n        const year = self.paddedInt(i16, 4) orelse return error.InvalidDate;\n\n        var with_dashes = false;\n        if (self.consumeIf('-')) {\n            if (len < 10) {\n                return error.InvalidDate;\n            }\n            with_dashes = true;\n        }\n\n        const month = self.paddedInt(u8, 2) orelse return error.InvalidDate;\n        if (self.consumeIf('-') == !with_dashes) {\n            return error.InvalidDate;\n        }\n\n        const day = self.paddedInt(u8, 2) orelse return error.InvalidDate;\n        return Date.init(if (negative) -year else year, month, day);\n    }\n\n    fn rfc3339Date(self: *Parser) !Date {\n        const len = self.unconsumed();\n        if (len < 10) {\n            return error.InvalidDate;\n        }\n\n        const negative = self.consumeIf('-');\n        const year = self.paddedInt(i16, 4) orelse return error.InvalidDate;\n\n        if (self.consumeIf('-') == false) {\n            return error.InvalidDate;\n        }\n\n        const month = self.paddedInt(u8, 2) orelse return error.InvalidDate;\n\n        if (self.consumeIf('-') == false) {\n            return error.InvalidDate;\n        }\n\n        const day = self.paddedInt(u8, 2) orelse return error.InvalidDate;\n        return Date.init(if (negative) -year else year, month, day);\n    }\n};\n\nconst testing = @import(\"testing.zig\");\ntest \"Date: json\" {\n    {\n        // date, positive year\n        const date = Date{ .year = 2023, .month = 9, .day = 22 };\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, date, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"2023-09-22\\\"\", out);\n    }\n\n    {\n        // date, negative year\n        const date = Date{ .year = -4, .month = 12, .day = 3 };\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, date, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"-0004-12-03\\\"\", out);\n    }\n\n    {\n        // parse\n        const ts = try std.json.parseFromSlice(TestStruct, testing.allocator, \"{\\\"date\\\":\\\"2023-09-22\\\"}\", .{});\n        defer ts.deinit();\n        try testing.expectEqual(Date{ .year = 2023, .month = 9, .day = 22 }, ts.value.date.?);\n    }\n}\n\ntest \"Date: format\" {\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Date{ .year = 2023, .month = 5, .day = 22 }});\n        try testing.expectString(\"2023-05-22\", out);\n    }\n\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Date{ .year = -102, .month = 12, .day = 9 }});\n        try testing.expectString(\"-0102-12-09\", out);\n    }\n}\n\ntest \"Date: parse ISO8601\" {\n    {\n        //valid YYYY-MM-DD\n        try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 22 }, try Date.parse(\"2023-05-22\", .iso8601));\n        try testing.expectEqual(Date{ .year = -2023, .month = 2, .day = 3 }, try Date.parse(\"-2023-02-03\", .iso8601));\n        try testing.expectEqual(Date{ .year = 1, .month = 2, .day = 3 }, try Date.parse(\"0001-02-03\", .iso8601));\n        try testing.expectEqual(Date{ .year = -1, .month = 2, .day = 3 }, try Date.parse(\"-0001-02-03\", .iso8601));\n    }\n\n    {\n        //valid YYYYMMDD\n        try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 22 }, try Date.parse(\"20230522\", .iso8601));\n        try testing.expectEqual(Date{ .year = -2023, .month = 2, .day = 3 }, try Date.parse(\"-20230203\", .iso8601));\n        try testing.expectEqual(Date{ .year = 1, .month = 2, .day = 3 }, try Date.parse(\"00010203\", .iso8601));\n        try testing.expectEqual(Date{ .year = -1, .month = 2, .day = 3 }, try Date.parse(\"-00010203\", .iso8601));\n    }\n}\n\ntest \"Date: parse RFC339\" {\n    {\n        //valid YYYY-MM-DD\n        try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 22 }, try Date.parse(\"2023-05-22\", .rfc3339));\n        try testing.expectEqual(Date{ .year = -2023, .month = 2, .day = 3 }, try Date.parse(\"-2023-02-03\", .rfc3339));\n        try testing.expectEqual(Date{ .year = 1, .month = 2, .day = 3 }, try Date.parse(\"0001-02-03\", .rfc3339));\n        try testing.expectEqual(Date{ .year = -1, .month = 2, .day = 3 }, try Date.parse(\"-0001-02-03\", .rfc3339));\n    }\n\n    {\n        //valid YYYYMMDD\n        try testing.expectError(error.InvalidDate, Date.parse(\"20230522\", .rfc3339));\n        try testing.expectError(error.InvalidDate, Date.parse(\"-20230203\", .rfc3339));\n        try testing.expectError(error.InvalidDate, Date.parse(\"00010203\", .rfc3339));\n        try testing.expectError(error.InvalidDate, Date.parse(\"-00010203\", .rfc3339));\n    }\n}\n\ntest \"Date: parse invalid common\" {\n    for (&[_]Date.Format{ .rfc3339, .iso8601 }) |format| {\n        {\n            // invalid format\n            try testing.expectError(error.InvalidDate, Date.parse(\"\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023/01-02\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-01/02\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"0001-01-01 \", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-1-02\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-01-2\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"9-01-2\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"99-01-2\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"999-01-2\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"-999-01-2\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"-1-01-2\", format));\n        }\n\n        {\n            // invalid month\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-00-22\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-0A-22\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-13-22\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-99-22\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"-2023-00-22\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"-2023-13-22\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"-2023-99-22\", format));\n        }\n\n        {\n            // invalid day\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-01-00\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-01-32\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-02-29\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-03-32\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-04-31\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-05-32\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-06-31\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-07-32\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-08-32\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-09-31\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-10-32\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-11-31\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2023-12-32\", format));\n        }\n\n        {\n            // valid (max day)\n            try testing.expectEqual(Date{ .year = 2023, .month = 1, .day = 31 }, try Date.parse(\"2023-01-31\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 2, .day = 28 }, try Date.parse(\"2023-02-28\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 3, .day = 31 }, try Date.parse(\"2023-03-31\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 4, .day = 30 }, try Date.parse(\"2023-04-30\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 31 }, try Date.parse(\"2023-05-31\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 6, .day = 30 }, try Date.parse(\"2023-06-30\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 7, .day = 31 }, try Date.parse(\"2023-07-31\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 8, .day = 31 }, try Date.parse(\"2023-08-31\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 9, .day = 30 }, try Date.parse(\"2023-09-30\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 10, .day = 31 }, try Date.parse(\"2023-10-31\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 30 }, try Date.parse(\"2023-11-30\", format));\n            try testing.expectEqual(Date{ .year = 2023, .month = 12, .day = 31 }, try Date.parse(\"2023-12-31\", format));\n        }\n\n        {\n            // leap years\n            try testing.expectEqual(Date{ .year = 2000, .month = 2, .day = 29 }, try Date.parse(\"2000-02-29\", format));\n            try testing.expectEqual(Date{ .year = 2400, .month = 2, .day = 29 }, try Date.parse(\"2400-02-29\", format));\n            try testing.expectEqual(Date{ .year = 2012, .month = 2, .day = 29 }, try Date.parse(\"2012-02-29\", format));\n            try testing.expectEqual(Date{ .year = 2024, .month = 2, .day = 29 }, try Date.parse(\"2024-02-29\", format));\n\n            try testing.expectError(error.InvalidDate, Date.parse(\"2000-02-30\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2400-02-30\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2012-02-30\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2024-02-30\", format));\n\n            try testing.expectError(error.InvalidDate, Date.parse(\"2100-02-29\", format));\n            try testing.expectError(error.InvalidDate, Date.parse(\"2200-02-29\", format));\n        }\n    }\n}\n\ntest \"Date: order\" {\n    {\n        const a = Date{ .year = 2023, .month = 5, .day = 22 };\n        const b = Date{ .year = 2023, .month = 5, .day = 22 };\n        try testing.expectEqual(std.math.Order.eq, a.order(b));\n    }\n\n    {\n        const a = Date{ .year = 2023, .month = 5, .day = 22 };\n        const b = Date{ .year = 2022, .month = 5, .day = 22 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = Date{ .year = 2022, .month = 6, .day = 22 };\n        const b = Date{ .year = 2022, .month = 5, .day = 22 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = Date{ .year = 2023, .month = 5, .day = 23 };\n        const b = Date{ .year = 2022, .month = 5, .day = 22 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n}\n\ntest \"Time: json\" {\n    {\n        // time no fraction\n        const time = Time{ .hour = 23, .min = 59, .sec = 2, .micros = 0 };\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, time, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"23:59:02\\\"\", out);\n    }\n\n    {\n        // time, milliseconds only\n        const time = Time{ .hour = 7, .min = 9, .sec = 32, .micros = 202000 };\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, time, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"07:09:32.202\\\"\", out);\n    }\n\n    {\n        // time, micros\n        const time = Time{ .hour = 1, .min = 2, .sec = 3, .micros = 123456 };\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, time, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"01:02:03.123456\\\"\", out);\n    }\n\n    {\n        // parse\n        const ts = try std.json.parseFromSlice(TestStruct, testing.allocator, \"{\\\"time\\\":\\\"01:02:03.123456\\\"}\", .{});\n        defer ts.deinit();\n        try testing.expectEqual(Time{ .hour = 1, .min = 2, .sec = 3, .micros = 123456 }, ts.value.time.?);\n    }\n}\n\ntest \"Time: format\" {\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Time{ .hour = 23, .min = 59, .sec = 59, .micros = 0 }});\n        try testing.expectString(\"23:59:59\", out);\n    }\n\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 12 }});\n        try testing.expectString(\"08:09:10.000012\", out);\n    }\n\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 123 }});\n        try testing.expectString(\"08:09:10.000123\", out);\n    }\n\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 1234 }});\n        try testing.expectString(\"08:09:10.001234\", out);\n    }\n\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 12345 }});\n        try testing.expectString(\"08:09:10.012345\", out);\n    }\n\n    {\n        var buf: [20]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 123456 }});\n        try testing.expectString(\"08:09:10.123456\", out);\n    }\n}\n\ntest \"Time: parse\" {\n    {\n        //valid\n        try testing.expectEqual(Time{ .hour = 9, .min = 8, .sec = 0, .micros = 0 }, try Time.parse(\"09:08\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 9, .min = 8, .sec = 5, .micros = 123000 }, try Time.parse(\"09:08:05.123\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 23, .min = 59, .sec = 59, .micros = 0 }, try Time.parse(\"23:59:59\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 0 }, try Time.parse(\"00:00:00\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 0 }, try Time.parse(\"00:00:00.0\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 1 }, try Time.parse(\"00:00:00.000001\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 12 }, try Time.parse(\"00:00:00.000012\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123 }, try Time.parse(\"00:00:00.000123\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 1234 }, try Time.parse(\"00:00:00.001234\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 12345 }, try Time.parse(\"00:00:00.012345\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse(\"00:00:00.123456\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse(\"00:00:00.1234567\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse(\"00:00:00.12345678\", .rfc3339));\n        try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse(\"00:00:00.123456789\", .rfc3339));\n    }\n\n    {\n        try testing.expectError(error.InvalidTime, Time.parse(\"\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"01:00:\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"1:00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"10:1:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"10:11:4\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"10:20:30.\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"10:20:30.a\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"10:20:30.1234567899\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"10:20:30.123Z\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"24:00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"00:60:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"00:00:60\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"0a:00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"00:0a:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"00:00:0a\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"00/00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, Time.parse(\"00:00 00\", .rfc3339));\n    }\n}\n\ntest \"Time: order\" {\n    {\n        const a = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 };\n        const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 };\n        try testing.expectEqual(std.math.Order.eq, a.order(b));\n    }\n\n    {\n        const a = Time{ .hour = 20, .min = 17, .sec = 22, .micros = 101002 };\n        const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = Time{ .hour = 19, .min = 18, .sec = 22, .micros = 101002 };\n        const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = Time{ .hour = 19, .min = 17, .sec = 23, .micros = 101002 };\n        const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101003 };\n        const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 };\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n}\n\ntest \"DateTime: initUTC\" {\n    // GO\n    // for i := 0; i < 100; i++ {\n    //   us := rand.Int63n(31536000000000000)\n    //   if i%2 == 1 {\n    //     us = -us\n    //   }\n    //   date := time.UnixMicro(us).UTC()\n    //   fmt.Printf(\"\\ttry testing.expectEqual(%d, (try DateTime.initUTC(%d, %d, %d, %d, %d, %d, %d)).micros);\\n\", us, date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), date.Nanosecond()/1000)\n    // }\n    try testing.expectEqual(31185488490276150, (try DateTime.initUTC(2958, 3, 25, 3, 41, 30, 276150)).micros);\n    try testing.expectEqual(-17564653328342207, (try DateTime.initUTC(1413, 5, 26, 9, 37, 51, 657793)).micros);\n    try testing.expectEqual(11204762425459393, (try DateTime.initUTC(2325, 1, 24, 18, 0, 25, 459393)).micros);\n    try testing.expectEqual(-11416605162739875, (try DateTime.initUTC(1608, 3, 22, 8, 47, 17, 260125)).micros);\n    try testing.expectEqual(4075732367920414, (try DateTime.initUTC(2099, 2, 25, 19, 52, 47, 920414)).micros);\n    try testing.expectEqual(-18408335598163579, (try DateTime.initUTC(1386, 8, 30, 13, 26, 41, 836421)).micros);\n    try testing.expectEqual(17086490946271926, (try DateTime.initUTC(2511, 6, 14, 7, 29, 6, 271926)).micros);\n    try testing.expectEqual(-235277150936616, (try DateTime.initUTC(1962, 7, 18, 21, 14, 9, 63384)).micros);\n    try testing.expectEqual(11104788804726682, (try DateTime.initUTC(2321, 11, 24, 15, 33, 24, 726682)).micros);\n    try testing.expectEqual(-4568937205156452, (try DateTime.initUTC(1825, 3, 20, 18, 46, 34, 843548)).micros);\n    try testing.expectEqual(24765673968274275, (try DateTime.initUTC(2754, 10, 17, 17, 52, 48, 274275)).micros);\n    try testing.expectEqual(-7121990846251510, (try DateTime.initUTC(1744, 4, 24, 13, 12, 33, 748490)).micros);\n    try testing.expectEqual(17226397205968456, (try DateTime.initUTC(2515, 11, 19, 14, 20, 5, 968456)).micros);\n    try testing.expectEqual(-6754262392339050, (try DateTime.initUTC(1755, 12, 19, 16, 0, 7, 660950)).micros);\n    try testing.expectEqual(16357572620714009, (try DateTime.initUTC(2488, 5, 7, 18, 10, 20, 714009)).micros);\n    try testing.expectEqual(-25688820176639049, (try DateTime.initUTC(1155, 12, 15, 16, 37, 3, 360951)).micros);\n    try testing.expectEqual(20334458172336139, (try DateTime.initUTC(2614, 5, 17, 12, 36, 12, 336139)).micros);\n    try testing.expectEqual(-30602962159178117, (try DateTime.initUTC(1000, 3, 26, 1, 10, 40, 821883)).micros);\n    try testing.expectEqual(10851036879825648, (try DateTime.initUTC(2313, 11, 9, 16, 54, 39, 825648)).micros);\n    try testing.expectEqual(-21853769826060317, (try DateTime.initUTC(1277, 6, 24, 20, 22, 53, 939683)).micros);\n    try testing.expectEqual(23747326217087461, (try DateTime.initUTC(2722, 7, 11, 7, 30, 17, 87461)).micros);\n    try testing.expectEqual(-6579703114708064, (try DateTime.initUTC(1761, 7, 1, 0, 41, 25, 291936)).micros);\n    try testing.expectEqual(14734931422924073, (try DateTime.initUTC(2436, 12, 6, 4, 30, 22, 924073)).micros);\n    try testing.expectEqual(-14370161672281011, (try DateTime.initUTC(1514, 8, 18, 16, 25, 27, 718989)).micros);\n    try testing.expectEqual(21611484560584058, (try DateTime.initUTC(2654, 11, 3, 22, 9, 20, 584058)).micros);\n    try testing.expectEqual(-15774514890527755, (try DateTime.initUTC(1470, 2, 15, 14, 18, 29, 472245)).micros);\n    try testing.expectEqual(12457884381373706, (try DateTime.initUTC(2364, 10, 10, 11, 26, 21, 373706)).micros);\n    try testing.expectEqual(-9291409512875127, (try DateTime.initUTC(1675, 7, 26, 12, 54, 47, 124873)).micros);\n    try testing.expectEqual(18766703512694310, (try DateTime.initUTC(2564, 9, 10, 5, 11, 52, 694310)).micros);\n    try testing.expectEqual(-10898338457124469, (try DateTime.initUTC(1624, 8, 23, 19, 45, 42, 875531)).micros);\n    try testing.expectEqual(27404278841361952, (try DateTime.initUTC(2838, 5, 29, 3, 40, 41, 361952)).micros);\n    try testing.expectEqual(-11493696741549109, (try DateTime.initUTC(1605, 10, 12, 2, 27, 38, 450891)).micros);\n    try testing.expectEqual(25167839321247044, (try DateTime.initUTC(2767, 7, 16, 10, 28, 41, 247044)).micros);\n    try testing.expectEqual(-8645720427930599, (try DateTime.initUTC(1696, 1, 10, 18, 59, 32, 69401)).micros);\n    try testing.expectEqual(7021225980669527, (try DateTime.initUTC(2192, 6, 29, 4, 33, 0, 669527)).micros);\n    try testing.expectEqual(-22567421500525473, (try DateTime.initUTC(1254, 11, 12, 23, 48, 19, 474527)).micros);\n    try testing.expectEqual(3592419409525180, (try DateTime.initUTC(2083, 11, 2, 22, 16, 49, 525180)).micros);\n    try testing.expectEqual(-24897829995733878, (try DateTime.initUTC(1181, 1, 7, 16, 6, 44, 266122)).micros);\n    try testing.expectEqual(1801796752202729, (try DateTime.initUTC(2027, 2, 5, 3, 5, 52, 202729)).micros);\n    try testing.expectEqual(-21458729756349585, (try DateTime.initUTC(1289, 12, 31, 1, 44, 3, 650415)).micros);\n    try testing.expectEqual(27431277767015263, (try DateTime.initUTC(2839, 4, 6, 15, 22, 47, 15263)).micros);\n    try testing.expectEqual(-11932647633976328, (try DateTime.initUTC(1591, 11, 14, 15, 39, 26, 23672)).micros);\n    try testing.expectEqual(11561116817530249, (try DateTime.initUTC(2336, 5, 11, 5, 20, 17, 530249)).micros);\n    try testing.expectEqual(-20238374988448844, (try DateTime.initUTC(1328, 9, 2, 13, 10, 11, 551156)).micros);\n    try testing.expectEqual(17825448287939368, (try DateTime.initUTC(2534, 11, 13, 1, 24, 47, 939368)).micros);\n    try testing.expectEqual(-16551182110752962, (try DateTime.initUTC(1445, 7, 7, 9, 24, 49, 247038)).micros);\n    try testing.expectEqual(7773488831126355, (try DateTime.initUTC(2216, 5, 1, 22, 27, 11, 126355)).micros);\n    try testing.expectEqual(-17967725644400042, (try DateTime.initUTC(1400, 8, 17, 5, 5, 55, 599958)).micros);\n    try testing.expectEqual(30634276344447791, (try DateTime.initUTC(2940, 10, 5, 9, 12, 24, 447791)).micros);\n    try testing.expectEqual(-3201531339091604, (try DateTime.initUTC(1868, 7, 19, 5, 44, 20, 908396)).micros);\n    try testing.expectEqual(16621702451341054, (try DateTime.initUTC(2496, 9, 19, 19, 34, 11, 341054)).micros);\n    try testing.expectEqual(-12321145808433043, (try DateTime.initUTC(1579, 7, 24, 3, 29, 51, 566957)).micros);\n    try testing.expectEqual(116851935152341, (try DateTime.initUTC(1973, 9, 14, 10, 52, 15, 152341)).micros);\n    try testing.expectEqual(-26516365395395707, (try DateTime.initUTC(1129, 9, 24, 14, 56, 44, 604293)).micros);\n    try testing.expectEqual(29944637164250909, (try DateTime.initUTC(2918, 11, 28, 10, 46, 4, 250909)).micros);\n    try testing.expectEqual(-14268089958574835, (try DateTime.initUTC(1517, 11, 12, 1, 40, 41, 425165)).micros);\n    try testing.expectEqual(10902808879115327, (try DateTime.initUTC(2315, 7, 1, 22, 1, 19, 115327)).micros);\n    try testing.expectEqual(-13675746347719473, (try DateTime.initUTC(1536, 8, 19, 21, 34, 12, 280527)).micros);\n    try testing.expectEqual(9823904882276154, (try DateTime.initUTC(2281, 4, 22, 14, 28, 2, 276154)).micros);\n    try testing.expectEqual(-8027825490751946, (try DateTime.initUTC(1715, 8, 11, 8, 28, 29, 248054)).micros);\n    try testing.expectEqual(8338818189787922, (try DateTime.initUTC(2234, 4, 1, 2, 23, 9, 787922)).micros);\n    try testing.expectEqual(-2417779710874201, (try DateTime.initUTC(1893, 5, 20, 10, 31, 29, 125799)).micros);\n    try testing.expectEqual(15579463520321126, (try DateTime.initUTC(2463, 9, 10, 20, 45, 20, 321126)).micros);\n    try testing.expectEqual(-30111774746323219, (try DateTime.initUTC(1015, 10, 19, 2, 7, 33, 676781)).micros);\n    try testing.expectEqual(8586318907201828, (try DateTime.initUTC(2242, 2, 2, 16, 35, 7, 201828)).micros);\n    try testing.expectEqual(-20727462914538728, (try DateTime.initUTC(1313, 3, 4, 19, 24, 45, 461272)).micros);\n    try testing.expectEqual(12684924982677857, (try DateTime.initUTC(2371, 12, 21, 6, 16, 22, 677857)).micros);\n    try testing.expectEqual(-26995363453933698, (try DateTime.initUTC(1114, 7, 21, 15, 55, 46, 66302)).micros);\n    try testing.expectEqual(5769549719315448, (try DateTime.initUTC(2152, 10, 30, 4, 41, 59, 315448)).micros);\n    try testing.expectEqual(-9362762735064704, (try DateTime.initUTC(1673, 4, 21, 16, 34, 24, 935296)).micros);\n    try testing.expectEqual(5196087673076825, (try DateTime.initUTC(2134, 8, 28, 21, 41, 13, 76825)).micros);\n    try testing.expectEqual(-10198286600499296, (try DateTime.initUTC(1646, 10, 30, 6, 36, 39, 500704)).micros);\n    try testing.expectEqual(19333137979539125, (try DateTime.initUTC(2582, 8, 23, 4, 6, 19, 539125)).micros);\n    try testing.expectEqual(-18867539824804327, (try DateTime.initUTC(1372, 2, 10, 16, 42, 55, 195673)).micros);\n    try testing.expectEqual(14853031249581056, (try DateTime.initUTC(2440, 9, 3, 2, 0, 49, 581056)).micros);\n    try testing.expectEqual(-1356282109230506, (try DateTime.initUTC(1927, 1, 9, 6, 58, 10, 769494)).micros);\n    try testing.expectEqual(15713222018105813, (try DateTime.initUTC(2467, 12, 6, 23, 53, 38, 105813)).micros);\n    try testing.expectEqual(-12693041975378709, (try DateTime.initUTC(1567, 10, 10, 19, 0, 24, 621291)).micros);\n    try testing.expectEqual(29394313298789588, (try DateTime.initUTC(2901, 6, 20, 23, 1, 38, 789588)).micros);\n    try testing.expectEqual(-10583952098364782, (try DateTime.initUTC(1634, 8, 10, 13, 18, 21, 635218)).micros);\n    try testing.expectEqual(22418800474726154, (try DateTime.initUTC(2680, 6, 3, 20, 34, 34, 726154)).micros);\n    try testing.expectEqual(-13067278028607441, (try DateTime.initUTC(1555, 12, 1, 8, 32, 51, 392559)).micros);\n    try testing.expectEqual(22348003126725817, (try DateTime.initUTC(2678, 3, 7, 10, 38, 46, 725817)).micros);\n    try testing.expectEqual(-11101998054915852, (try DateTime.initUTC(1618, 3, 11, 15, 39, 5, 84148)).micros);\n    try testing.expectEqual(30004645932503986, (try DateTime.initUTC(2920, 10, 22, 23, 52, 12, 503986)).micros);\n    try testing.expectEqual(-27551013013624622, (try DateTime.initUTC(1096, 12, 10, 12, 49, 46, 375378)).micros);\n    try testing.expectEqual(10162791607756167, (try DateTime.initUTC(2292, 1, 17, 21, 40, 7, 756167)).micros);\n    try testing.expectEqual(-31309636417799549, (try DateTime.initUTC(977, 11, 1, 22, 46, 22, 200451)).micros);\n    try testing.expectEqual(9816298180956872, (try DateTime.initUTC(2281, 1, 24, 13, 29, 40, 956872)).micros);\n    try testing.expectEqual(-13248552913008079, (try DateTime.initUTC(1550, 3, 4, 6, 24, 46, 991921)).micros);\n    try testing.expectEqual(24898184818866845, (try DateTime.initUTC(2758, 12, 29, 10, 26, 58, 866845)).micros);\n    try testing.expectEqual(-10721424878768860, (try DateTime.initUTC(1630, 4, 2, 10, 25, 21, 231140)).micros);\n    try testing.expectEqual(3556757075942051, (try DateTime.initUTC(2082, 9, 16, 4, 4, 35, 942051)).micros);\n    try testing.expectEqual(-9515936853544912, (try DateTime.initUTC(1668, 6, 13, 20, 12, 26, 455088)).micros);\n    try testing.expectEqual(23236928933459964, (try DateTime.initUTC(2706, 5, 8, 22, 28, 53, 459964)).micros);\n    try testing.expectEqual(-5811784886171477, (try DateTime.initUTC(1785, 10, 30, 23, 18, 33, 828523)).micros);\n    try testing.expectEqual(27342496921109542, (try DateTime.initUTC(2836, 6, 13, 2, 2, 1, 109542)).micros);\n    try testing.expectEqual(-25369943235288340, (try DateTime.initUTC(1166, 1, 22, 9, 32, 44, 711660)).micros);\n    try testing.expectEqual(10054378230055484, (try DateTime.initUTC(2288, 8, 11, 2, 50, 30, 55484)).micros);\n    try testing.expectEqual(-10826899878642792, (try DateTime.initUTC(1626, 11, 28, 15, 48, 41, 357208)).micros);\n}\n\ntest \"DateTime: now\" {\n    const dt = DateTime.now();\n    try testing.expectDelta(std.time.microTimestamp(), dt.micros, 1000);\n}\n\ntest \"DateTime: date\" {\n    try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 25 }, (try DateTime.fromUnix(1700886257, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 25 }, (try DateTime.fromUnix(1700886257655, .milliseconds)).date());\n    try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 25 }, (try DateTime.fromUnix(1700886257655392, .microseconds)).date());\n    try testing.expectEqual(Date{ .year = 1970, .month = 1, .day = 1 }, (try DateTime.fromUnix(0, .milliseconds)).date());\n\n    // GO:\n    // for i := 0; i < 100; i++ {\n    //   us := rand.Int63n(31536000000000000)\n    //   if i%2 == 1 {\n    //     us = -us\n    //   }\n    //   date := time.UnixMicro(us).UTC()\n    //   fmt.Printf(\"\\ttry testing.expectEqual(Date{.year = %d, .month = %d, .day = %d}, DateTime.fromUnix(%d, .seconds).date());\\n\", date.Year(), date.Month(), date.Day(), date.Unix())\n    // }\n    try testing.expectEqual(Date{ .year = 2438, .month = 8, .day = 8 }, (try DateTime.fromUnix(14787635606, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1290, .month = 10, .day = 9 }, (try DateTime.fromUnix(-21434368940, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2769, .month = 12, .day = 3 }, (try DateTime.fromUnix(25243136028, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1437, .month = 6, .day = 30 }, (try DateTime.fromUnix(-16804239664, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2752, .month = 4, .day = 7 }, (try DateTime.fromUnix(24685876670, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1484, .month = 1, .day = 29 }, (try DateTime.fromUnix(-15334209737, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2300, .month = 1, .day = 4 }, (try DateTime.fromUnix(10414107497, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1520, .month = 3, .day = 27 }, (try DateTime.fromUnix(-14193188705, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2628, .month = 11, .day = 21 }, (try DateTime.fromUnix(20792540664, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1807, .month = 2, .day = 21 }, (try DateTime.fromUnix(-5139411928, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2249, .month = 12, .day = 12 }, (try DateTime.fromUnix(8834245007, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1694, .month = 11, .day = 17 }, (try DateTime.fromUnix(-8681990253, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2725, .month = 6, .day = 10 }, (try DateTime.fromUnix(23839369640, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1947, .month = 2, .day = 16 }, (try DateTime.fromUnix(-721811319, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2293, .month = 9, .day = 28 }, (try DateTime.fromUnix(10216323340, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1614, .month = 8, .day = 12 }, (try DateTime.fromUnix(-11214942944, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2923, .month = 6, .day = 24 }, (try DateTime.fromUnix(30088834422, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1120, .month = 4, .day = 16 }, (try DateTime.fromUnix(-26814276389, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2035, .month = 12, .day = 9 }, (try DateTime.fromUnix(2080850037, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1167, .month = 1, .day = 15 }, (try DateTime.fromUnix(-25338977309, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2665, .month = 4, .day = 15 }, (try DateTime.fromUnix(21941133655, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1375, .month = 6, .day = 18 }, (try DateTime.fromUnix(-18761787336, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2189, .month = 6, .day = 13 }, (try DateTime.fromUnix(6925211914, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1938, .month = 1, .day = 12 }, (try DateTime.fromUnix(-1008879186, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2556, .month = 6, .day = 9 }, (try DateTime.fromUnix(18506255391, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1294, .month = 10, .day = 29 }, (try DateTime.fromUnix(-21306371902, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2330, .month = 3, .day = 19 }, (try DateTime.fromUnix(11367189469, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1696, .month = 5, .day = 22 }, (try DateTime.fromUnix(-8634251099, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2759, .month = 5, .day = 14 }, (try DateTime.fromUnix(24909971092, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1641, .month = 1, .day = 31 }, (try DateTime.fromUnix(-10379518549, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2451, .month = 6, .day = 26 }, (try DateTime.fromUnix(15194147684, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1962, .month = 1, .day = 4 }, (try DateTime.fromUnix(-252197440, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2883, .month = 11, .day = 15 }, (try DateTime.fromUnix(28839089617, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1587, .month = 8, .day = 5 }, (try DateTime.fromUnix(-12067604792, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2724, .month = 5, .day = 28 }, (try DateTime.fromUnix(23806729201, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1043, .month = 2, .day = 25 }, (try DateTime.fromUnix(-29248487174, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2927, .month = 3, .day = 9 }, (try DateTime.fromUnix(30205844459, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1451, .month = 6, .day = 16 }, (try DateTime.fromUnix(-16363722083, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2145, .month = 1, .day = 21 }, (try DateTime.fromUnix(5524305523, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1497, .month = 10, .day = 31 }, (try DateTime.fromUnix(-14900125085, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2162, .month = 4, .day = 1 }, (try DateTime.fromUnix(6066812142, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1738, .month = 8, .day = 12 }, (try DateTime.fromUnix(-7301852750, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2100, .month = 2, .day = 7 }, (try DateTime.fromUnix(4105665807, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1847, .month = 9, .day = 29 }, (try DateTime.fromUnix(-3858020808, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2370, .month = 9, .day = 19 }, (try DateTime.fromUnix(12645416176, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1292, .month = 7, .day = 8 }, (try DateTime.fromUnix(-21379166225, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2931, .month = 12, .day = 19 }, (try DateTime.fromUnix(30356691249, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1064, .month = 5, .day = 12 }, (try DateTime.fromUnix(-28579189254, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2295, .month = 5, .day = 13 }, (try DateTime.fromUnix(10267494406, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1449, .month = 12, .day = 4 }, (try DateTime.fromUnix(-16411941423, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2565, .month = 1, .day = 16 }, (try DateTime.fromUnix(18777760055, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1968, .month = 6, .day = 25 }, (try DateTime.fromUnix(-47882241, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2817, .month = 5, .day = 9 }, (try DateTime.fromUnix(26739900891, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1334, .month = 7, .day = 16 }, (try DateTime.fromUnix(-20053254809, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2945, .month = 4, .day = 24 }, (try DateTime.fromUnix(30777844895, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1930, .month = 2, .day = 27 }, (try DateTime.fromUnix(-1257362995, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2768, .month = 10, .day = 19 }, (try DateTime.fromUnix(25207675701, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1372, .month = 6, .day = 12 }, (try DateTime.fromUnix(-18856904218, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2603, .month = 8, .day = 29 }, (try DateTime.fromUnix(19996315706, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1201, .month = 4, .day = 7 }, (try DateTime.fromUnix(-24258926407, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2466, .month = 4, .day = 16 }, (try DateTime.fromUnix(15661407305, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1513, .month = 5, .day = 7 }, (try DateTime.fromUnix(-14410616341, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2619, .month = 9, .day = 11 }, (try DateTime.fromUnix(20502308837, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1501, .month = 5, .day = 13 }, (try DateTime.fromUnix(-14788768973, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2765, .month = 11, .day = 19 }, (try DateTime.fromUnix(25115683551, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1881, .month = 2, .day = 9 }, (try DateTime.fromUnix(-2805094638, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2253, .month = 4, .day = 28 }, (try DateTime.fromUnix(8940802800, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1941, .month = 11, .day = 23 }, (try DateTime.fromUnix(-886973505, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2565, .month = 1, .day = 18 }, (try DateTime.fromUnix(18777963967, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1313, .month = 5, .day = 20 }, (try DateTime.fromUnix(-20720877804, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2401, .month = 5, .day = 6 }, (try DateTime.fromUnix(13611949193, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1146, .month = 11, .day = 2 }, (try DateTime.fromUnix(-25976564837, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2115, .month = 6, .day = 11 }, (try DateTime.fromUnix(4589719542, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1276, .month = 8, .day = 1 }, (try DateTime.fromUnix(-21882043432, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2224, .month = 4, .day = 26 }, (try DateTime.fromUnix(8025468043, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1336, .month = 6, .day = 19 }, (try DateTime.fromUnix(-19992405201, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2717, .month = 5, .day = 5 }, (try DateTime.fromUnix(23583761778, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1222, .month = 3, .day = 15 }, (try DateTime.fromUnix(-23598239244, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2841, .month = 8, .day = 29 }, (try DateTime.fromUnix(27506984246, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1818, .month = 7, .day = 28 }, (try DateTime.fromUnix(-4778656923, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2533, .month = 5, .day = 13 }, (try DateTime.fromUnix(17778031068, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1146, .month = 7, .day = 28 }, (try DateTime.fromUnix(-25984946441, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2451, .month = 2, .day = 1 }, (try DateTime.fromUnix(15181688532, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1091, .month = 8, .day = 28 }, (try DateTime.fromUnix(-27717880960, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2168, .month = 4, .day = 12 }, (try DateTime.fromUnix(6257133476, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1718, .month = 10, .day = 16 }, (try DateTime.fromUnix(-7927438165, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2614, .month = 8, .day = 21 }, (try DateTime.fromUnix(20342724001, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1869, .month = 5, .day = 4 }, (try DateTime.fromUnix(-3176499822, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2504, .month = 4, .day = 20 }, (try DateTime.fromUnix(16860953121, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1401, .month = 5, .day = 2 }, (try DateTime.fromUnix(-17945432544, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2467, .month = 8, .day = 2 }, (try DateTime.fromUnix(15702325347, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1654, .month = 3, .day = 12 }, (try DateTime.fromUnix(-9965864717, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2371, .month = 9, .day = 2 }, (try DateTime.fromUnix(12675412066, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1784, .month = 1, .day = 16 }, (try DateTime.fromUnix(-5868249970, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2907, .month = 8, .day = 25 }, (try DateTime.fromUnix(29589265328, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 987, .month = 4, .day = 9 }, (try DateTime.fromUnix(-31011963272, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1980, .month = 10, .day = 19 }, (try DateTime.fromUnix(340838803, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1386, .month = 5, .day = 18 }, (try DateTime.fromUnix(-18417299412, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 2622, .month = 2, .day = 5 }, (try DateTime.fromUnix(20578157994, .seconds)).date());\n    try testing.expectEqual(Date{ .year = 1056, .month = 11, .day = 6 }, (try DateTime.fromUnix(-28816263601, .seconds)).date());\n}\n\ntest \"DateTime: time\" {\n    // GO:\n    // for i := 0; i < 100; i++ {\n    //   us := rand.Int63n(31536000000000000)\n    //   if i%2 == 1 {\n    //     us = -us\n    //   }\n    //   date := time.UnixMicro(us).UTC()\n    //   fmt.Printf(\"\\ttry testing.expectEqual(Time{.hour = %d, .min = %d, .sec = %d, .micros = %d}, (try DateTime.fromUnix(%d, .microseconds)).time());\\n\", date.Hour(), date.Minute(), date.Second(), date.Nanosecond()/1000, us)\n    // }\n    try testing.expectEqual(Time{ .hour = 18, .min = 56, .sec = 18, .micros = 38399 }, (try DateTime.fromUnix(6940752978038399, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 14, .min = 10, .sec = 48, .micros = 481799 }, (try DateTime.fromUnix(-15037004951518201, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 13, .min = 49, .sec = 27, .micros = 814723 }, (try DateTime.fromUnix(26507483367814723, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 3, .min = 53, .sec = 47, .micros = 990825 }, (try DateTime.fromUnix(-15290625972009175, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 9, .min = 28, .sec = 54, .micros = 16606 }, (try DateTime.fromUnix(28046078934016606, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 36, .sec = 38, .micros = 380600 }, (try DateTime.fromUnix(-8638640601619400, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 29, .sec = 27, .micros = 109527 }, (try DateTime.fromUnix(26649192567109527, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 23, .min = 54, .sec = 48, .micros = 10233 }, (try DateTime.fromUnix(-24667200311989767, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 44, .sec = 50, .micros = 913226 }, (try DateTime.fromUnix(22200932690913226, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 36, .sec = 19, .micros = 337687 }, (try DateTime.fromUnix(-13186952620662313, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 20, .min = 6, .sec = 37, .micros = 157270 }, (try DateTime.fromUnix(17827416397157270, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 4, .min = 43, .sec = 33, .micros = 871331 }, (try DateTime.fromUnix(-15558635786128669, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 0, .min = 26, .sec = 54, .micros = 557236 }, (try DateTime.fromUnix(23322644814557236, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 7, .min = 38, .sec = 40, .micros = 370732 }, (try DateTime.fromUnix(-1368030079629268, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 2, .min = 31, .sec = 9, .micros = 223691 }, (try DateTime.fromUnix(20164386669223691, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 12, .min = 41, .sec = 23, .micros = 165207 }, (try DateTime.fromUnix(-20761960716834793, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 0, .min = 46, .sec = 49, .micros = 962075 }, (try DateTime.fromUnix(549247609962075, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 2, .min = 7, .sec = 12, .micros = 984678 }, (try DateTime.fromUnix(-11643688367015322, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 11, .min = 32, .sec = 16, .micros = 343799 }, (try DateTime.fromUnix(4022998336343799, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 26, .sec = 54, .micros = 366277 }, (try DateTime.fromUnix(-8557597985633723, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 16, .min = 1, .sec = 4, .micros = 485152 }, (try DateTime.fromUnix(15070896064485152, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 4, .min = 14, .sec = 18, .micros = 923558 }, (try DateTime.fromUnix(-15995389541076442, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 37, .sec = 58, .micros = 948826 }, (try DateTime.fromUnix(16828148278948826, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 6, .min = 52, .sec = 27, .micros = 1770 }, (try DateTime.fromUnix(-30509975252998230, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 0, .min = 32, .sec = 28, .micros = 381047 }, (try DateTime.fromUnix(7813499548381047, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 14, .min = 1, .sec = 49, .micros = 267686 }, (try DateTime.fromUnix(-14265712690732314, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 4, .min = 53, .sec = 23, .micros = 233239 }, (try DateTime.fromUnix(31107646403233239, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 3, .min = 0, .sec = 53, .micros = 292242 }, (try DateTime.fromUnix(-10317099546707758, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 8, .min = 22, .sec = 13, .micros = 966628 }, (try DateTime.fromUnix(11215959733966628, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 32, .sec = 22, .micros = 779813 }, (try DateTime.fromUnix(-15711949657220187, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 1, .min = 6, .sec = 36, .micros = 405828 }, (try DateTime.fromUnix(6872691996405828, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 12, .min = 0, .sec = 55, .micros = 420129 }, (try DateTime.fromUnix(-31068273544579871, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 17, .sec = 6, .micros = 930158 }, (try DateTime.fromUnix(26304473826930158, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 12, .min = 45, .sec = 25, .micros = 203619 }, (try DateTime.fromUnix(-5358482074796381, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 19, .min = 28, .sec = 0, .micros = 476749 }, (try DateTime.fromUnix(9134623680476749, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 11, .min = 58, .sec = 41, .micros = 864572 }, (try DateTime.fromUnix(-29314353678135428, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 6, .min = 19, .sec = 27, .micros = 566937 }, (try DateTime.fromUnix(9005494767566937, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 9, .min = 3, .sec = 17, .micros = 164061 }, (try DateTime.fromUnix(-24631052202835939, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 23, .min = 2, .sec = 41, .micros = 147703 }, (try DateTime.fromUnix(27754959761147703, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 16, .min = 51, .sec = 1, .micros = 710888 }, (try DateTime.fromUnix(-29839475338289112, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 1, .min = 31, .sec = 44, .micros = 244667 }, (try DateTime.fromUnix(13143000704244667, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 14, .min = 40, .sec = 45, .micros = 594500 }, (try DateTime.fromUnix(-27029323154405500, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 3, .min = 28, .sec = 18, .micros = 941443 }, (try DateTime.fromUnix(26929337298941443, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 18, .min = 34, .sec = 26, .micros = 418287 }, (try DateTime.fromUnix(-16849401933581713, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 16, .min = 51, .sec = 12, .micros = 390293 }, (try DateTime.fromUnix(24013471872390293, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 27, .sec = 59, .micros = 116472 }, (try DateTime.fromUnix(-4881839520883528, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 38, .sec = 58, .micros = 829840 }, (try DateTime.fromUnix(28012689538829840, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 13, .min = 31, .sec = 51, .micros = 397163 }, (try DateTime.fromUnix(-14000034488602837, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 16, .min = 25, .sec = 36, .micros = 566333 }, (try DateTime.fromUnix(3819630336566333, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 23, .min = 52, .sec = 35, .micros = 404576 }, (try DateTime.fromUnix(-24790838844595424, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 14, .min = 17, .sec = 56, .micros = 248627 }, (try DateTime.fromUnix(4303462676248627, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 56, .sec = 31, .micros = 445770 }, (try DateTime.fromUnix(-7573827808554230, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 1, .min = 36, .sec = 32, .micros = 60901 }, (try DateTime.fromUnix(12791180192060901, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 4, .min = 12, .sec = 1, .micros = 816276 }, (try DateTime.fromUnix(-29726596078183724, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 25, .sec = 2, .micros = 88680 }, (try DateTime.fromUnix(9072494702088680, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 7, .min = 14, .sec = 18, .micros = 149127 }, (try DateTime.fromUnix(-20968821941850873, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 15, .min = 45, .sec = 55, .micros = 818121 }, (try DateTime.fromUnix(14590424755818121, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 13, .min = 45, .sec = 5, .micros = 544234 }, (try DateTime.fromUnix(-21099694494455766, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 20, .min = 58, .sec = 32, .micros = 361661 }, (try DateTime.fromUnix(27070837112361661, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 18, .min = 42, .sec = 3, .micros = 375293 }, (try DateTime.fromUnix(-22783699076624707, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 15, .min = 5, .sec = 18, .micros = 844868 }, (try DateTime.fromUnix(3924515118844868, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 39, .sec = 15, .micros = 454348 }, (try DateTime.fromUnix(-19519510844545652, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 34, .sec = 57, .micros = 584438 }, (try DateTime.fromUnix(25405223697584438, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 12, .min = 58, .sec = 48, .micros = 604253 }, (try DateTime.fromUnix(-23848167671395747, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 21, .min = 6, .sec = 10, .micros = 130143 }, (try DateTime.fromUnix(9179039170130143, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 11, .min = 40, .sec = 45, .micros = 806457 }, (try DateTime.fromUnix(-10457900354193543, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 32, .sec = 3, .micros = 84471 }, (try DateTime.fromUnix(20206560723084471, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 11, .min = 8, .sec = 48, .micros = 571978 }, (try DateTime.fromUnix(-13147966271428022, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 10, .min = 37, .sec = 9, .micros = 847397 }, (try DateTime.fromUnix(9639599829847397, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 20, .min = 15, .sec = 37, .micros = 731453 }, (try DateTime.fromUnix(-17972509462268547, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 0, .min = 36, .sec = 51, .micros = 658834 }, (try DateTime.fromUnix(23080639011658834, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 3, .min = 6, .sec = 2, .micros = 359939 }, (try DateTime.fromUnix(-13484004837640061, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 1, .min = 24, .sec = 8, .micros = 76822 }, (try DateTime.fromUnix(22642161848076822, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 20, .sec = 47, .micros = 940649 }, (try DateTime.fromUnix(-9576815952059351, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 16, .min = 19, .sec = 30, .micros = 228423 }, (try DateTime.fromUnix(11237847570228423, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 16, .min = 54, .sec = 33, .micros = 913828 }, (try DateTime.fromUnix(-9146156726086172, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 20, .min = 14, .sec = 10, .micros = 663120 }, (try DateTime.fromUnix(12400805650663120, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 15, .min = 22, .sec = 22, .micros = 500411 }, (try DateTime.fromUnix(-13183893457499589, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 18, .min = 42, .sec = 11, .micros = 637021 }, (try DateTime.fromUnix(17415888131637021, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 7, .sec = 43, .micros = 497651 }, (try DateTime.fromUnix(-3828045136502349, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 9, .min = 25, .sec = 22, .micros = 960397 }, (try DateTime.fromUnix(25585406722960397, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 20, .min = 36, .sec = 31, .micros = 312572 }, (try DateTime.fromUnix(-11209202608687428, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 5, .min = 25, .sec = 18, .micros = 104173 }, (try DateTime.fromUnix(7748544318104173, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 11, .min = 23, .sec = 25, .micros = 504363 }, (try DateTime.fromUnix(-22111446994495637, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 19, .min = 48, .sec = 44, .micros = 703684 }, (try DateTime.fromUnix(21347696924703684, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 12, .min = 10, .sec = 21, .micros = 67035 }, (try DateTime.fromUnix(-29976004178932965, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 6, .min = 0, .sec = 55, .micros = 355102 }, (try DateTime.fromUnix(15622869655355102, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 21, .min = 12, .sec = 1, .micros = 574873 }, (try DateTime.fromUnix(-28386384478425127, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 29, .sec = 45, .micros = 886627 }, (try DateTime.fromUnix(27787703385886627, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 8, .min = 43, .sec = 51, .micros = 403514 }, (try DateTime.fromUnix(-591981368596486, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 12, .min = 1, .sec = 19, .micros = 667089 }, (try DateTime.fromUnix(411998479667089, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 14, .min = 15, .sec = 53, .micros = 366760 }, (try DateTime.fromUnix(-29916899046633240, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 19, .min = 31, .sec = 23, .micros = 639485 }, (try DateTime.fromUnix(29847555083639485, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 0, .min = 21, .sec = 29, .micros = 207122 }, (try DateTime.fromUnix(-13356229110792878, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 10, .min = 35, .sec = 51, .micros = 789976 }, (try DateTime.fromUnix(2401353351789976, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 23, .min = 51, .sec = 4, .micros = 23674 }, (try DateTime.fromUnix(-8687002135976326, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 3, .min = 23, .sec = 21, .micros = 985741 }, (try DateTime.fromUnix(7637772201985741, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 22, .min = 3, .sec = 34, .micros = 497666 }, (try DateTime.fromUnix(-22331814985502334, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 15, .sec = 11, .micros = 818441 }, (try DateTime.fromUnix(14544983711818441, .microseconds)).time());\n    try testing.expectEqual(Time{ .hour = 17, .min = 47, .sec = 39, .micros = 303089 }, (try DateTime.fromUnix(-19977775940696911, .microseconds)).time());\n}\n\ntest \"DateTime: parse RFC822\" {\n    // try testing.expectError(error.InvalidDateTime, DateTime.parse(\"\", .rfc822));\n    // try testing.expectError(error.InvalidDateTime, DateTime.parse(\"nope\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Oth, 01 Jan 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Mon , 01 Jan 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Mon,  01 Jan 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\" Mon, 1 Jan 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Wed, 1 Jan 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Wed, 01  Jan 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Wed, 01 J 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Wed, 01 Ja 20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Wed, 01 Jan 2 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"Wed, 01 Jan  20 10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20  10:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 1:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 a:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 1a:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 200:10 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:001 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:a Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a: Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:1 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:a Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:999 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:999 Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:22\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:22  Z\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:22 X\", .rfc822));\n    try testing.expectError(error.InvalidTime, DateTime.parse(\"Wed, 01 Jan 20 20:1a:22 ZZ\", .rfc822));\n\n    {\n        const dt = try DateTime.parse(\"31 Dec 68 23:59 Z\", .rfc822);\n        try testing.expectEqual(3124223940000000, dt.micros);\n        try testing.expectEqual(2068, dt.date().year);\n        try testing.expectEqual(12, dt.date().month);\n        try testing.expectEqual(31, dt.date().day);\n        try testing.expectEqual(23, dt.time().hour);\n        try testing.expectEqual(59, dt.time().min);\n        try testing.expectEqual(0, dt.time().sec);\n        try testing.expectEqual(0, dt.time().micros);\n    }\n\n    {\n        const dt = try DateTime.parse(\"Mon, 31 Dec 68 23:59 Z\", .rfc822);\n        try testing.expectEqual(3124223940000000, dt.micros);\n        try testing.expectEqual(2068, dt.date().year);\n        try testing.expectEqual(12, dt.date().month);\n        try testing.expectEqual(31, dt.date().day);\n        try testing.expectEqual(23, dt.time().hour);\n        try testing.expectEqual(59, dt.time().min);\n        try testing.expectEqual(0, dt.time().sec);\n        try testing.expectEqual(0, dt.time().micros);\n    }\n\n    {\n        const dt = try DateTime.parse(\"01 Jan 69 01:22:03 GMT\", .rfc822);\n        try testing.expectEqual(-31531077000000, dt.micros);\n        try testing.expectEqual(1969, dt.date().year);\n        try testing.expectEqual(1, dt.date().month);\n        try testing.expectEqual(1, dt.date().day);\n        try testing.expectEqual(1, dt.time().hour);\n        try testing.expectEqual(22, dt.time().min);\n        try testing.expectEqual(3, dt.time().sec);\n        try testing.expectEqual(0, dt.time().micros);\n    }\n\n    {\n        const dt = try DateTime.parse(\"Sat, 18 Jan 2070 01:22:03 GMT\", .rfc822);\n        try testing.expectEqual(3157233723000000, dt.micros);\n        try testing.expectEqual(2070, dt.date().year);\n        try testing.expectEqual(1, dt.date().month);\n        try testing.expectEqual(18, dt.date().day);\n        try testing.expectEqual(1, dt.time().hour);\n        try testing.expectEqual(22, dt.time().min);\n        try testing.expectEqual(3, dt.time().sec);\n        try testing.expectEqual(0, dt.time().micros);\n    }\n}\n\ntest \"DateTime: parse RFC3339\" {\n    {\n        const dt = try DateTime.parse(\"-3221-01-02T03:04:05Z\", .rfc3339);\n        try testing.expectEqual(-163812056155000000, dt.micros);\n        try testing.expectEqual(-3221, dt.date().year);\n        try testing.expectEqual(1, dt.date().month);\n        try testing.expectEqual(2, dt.date().day);\n        try testing.expectEqual(3, dt.time().hour);\n        try testing.expectEqual(4, dt.time().min);\n        try testing.expectEqual(5, dt.time().sec);\n        try testing.expectEqual(0, dt.time().micros);\n    }\n\n    {\n        const dt = try DateTime.parse(\"0001-02-03T04:05:06.789+00:00\", .rfc3339);\n        try testing.expectEqual(-62132730893211000, dt.micros);\n        try testing.expectEqual(1, dt.date().year);\n        try testing.expectEqual(2, dt.date().month);\n        try testing.expectEqual(3, dt.date().day);\n        try testing.expectEqual(4, dt.time().hour);\n        try testing.expectEqual(5, dt.time().min);\n        try testing.expectEqual(6, dt.time().sec);\n        try testing.expectEqual(789000, dt.time().micros);\n    }\n\n    {\n        const dt = try DateTime.parse(\"5000-12-31T23:59:58.987654321Z\", .rfc3339);\n        try testing.expectEqual(95649119998987654, dt.micros);\n        try testing.expectEqual(5000, dt.date().year);\n        try testing.expectEqual(12, dt.date().month);\n        try testing.expectEqual(31, dt.date().day);\n        try testing.expectEqual(23, dt.time().hour);\n        try testing.expectEqual(59, dt.time().min);\n        try testing.expectEqual(58, dt.time().sec);\n        try testing.expectEqual(987654, dt.time().micros);\n    }\n\n    {\n        // invalid format\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023/01-02T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-01/02T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDateTime, DateTime.parse(\"0001-01-01 T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDateTime, DateTime.parse(\"0001-01-01t00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDateTime, DateTime.parse(\"0001-01-01 00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-1-02T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-01-2T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"9-01-2T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"99-01-2T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"999-01-2T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"-999-01-2T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"-1-01-2T00:00Z\", .rfc3339));\n    }\n\n    // date portion is ISO8601\n    try testing.expectError(error.InvalidDate, DateTime.parse(\"20230102T23:59:58.987654321Z\", .rfc3339));\n\n    {\n        // invalid month\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-00-22T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-0A-22T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-13-22T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-99-22T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"-2023-00-22T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"-2023-13-22T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"-2023-99-22T00:00Z\", .rfc3339));\n    }\n\n    {\n        // invalid day\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-01-00T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-01-32T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-02-29T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-03-32T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-04-31T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-05-32T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-06-31T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-07-32T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-08-32T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-09-31T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-10-32T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-11-31T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2023-12-32T00:00Z\", .rfc3339));\n    }\n\n    {\n        // valid (max day)\n        try testing.expectEqual(1675123200000000, (try DateTime.parse(\"2023-01-31T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1677542400000000, (try DateTime.parse(\"2023-02-28T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1680220800000000, (try DateTime.parse(\"2023-03-31T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1682812800000000, (try DateTime.parse(\"2023-04-30T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1685491200000000, (try DateTime.parse(\"2023-05-31T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1688083200000000, (try DateTime.parse(\"2023-06-30T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1690761600000000, (try DateTime.parse(\"2023-07-31T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1693440000000000, (try DateTime.parse(\"2023-08-31T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1696032000000000, (try DateTime.parse(\"2023-09-30T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1698710400000000, (try DateTime.parse(\"2023-10-31T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1701302400000000, (try DateTime.parse(\"2023-11-30T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1703980800000000, (try DateTime.parse(\"2023-12-31T00:00Z\", .rfc3339)).micros);\n    }\n\n    {\n        // leap years\n        try testing.expectEqual(951782400000000, (try DateTime.parse(\"2000-02-29T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(13574563200000000, (try DateTime.parse(\"2400-02-29T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1330473600000000, (try DateTime.parse(\"2012-02-29T00:00Z\", .rfc3339)).micros);\n        try testing.expectEqual(1709164800000000, (try DateTime.parse(\"2024-02-29T00:00Z\", .rfc3339)).micros);\n\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2000-02-30T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2400-02-30T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2012-02-30T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2024-02-30T00:00Z\", .rfc3339));\n\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2100-02-29T00:00Z\", .rfc3339));\n        try testing.expectError(error.InvalidDate, DateTime.parse(\"2200-02-29T00:00Z\", .rfc3339));\n    }\n\n    {\n        // invalid time\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T01:00:\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T1:00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T10:1:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T10:11:4\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T10:20:30.\", .rfc3339));\n        try testing.expectError(error.InvalidDateTime, DateTime.parse(\"2023-10-10T10:20:30.a\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T10:20:30.1234567899\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T24:00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T00:60:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T00:00:60\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T0a:00:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T00:0a:00\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T00:00:0a\", .rfc3339));\n        try testing.expectError(error.InvalidTime, DateTime.parse(\"2023-10-10T00/00:00\", .rfc3339));\n        try testing.expectError(error.InvalidDateTime, DateTime.parse(\"2023-10-10T00:00 00\", .rfc3339));\n    }\n}\n\ntest \"DateTime: json\" {\n    {\n        // DateTime, time no fraction\n        const dt = try DateTime.parse(\"2023-09-22T23:59:02Z\", .rfc3339);\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, dt, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"2023-09-22T23:59:02Z\\\"\", out);\n    }\n\n    {\n        // time, milliseconds only\n        const dt = try DateTime.parse(\"2023-09-22T07:09:32.202Z\", .rfc3339);\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, dt, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"2023-09-22T07:09:32.202Z\\\"\", out);\n    }\n\n    {\n        // time, micros\n        const dt = try DateTime.parse(\"-0004-12-03T01:02:03.123456Z\", .rfc3339);\n        const out = try std.json.Stringify.valueAlloc(testing.allocator, dt, .{});\n        defer testing.allocator.free(out);\n        try testing.expectString(\"\\\"-0004-12-03T01:02:03.123456Z\\\"\", out);\n    }\n\n    {\n        // parse\n        const ts = try std.json.parseFromSlice(TestStruct, testing.allocator, \"{\\\"datetime\\\":\\\"2023-09-22T07:09:32.202Z\\\"}\", .{});\n        defer ts.deinit();\n        try testing.expectEqual(try DateTime.parse(\"2023-09-22T07:09:32.202Z\", .rfc3339), ts.value.datetime.?);\n    }\n}\n\ntest \"DateTime: format\" {\n    {\n        var buf: [30]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{try DateTime.initUTC(2023, 5, 22, 23, 59, 59, 0)});\n        try testing.expectString(\"2023-05-22T23:59:59Z\", out);\n    }\n\n    {\n        var buf: [30]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{try DateTime.initUTC(2023, 5, 22, 8, 9, 10, 12)});\n        try testing.expectString(\"2023-05-22T08:09:10.000012Z\", out);\n    }\n\n    {\n        var buf: [30]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{try DateTime.initUTC(2023, 5, 22, 8, 9, 10, 123)});\n        try testing.expectString(\"2023-05-22T08:09:10.000123Z\", out);\n    }\n\n    {\n        var buf: [30]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{try DateTime.initUTC(2023, 5, 22, 8, 9, 10, 1234)});\n        try testing.expectString(\"2023-05-22T08:09:10.001234Z\", out);\n    }\n\n    {\n        var buf: [30]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{try DateTime.initUTC(-102, 12, 9, 8, 9, 10, 12345)});\n        try testing.expectString(\"-0102-12-09T08:09:10.012345Z\", out);\n    }\n\n    {\n        var buf: [30]u8 = undefined;\n        const out = try std.fmt.bufPrint(&buf, \"{f}\", .{try DateTime.initUTC(-102, 12, 9, 8, 9, 10, 123456)});\n        try testing.expectString(\"-0102-12-09T08:09:10.123456Z\", out);\n    }\n}\n\ntest \"DateTime: order\" {\n    {\n        const a = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002);\n        const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002);\n        try testing.expectEqual(std.math.Order.eq, a.order(b));\n    }\n\n    {\n        const a = try DateTime.initUTC(2023, 5, 22, 12, 59, 2, 492);\n        const b = try DateTime.initUTC(2022, 5, 22, 23, 59, 2, 492);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = try DateTime.initUTC(2022, 6, 22, 23, 59, 2, 492);\n        const b = try DateTime.initUTC(2022, 5, 22, 23, 33, 2, 492);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = try DateTime.initUTC(2023, 5, 23, 23, 59, 2, 492);\n        const b = try DateTime.initUTC(2022, 5, 22, 23, 59, 11, 492);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = try DateTime.initUTC(2023, 11, 23, 20, 17, 22, 101002);\n        const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = try DateTime.initUTC(2023, 11, 23, 19, 18, 22, 101002);\n        const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = try DateTime.initUTC(2023, 11, 23, 19, 17, 23, 101002);\n        const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n\n    {\n        const a = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101003);\n        const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002);\n        try testing.expectEqual(std.math.Order.gt, a.order(b));\n        try testing.expectEqual(std.math.Order.lt, b.order(a));\n    }\n}\n\ntest \"DateTime: unix\" {\n    {\n        const dt = try DateTime.initUTC(-4322, 1, 1, 0, 0, 0, 0);\n        try testing.expectEqual(-198556272000, dt.unix(.seconds));\n        try testing.expectEqual(-198556272000000, dt.unix(.milliseconds));\n        try testing.expectEqual(-198556272000000000, dt.unix(.microseconds));\n    }\n\n    {\n        const dt = try DateTime.initUTC(1970, 1, 1, 0, 0, 0, 0);\n        try testing.expectEqual(0, dt.unix(.seconds));\n        try testing.expectEqual(0, dt.unix(.milliseconds));\n        try testing.expectEqual(0, dt.unix(.microseconds));\n    }\n\n    {\n        const dt = try DateTime.initUTC(2023, 11, 24, 12, 6, 14, 918000);\n        try testing.expectEqual(1700827574, dt.unix(.seconds));\n        try testing.expectEqual(1700827574918, dt.unix(.milliseconds));\n        try testing.expectEqual(1700827574918000, dt.unix(.microseconds));\n    }\n\n    // microseconds\n    // GO:\n    // for i := 0; i < 50; i++ {\n    //   us := rand.Int63n(3153600000000000)\n    //   if i%2 == 1 {\n    //     us = -us\n    //   }\n    //   date := time.UnixMicro(us).UTC()\n    //   fmt.Printf(\"\\ttry testing.expectEqual(%d, (try DateTime.parse(\\\"%s\\\", .rfc3339)).unix(.microseconds));\\n\", us, date.Format(time.RFC3339Nano))\n    // }\n    try testing.expectEqual(2568689002670356, (try DateTime.parse(\"2051-05-26T04:43:22.670356Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2994122503199268, (try DateTime.parse(\"1875-02-13T19:18:16.800732Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2973860981156244, (try DateTime.parse(\"2064-03-27T16:29:41.156244Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2122539648627924, (try DateTime.parse(\"1902-09-28T13:39:11.372076Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1440540448439442, (try DateTime.parse(\"2015-08-25T22:07:28.439442Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-843471236299718, (try DateTime.parse(\"1943-04-10T14:26:03.700282Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2428009970341301, (try DateTime.parse(\"2046-12-09T23:12:50.341301Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-861640488391156, (try DateTime.parse(\"1942-09-12T07:25:11.608844Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(107457228254516, (try DateTime.parse(\"1973-05-28T17:13:48.254516Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-858997335483954, (try DateTime.parse(\"1942-10-12T21:37:44.516046Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1879201014676957, (try DateTime.parse(\"2029-07-20T00:16:54.676957Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2779215184508509, (try DateTime.parse(\"1881-12-06T03:46:55.491491Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(790920073212180, (try DateTime.parse(\"1995-01-24T04:01:13.21218Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-1986764905311346, (try DateTime.parse(\"1907-01-17T00:51:34.688654Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1567001594851223, (try DateTime.parse(\"2019-08-28T14:13:14.851223Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2786308994565191, (try DateTime.parse(\"1881-09-15T01:16:45.434809Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1190930851203854, (try DateTime.parse(\"2007-09-27T22:07:31.203854Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-13894507787609, (try DateTime.parse(\"1969-07-24T04:24:52.212391Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1283185581222987, (try DateTime.parse(\"2010-08-30T16:26:21.222987Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-3080071240438154, (try DateTime.parse(\"1872-05-25T00:39:19.561846Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(3091078494301752, (try DateTime.parse(\"2067-12-14T08:54:54.301752Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2788286096253476, (try DateTime.parse(\"1881-08-23T04:05:03.746524Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1226140349962650, (try DateTime.parse(\"2008-11-08T10:32:29.96265Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-173789078990530, (try DateTime.parse(\"1964-06-29T13:15:21.00947Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2202006978733437, (try DateTime.parse(\"2039-10-12T04:36:18.733437Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-1957390566907891, (try DateTime.parse(\"1907-12-23T00:23:53.092109Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2704228013874812, (try DateTime.parse(\"2055-09-10T22:26:53.874812Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2162891323622724, (try DateTime.parse(\"1901-06-18T12:51:16.377276Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2985526644225853, (try DateTime.parse(\"2064-08-09T16:57:24.225853Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2714126911982044, (try DateTime.parse(\"1883-12-29T11:51:28.017956Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1389358847381035, (try DateTime.parse(\"2014-01-10T13:00:47.381035Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2599632972496238, (try DateTime.parse(\"1887-08-15T15:43:47.503762Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2842567982275671, (try DateTime.parse(\"2060-01-29T02:13:02.275671Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2924719405531619, (try DateTime.parse(\"1877-04-27T01:56:34.468381Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(929389345478708, (try DateTime.parse(\"1999-06-14T19:42:25.478708Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2928161617689577, (try DateTime.parse(\"1877-03-18T05:46:22.310423Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1981926664387480, (try DateTime.parse(\"2032-10-20T23:11:04.38748Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-3077852548046313, (try DateTime.parse(\"1872-06-19T16:57:31.953687Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(323327680783683, (try DateTime.parse(\"1980-03-31T05:14:40.783683Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-1282955701919591, (try DateTime.parse(\"1929-05-06T23:24:58.080409Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(1382921217423641, (try DateTime.parse(\"2013-10-28T00:46:57.423641Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-1431006940775286, (try DateTime.parse(\"1924-08-27T10:04:19.224714Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(3074639946025509, (try DateTime.parse(\"2067-06-07T02:39:06.025509Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2634608860053384, (try DateTime.parse(\"1886-07-06T20:12:19.946616Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2779915686281386, (try DateTime.parse(\"2058-02-02T22:48:06.281386Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2016252325938190, (try DateTime.parse(\"1906-02-09T17:54:34.06181Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(342848400150959, (try DateTime.parse(\"1980-11-12T03:40:00.150959Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-2645960576992651, (try DateTime.parse(\"1886-02-25T10:57:03.007349Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(2460926767780856, (try DateTime.parse(\"2047-12-25T22:46:07.780856Z\", .rfc3339)).unix(.microseconds));\n    try testing.expectEqual(-3072719558320472, (try DateTime.parse(\"1872-08-18T02:47:21.679528Z\", .rfc3339)).unix(.microseconds));\n\n    // milliseconds\n    // GO\n    // for i := 0; i < 50; i++ {\n    //   us := rand.Int63n(3153600000000000)\n    //   if i%2 == 1 {\n    //     us = -us\n    //   }\n    //   date := time.UnixMicro(us).UTC()\n    //   fmt.Printf(\"\\ttry testing.expectEqual(%d, (try DateTime.parse(\\\"%s\\\", .rfc3339)).unix(.milliseconds));\\n\", us/1000, date.Format(time.RFC3339Nano))\n    // }\n    try testing.expectEqual(1397526377500, (try DateTime.parse(\"2014-04-15T01:46:17.500928Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-586731476093, (try DateTime.parse(\"1951-05-30T03:02:03.906951Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2626709817261, (try DateTime.parse(\"2053-03-27T17:36:57.261986Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2699459388451, (try DateTime.parse(\"1884-06-16T06:10:11.548899Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(187068511670, (try DateTime.parse(\"1975-12-06T03:28:31.670454Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-785593098555, (try DateTime.parse(\"1945-02-08T11:41:41.444519Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2482013929293, (try DateTime.parse(\"2048-08-26T00:18:49.293566Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-39404841784, (try DateTime.parse(\"1968-10-01T22:12:38.215367Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(1534769380821, (try DateTime.parse(\"2018-08-20T12:49:40.821612Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1980714497790, (try DateTime.parse(\"1907-03-28T01:31:42.209908Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(1981870811721, (try DateTime.parse(\"2032-10-20T07:40:11.721424Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-554657243269, (try DateTime.parse(\"1952-06-04T08:32:36.730587Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(78531146024, (try DateTime.parse(\"1972-06-27T22:12:26.024177Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2360798362731, (try DateTime.parse(\"1895-03-10T22:40:37.268319Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2843392029355, (try DateTime.parse(\"2060-02-07T15:07:09.355931Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1289360209568, (try DateTime.parse(\"1929-02-21T20:23:10.431793Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2440116994057, (try DateTime.parse(\"2047-04-29T02:16:34.057859Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1958937239211, (try DateTime.parse(\"1907-12-05T02:46:00.788847Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2092930144205, (try DateTime.parse(\"2036-04-27T17:29:04.205599Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1314934006371, (try DateTime.parse(\"1928-05-01T20:33:13.628366Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(1987707686213, (try DateTime.parse(\"2032-12-26T21:01:26.21383Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2863567343704, (try DateTime.parse(\"1879-04-04T20:37:36.295226Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(1776340450602, (try DateTime.parse(\"2026-04-16T11:54:10.602059Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-135109264096, (try DateTime.parse(\"1965-09-20T05:38:55.903281Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(664556549013, (try DateTime.parse(\"1991-01-22T15:02:29.013079Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1265741428742, (try DateTime.parse(\"1929-11-22T05:09:31.257333Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(677440942549, (try DateTime.parse(\"1991-06-20T18:02:22.549734Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-3086845293210, (try DateTime.parse(\"1872-03-07T14:58:26.789666Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2662366721158, (try DateTime.parse(\"2054-05-14T10:18:41.158507Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-35310777646, (try DateTime.parse(\"1968-11-18T07:27:02.353055Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(466748318057, (try DateTime.parse(\"1984-10-16T04:18:38.057985Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1142849776788, (try DateTime.parse(\"1933-10-14T13:43:43.211425Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(299657172861, (try DateTime.parse(\"1979-07-01T06:06:12.86151Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2674956599650, (try DateTime.parse(\"1885-03-26T20:30:00.34904Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2608306771546, (try DateTime.parse(\"2052-08-26T17:39:31.546441Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2890194900832, (try DateTime.parse(\"1878-05-31T16:04:59.167405Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(396552033685, (try DateTime.parse(\"1982-07-26T17:20:33.68525Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-107099840493, (try DateTime.parse(\"1966-08-10T10:02:39.506219Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(3003275118291, (try DateTime.parse(\"2065-03-03T03:05:18.291675Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1827348315834, (try DateTime.parse(\"1912-02-05T03:14:44.165534Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(276927903561, (try DateTime.parse(\"1978-10-11T04:25:03.561761Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2769749223625, (try DateTime.parse(\"1882-03-25T17:12:56.374223Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2626498021199, (try DateTime.parse(\"2053-03-25T06:47:01.199662Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-1394547124859, (try DateTime.parse(\"1925-10-23T09:47:55.140254Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(272330504585, (try DateTime.parse(\"1978-08-18T23:21:44.585364Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2210407675350, (try DateTime.parse(\"1899-12-15T13:52:04.649158Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(1506546882755, (try DateTime.parse(\"2017-09-27T21:14:42.755649Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-2320627977264, (try DateTime.parse(\"1896-06-17T21:07:02.735544Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(2719300156090, (try DateTime.parse(\"2056-03-03T09:09:16.090337Z\", .rfc3339)).unix(.milliseconds));\n    try testing.expectEqual(-450791776320, (try DateTime.parse(\"1955-09-19T12:03:43.679144Z\", .rfc3339)).unix(.milliseconds));\n\n    // seconds\n    // GO\n    // for i := 0; i < 50; i++ {\n    //   us := rand.Int63n(3153600000000000)\n    //   if i%2 == 1 {\n    //     us = -us\n    //   }\n    //   date := time.UnixMicro(us).UTC()\n    //   fmt.Printf(\"\\ttry testing.expectEqual(%d, (try DateTime.parse(\\\"%s\\\", .rfc3339)).unix(.milliseconds));\\n\", us/1000/1000, date.Format(time.RFC3339Nano))\n    // }\n    try testing.expectEqual(1019355037, (try DateTime.parse(\"2002-04-21T02:10:37.264298Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2639191098, (try DateTime.parse(\"1886-05-14T19:21:41.481076Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(552479765, (try DateTime.parse(\"1987-07-05T10:36:05.374475Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2842270449, (try DateTime.parse(\"1879-12-07T08:25:50.857157Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2287542812, (try DateTime.parse(\"2042-06-28T04:33:32.585424Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1032056861, (try DateTime.parse(\"1937-04-18T21:32:18.185245Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2294125759, (try DateTime.parse(\"2042-09-12T09:09:19.324234Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2434666174, (try DateTime.parse(\"1892-11-05T23:50:25.855342Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2130180824, (try DateTime.parse(\"2037-07-02T20:53:44.663679Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2088926942, (try DateTime.parse(\"1903-10-22T14:30:57.110159Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(210188161, (try DateTime.parse(\"1976-08-29T17:36:01.512348Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1594811550, (try DateTime.parse(\"1919-06-19T12:47:29.692995Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(408055212, (try DateTime.parse(\"1982-12-06T20:40:12.74791Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-763370385, (try DateTime.parse(\"1945-10-23T16:40:14.54824Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2220686606, (try DateTime.parse(\"2040-05-15T09:23:26.183323Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1829267394, (try DateTime.parse(\"1912-01-13T22:10:05.152891Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(186103622, (try DateTime.parse(\"1975-11-24T23:27:02.092278Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-104963797, (try DateTime.parse(\"1966-09-04T03:23:22.379643Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(188664629, (try DateTime.parse(\"1975-12-24T14:50:29.082285Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-978305356, (try DateTime.parse(\"1939-01-01T00:30:43.460779Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(1857079750, (try DateTime.parse(\"2028-11-05T23:29:10.225783Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1059764722, (try DateTime.parse(\"1936-06-02T04:54:37.841836Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2931563560, (try DateTime.parse(\"2062-11-24T03:12:40.682221Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-58861051, (try DateTime.parse(\"1968-02-19T17:42:28.861019Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2540374023, (try DateTime.parse(\"2050-07-02T11:27:03.083527Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-369803898, (try DateTime.parse(\"1958-04-13T20:41:41.391534Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(1150522786, (try DateTime.parse(\"2006-06-17T05:39:46.776689Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-3094311182, (try DateTime.parse(\"1871-12-12T05:06:57.955425Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2742945297, (try DateTime.parse(\"2056-12-02T01:14:57.552041Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-3055421456, (try DateTime.parse(\"1873-03-06T07:49:03.861761Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(1935913185, (try DateTime.parse(\"2031-05-07T09:39:45.408961Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1546803921, (try DateTime.parse(\"1920-12-26T04:14:38.089431Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2430955251, (try DateTime.parse(\"2047-01-13T01:20:51.611416Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1162742133, (try DateTime.parse(\"1933-02-26T08:04:26.776057Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2820984010, (try DateTime.parse(\"2059-05-24T06:40:10.9707Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2671779872, (try DateTime.parse(\"1885-05-02T14:55:27.010415Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(419726969, (try DateTime.parse(\"1983-04-20T22:49:29.184213Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2886236400, (try DateTime.parse(\"1878-07-16T11:39:59.700923Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(1091845921, (try DateTime.parse(\"2004-08-07T02:32:01.949043Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-1345585389, (try DateTime.parse(\"1927-05-13T02:16:50.807413Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(968555612, (try DateTime.parse(\"2000-09-10T03:13:32.056103Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-525723150, (try DateTime.parse(\"1953-05-05T05:47:29.657935Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2179443523, (try DateTime.parse(\"2039-01-24T00:58:43.238504Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2200838901, (try DateTime.parse(\"1900-04-05T07:51:38.801707Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(567335109, (try DateTime.parse(\"1987-12-24T09:05:09.535877Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-714932675, (try DateTime.parse(\"1947-05-07T07:35:24.863781Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(2735649359, (try DateTime.parse(\"2056-09-08T14:35:59.483204Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-2386101706, (try DateTime.parse(\"1894-05-22T01:58:13.445088Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(115985094, (try DateTime.parse(\"1973-09-04T10:04:54.005266Z\", .rfc3339)).unix(.seconds));\n    try testing.expectEqual(-3046532170, (try DateTime.parse(\"1873-06-17T05:03:49.260067Z\", .rfc3339)).unix(.seconds));\n}\n\ntest \"DateTime: limits\" {\n    {\n        // min\n        const dt1 = try DateTime.initUTC(-4712, 1, 1, 0, 0, 0, 0);\n        const dt2 = try DateTime.parse(\"-4712-01-01T00:00:00.000000Z\", .rfc3339);\n        const dt3 = try DateTime.fromUnix(-210863520000, .seconds);\n        const dt4 = try DateTime.fromUnix(-210863520000000, .milliseconds);\n        const dt5 = try DateTime.fromUnix(-210863520000000000, .microseconds);\n        for ([_]DateTime{ dt1, dt2, dt3, dt4, dt5 }) |dt| {\n            try testing.expectEqual(-4712, dt.date().year);\n            try testing.expectEqual(1, dt.date().month);\n            try testing.expectEqual(1, dt.date().day);\n            try testing.expectEqual(0, dt.time().hour);\n            try testing.expectEqual(0, dt.time().min);\n            try testing.expectEqual(0, dt.time().sec);\n            try testing.expectEqual(0, dt.time().micros);\n            try testing.expectEqual(-210863520000, dt.unix(.seconds));\n            try testing.expectEqual(-210863520000000, dt.unix(.milliseconds));\n            try testing.expectEqual(-210863520000000000, dt.unix(.microseconds));\n        }\n    }\n\n    {\n        // max\n        const dt1 = try DateTime.initUTC(9999, 12, 31, 23, 59, 59, 999999);\n        const dt2 = try DateTime.parse(\"9999-12-31T23:59:59.999999Z\", .rfc3339);\n        const dt3 = try DateTime.fromUnix(253402300799, .seconds);\n        const dt4 = try DateTime.fromUnix(253402300799999, .milliseconds);\n        const dt5 = try DateTime.fromUnix(253402300799999999, .microseconds);\n        for ([_]DateTime{ dt1, dt2, dt3, dt4, dt5 }, 0..) |dt, i| {\n            try testing.expectEqual(9999, dt.date().year);\n            try testing.expectEqual(12, dt.date().month);\n            try testing.expectEqual(31, dt.date().day);\n            try testing.expectEqual(23, dt.time().hour);\n            try testing.expectEqual(59, dt.time().min);\n            try testing.expectEqual(59, dt.time().sec);\n\n            try testing.expectEqual(253402300799, dt.unix(.seconds));\n\n            if (i == 2) {\n                try testing.expectEqual(0, dt.time().micros);\n                try testing.expectEqual(253402300799000, dt.unix(.milliseconds));\n                try testing.expectEqual(253402300799000000, dt.unix(.microseconds));\n            } else if (i == 3) {\n                try testing.expectEqual(999000, dt.time().micros);\n                try testing.expectEqual(253402300799999, dt.unix(.milliseconds));\n                try testing.expectEqual(253402300799999000, dt.unix(.microseconds));\n            } else {\n                try testing.expectEqual(999999, dt.time().micros);\n                try testing.expectEqual(253402300799999, dt.unix(.milliseconds));\n                try testing.expectEqual(253402300799999999, dt.unix(.microseconds));\n            }\n        }\n    }\n}\n\ntest \"DateTime: add\" {\n    {\n        // positive\n        var dt = try DateTime.parse(\"2023-11-26T03:13:46.540234Z\", .rfc3339);\n\n        dt = try dt.add(800, .microseconds);\n        try expectDateTime(\"2023-11-26T03:13:46.541034Z\", dt);\n\n        dt = try dt.add(950, .milliseconds);\n        try expectDateTime(\"2023-11-26T03:13:47.491034Z\", dt);\n\n        dt = try dt.add(32, .seconds);\n        try expectDateTime(\"2023-11-26T03:14:19.491034Z\", dt);\n\n        dt = try dt.add(1489, .minutes);\n        try expectDateTime(\"2023-11-27T04:03:19.491034Z\", dt);\n\n        dt = try dt.add(6, .days);\n        try expectDateTime(\"2023-12-03T04:03:19.491034Z\", dt);\n    }\n\n    {\n        // negative\n        var dt = try DateTime.parse(\"2023-11-26T03:13:46.540234Z\", .rfc3339);\n\n        dt = try dt.add(-800, .microseconds);\n        try expectDateTime(\"2023-11-26T03:13:46.539434Z\", dt);\n\n        dt = try dt.add(-950, .milliseconds);\n        try expectDateTime(\"2023-11-26T03:13:45.589434Z\", dt);\n\n        dt = try dt.add(-50, .seconds);\n        try expectDateTime(\"2023-11-26T03:12:55.589434Z\", dt);\n\n        dt = try dt.add(-1489, .minutes);\n        try expectDateTime(\"2023-11-25T02:23:55.589434Z\", dt);\n\n        dt = try dt.add(-6, .days);\n        try expectDateTime(\"2023-11-19T02:23:55.589434Z\", dt);\n    }\n}\n\ntest \"DateTime: sub\" {\n    {\n        const a = try DateTime.parse(\"2023-11-26T03:13:46.540234Z\", .rfc3339);\n        const b = try DateTime.parse(\"2023-11-26T03:13:47.540236Z\", .rfc3339);\n\n        try testing.expectEqual(-1, a.sub(b, .seconds));\n        try testing.expectEqual(-1000, a.sub(b, .milliseconds));\n        try testing.expectEqual(-1000002, a.sub(b, .microseconds));\n    }\n\n    {\n        const a = try DateTime.parse(\"2023-11-27T03:13:47.540234Z\", .rfc3339);\n        const b = try DateTime.parse(\"2023-11-26T03:13:47.540234Z\", .rfc3339);\n\n        try testing.expectEqual(86400, a.sub(b, .seconds));\n        try testing.expectEqual(86400000, a.sub(b, .milliseconds));\n        try testing.expectEqual(86400000000, a.sub(b, .microseconds));\n    }\n}\n\nfn expectDateTime(expected: []const u8, dt: DateTime) !void {\n    var buf: [30]u8 = undefined;\n    const actual = try std.fmt.bufPrint(&buf, \"{f}\", .{dt});\n    try testing.expectString(expected, actual);\n}\n\nconst TestStruct = struct {\n    date: ?Date = null,\n    time: ?Time = null,\n    datetime: ?DateTime = null,\n};\n"
  },
  {
    "path": "src/html5ever/Cargo.toml",
    "content": "[package]\nname = \"litefetch-html5ever\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nname = \"litefetch_html5ever\"\npath = \"lib.rs\"\ncrate-type = [\"cdylib\", \"staticlib\"]\n\n[dependencies]\nhtml5ever = \"0.35.0\"\nstring_cache = \"0.9.0\"\ntyped-arena = \"2.0.2\"\ntikv-jemallocator = {version = \"0.6.0\", features = [\"stats\"]}\ntikv-jemalloc-ctl = {version = \"0.6.0\", features = [\"stats\"]}\nxml5ever = \"0.35.0\"\n\n[profile.release]\nlto = true\ncodegen-units = 1\n"
  },
  {
    "path": "src/html5ever/lib.rs",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nmod sink;\nmod types;\n\n#[cfg(debug_assertions)]\n#[global_allocator]\nstatic GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;\n\nuse std::cell::Cell;\nuse std::os::raw::{c_uchar, c_void};\nuse types::*;\n\nuse html5ever::interface::tree_builder::QuirksMode;\nuse html5ever::tendril::{StrTendril, TendrilSink};\nuse html5ever::{ns, parse_document, parse_fragment, LocalName, ParseOpts, Parser, QualName};\n\n#[no_mangle]\npub extern \"C\" fn html5ever_parse_document(\n    html: *mut c_uchar,\n    len: usize,\n    document: Ref,\n    ctx: Ref,\n    create_element_callback: CreateElementCallback,\n    get_data_callback: GetDataCallback,\n    append_callback: AppendCallback,\n    parse_error_callback: ParseErrorCallback,\n    pop_callback: PopCallback,\n    create_comment_callback: CreateCommentCallback,\n    create_processing_instruction: CreateProcessingInstruction,\n    append_doctype_to_document: AppendDoctypeToDocumentCallback,\n    add_attrs_if_missing_callback: AddAttrsIfMissingCallback,\n    get_template_contents_callback: GetTemplateContentsCallback,\n    remove_from_parent_callback: RemoveFromParentCallback,\n    reparent_children_callback: ReparentChildrenCallback,\n    append_before_sibling_callback: AppendBeforeSiblingCallback,\n    append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,\n) -> () {\n    if html.is_null() || len == 0 {\n        return ();\n    }\n\n    let arena = typed_arena::Arena::new();\n\n    let sink = sink::Sink {\n        ctx: ctx,\n        arena: &arena,\n        document: document,\n        quirks_mode: Cell::new(QuirksMode::NoQuirks),\n        pop_callback: pop_callback,\n        append_callback: append_callback,\n        get_data_callback: get_data_callback,\n        parse_error_callback: parse_error_callback,\n        create_element_callback: create_element_callback,\n        create_comment_callback: create_comment_callback,\n        create_processing_instruction: create_processing_instruction,\n        append_doctype_to_document: append_doctype_to_document,\n        add_attrs_if_missing_callback: add_attrs_if_missing_callback,\n        get_template_contents_callback: get_template_contents_callback,\n        remove_from_parent_callback: remove_from_parent_callback,\n        reparent_children_callback: reparent_children_callback,\n        append_before_sibling_callback: append_before_sibling_callback,\n        append_based_on_parent_node_callback: append_based_on_parent_node_callback,\n    };\n\n    let bytes = unsafe { std::slice::from_raw_parts(html, len) };\n    parse_document(sink, Default::default())\n        .from_utf8()\n        .one(bytes);\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_parse_fragment(\n    html: *mut c_uchar,\n    len: usize,\n    document: Ref,\n    ctx: Ref,\n    create_element_callback: CreateElementCallback,\n    get_data_callback: GetDataCallback,\n    append_callback: AppendCallback,\n    parse_error_callback: ParseErrorCallback,\n    pop_callback: PopCallback,\n    create_comment_callback: CreateCommentCallback,\n    create_processing_instruction: CreateProcessingInstruction,\n    append_doctype_to_document: AppendDoctypeToDocumentCallback,\n    add_attrs_if_missing_callback: AddAttrsIfMissingCallback,\n    get_template_contents_callback: GetTemplateContentsCallback,\n    remove_from_parent_callback: RemoveFromParentCallback,\n    reparent_children_callback: ReparentChildrenCallback,\n    append_before_sibling_callback: AppendBeforeSiblingCallback,\n    append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,\n) -> () {\n    if html.is_null() || len == 0 {\n        return ();\n    }\n\n    let arena = typed_arena::Arena::new();\n\n    let sink = sink::Sink {\n        ctx: ctx,\n        arena: &arena,\n        document: document,\n        quirks_mode: Cell::new(QuirksMode::NoQuirks),\n        pop_callback: pop_callback,\n        append_callback: append_callback,\n        get_data_callback: get_data_callback,\n        parse_error_callback: parse_error_callback,\n        create_element_callback: create_element_callback,\n        create_comment_callback: create_comment_callback,\n        create_processing_instruction: create_processing_instruction,\n        append_doctype_to_document: append_doctype_to_document,\n        add_attrs_if_missing_callback: add_attrs_if_missing_callback,\n        get_template_contents_callback: get_template_contents_callback,\n        remove_from_parent_callback: remove_from_parent_callback,\n        reparent_children_callback: reparent_children_callback,\n        append_before_sibling_callback: append_before_sibling_callback,\n        append_based_on_parent_node_callback: append_based_on_parent_node_callback,\n    };\n\n    let bytes = unsafe { std::slice::from_raw_parts(html, len) };\n    parse_fragment(\n        sink,\n        Default::default(),\n        QualName::new(None, ns!(html), LocalName::from(\"body\")),\n        vec![], // attributes\n        false,  // context_element_allows_scripting\n    )\n    .from_utf8()\n    .one(bytes);\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_attribute_iterator_next(\n    c_iter: *const c_void,\n) -> CNullable<CAttribute> {\n    let iter: &mut CAttributeIterator = unsafe { &mut *(c_iter as *mut CAttributeIterator) };\n\n    let pos = iter.pos;\n    if pos == iter.vec.len() {\n        return CNullable::<CAttribute>::none();\n    }\n\n    let attr = &iter.vec[pos];\n    iter.pos += 1;\n    return CNullable::<CAttribute>::some(CAttribute {\n        name: CQualName::create(&attr.name),\n        value: StringSlice {\n            ptr: attr.value.as_ptr(),\n            len: attr.value.len(),\n        },\n    });\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_attribute_iterator_count(c_iter: *const c_void) -> usize {\n    let iter: &mut CAttributeIterator = unsafe { &mut *(c_iter as *mut CAttributeIterator) };\n    return iter.vec.len();\n}\n\n#[cfg(debug_assertions)]\n#[repr(C)]\npub struct Memory {\n    pub resident: usize,\n    pub allocated: usize,\n}\n\n#[cfg(debug_assertions)]\n#[no_mangle]\npub extern \"C\" fn html5ever_get_memory_usage() -> Memory {\n    use tikv_jemalloc_ctl::{epoch, stats};\n\n    // many statistics are cached and only updated when the epoch is advanced.\n    epoch::advance().unwrap();\n\n    return Memory {\n        resident: stats::resident::read().unwrap(),\n        allocated: stats::allocated::read().unwrap(),\n    };\n}\n\n// Streaming parser API\n// The Parser type from html5ever implements TendrilSink and supports streaming\npub struct StreamingParser {\n    #[allow(dead_code)]\n    arena: Box<typed_arena::Arena<sink::ElementData>>,\n    parser: Box<dyn std::any::Any>,\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_streaming_parser_create(\n    document: Ref,\n    ctx: Ref,\n    create_element_callback: CreateElementCallback,\n    get_data_callback: GetDataCallback,\n    append_callback: AppendCallback,\n    parse_error_callback: ParseErrorCallback,\n    pop_callback: PopCallback,\n    create_comment_callback: CreateCommentCallback,\n    create_processing_instruction: CreateProcessingInstruction,\n    append_doctype_to_document: AppendDoctypeToDocumentCallback,\n    add_attrs_if_missing_callback: AddAttrsIfMissingCallback,\n    get_template_contents_callback: GetTemplateContentsCallback,\n    remove_from_parent_callback: RemoveFromParentCallback,\n    reparent_children_callback: ReparentChildrenCallback,\n    append_before_sibling_callback: AppendBeforeSiblingCallback,\n    append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,\n) -> *mut c_void {\n    let arena = Box::new(typed_arena::Arena::new());\n\n    // SAFETY: We're creating a self-referential structure here.\n    // The arena is stored in the StreamingParser and lives as long as the parser.\n    // The sink contains a reference to the arena that's valid for the parser's lifetime.\n    let arena_ref: &'static typed_arena::Arena<sink::ElementData> =\n        unsafe { std::mem::transmute(arena.as_ref()) };\n\n    let sink = sink::Sink {\n        ctx: ctx,\n        arena: arena_ref,\n        document: document,\n        quirks_mode: Cell::new(QuirksMode::NoQuirks),\n        pop_callback: pop_callback,\n        append_callback: append_callback,\n        get_data_callback: get_data_callback,\n        parse_error_callback: parse_error_callback,\n        create_element_callback: create_element_callback,\n        create_comment_callback: create_comment_callback,\n        create_processing_instruction: create_processing_instruction,\n        append_doctype_to_document: append_doctype_to_document,\n        add_attrs_if_missing_callback: add_attrs_if_missing_callback,\n        get_template_contents_callback: get_template_contents_callback,\n        remove_from_parent_callback: remove_from_parent_callback,\n        reparent_children_callback: reparent_children_callback,\n        append_before_sibling_callback: append_before_sibling_callback,\n        append_based_on_parent_node_callback: append_based_on_parent_node_callback,\n    };\n\n    // Create a parser which implements TendrilSink for streaming parsing\n    let parser = parse_document(sink, ParseOpts::default());\n\n    let streaming_parser = Box::new(StreamingParser {\n        arena,\n        parser: Box::new(parser),\n    });\n\n    return Box::into_raw(streaming_parser) as *mut c_void;\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_streaming_parser_feed(\n    parser_ptr: *mut c_void,\n    html: *const c_uchar,\n    len: usize,\n) -> i32 {\n    if parser_ptr.is_null() || html.is_null() || len == 0 {\n        return 0;\n    }\n\n    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        let streaming_parser = unsafe { &mut *(parser_ptr as *mut StreamingParser) };\n        let bytes = unsafe { std::slice::from_raw_parts(html, len) };\n\n        // Convert bytes to UTF-8 string\n        if let Ok(s) = std::str::from_utf8(bytes) {\n            let tendril = StrTendril::from(s);\n\n            // Feed the chunk to the parser\n            // The Parser implements TendrilSink, so we can call process() on it\n            let parser = streaming_parser\n                .parser\n                .downcast_mut::<Parser<sink::Sink>>()\n                .expect(\"Invalid parser type\");\n\n            parser.process(tendril);\n        }\n    }));\n\n    match result {\n        Ok(_) => 0,   // Success\n        Err(_) => -1, // Panic occurred\n    }\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_streaming_parser_finish(parser_ptr: *mut c_void) {\n    if parser_ptr.is_null() {\n        return;\n    }\n\n    let streaming_parser = unsafe { Box::from_raw(parser_ptr as *mut StreamingParser) };\n\n    // Extract and finish the parser\n    let parser = streaming_parser\n        .parser\n        .downcast::<Parser<sink::Sink>>()\n        .expect(\"Invalid parser type\");\n\n    // Finish consumes the parser, which will call finish() on the sink\n    parser.finish();\n\n    // Note: The arena will be dropped here automatically\n}\n\n#[no_mangle]\npub extern \"C\" fn html5ever_streaming_parser_destroy(parser_ptr: *mut c_void) {\n    if parser_ptr.is_null() {\n        return;\n    }\n\n    // Drop the parser box without finishing\n    // This is for cases where you want to cancel parsing\n    unsafe {\n        let _ = Box::from_raw(parser_ptr as *mut StreamingParser);\n    }\n}\n\n#[no_mangle]\npub extern \"C\" fn xml5ever_parse_document(\n    xml: *mut c_uchar,\n    len: usize,\n    document: Ref,\n    ctx: Ref,\n    create_element_callback: CreateElementCallback,\n    get_data_callback: GetDataCallback,\n    append_callback: AppendCallback,\n    parse_error_callback: ParseErrorCallback,\n    pop_callback: PopCallback,\n    create_comment_callback: CreateCommentCallback,\n    create_processing_instruction: CreateProcessingInstruction,\n    append_doctype_to_document: AppendDoctypeToDocumentCallback,\n    add_attrs_if_missing_callback: AddAttrsIfMissingCallback,\n    get_template_contents_callback: GetTemplateContentsCallback,\n    remove_from_parent_callback: RemoveFromParentCallback,\n    reparent_children_callback: ReparentChildrenCallback,\n    append_before_sibling_callback: AppendBeforeSiblingCallback,\n    append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,\n) -> () {\n    if xml.is_null() || len == 0 {\n        return ();\n    }\n\n    let arena = typed_arena::Arena::new();\n\n    let sink = sink::Sink {\n        ctx: ctx,\n        arena: &arena,\n        document: document,\n        quirks_mode: Cell::new(QuirksMode::NoQuirks),\n        pop_callback: pop_callback,\n        append_callback: append_callback,\n        get_data_callback: get_data_callback,\n        parse_error_callback: parse_error_callback,\n        create_element_callback: create_element_callback,\n        create_comment_callback: create_comment_callback,\n        create_processing_instruction: create_processing_instruction,\n        append_doctype_to_document: append_doctype_to_document,\n        add_attrs_if_missing_callback: add_attrs_if_missing_callback,\n        get_template_contents_callback: get_template_contents_callback,\n        remove_from_parent_callback: remove_from_parent_callback,\n        reparent_children_callback: reparent_children_callback,\n        append_before_sibling_callback: append_before_sibling_callback,\n        append_based_on_parent_node_callback: append_based_on_parent_node_callback,\n    };\n\n    let bytes = unsafe { std::slice::from_raw_parts(xml, len) };\n    xml5ever::driver::parse_document(sink, xml5ever::driver::XmlParseOpts::default())\n        .from_utf8()\n        .one(bytes);\n}\n"
  },
  {
    "path": "src/html5ever/sink.rs",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nuse std::ptr;\nuse std::cell::Cell;\nuse std::borrow::Cow;\nuse std::os::raw::{c_void};\n\nuse crate::types::*;\n\nuse html5ever::tendril::{StrTendril};\nuse html5ever::{Attribute, QualName};\nuse html5ever::interface::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink};\n\ntype Arena<'arena> = &'arena typed_arena::Arena<ElementData>;\n\n// Made public so it can be used from lib.rs\npub struct ElementData {\n    pub qname: QualName,\n    pub mathml_annotation_xml_integration_point: bool,\n}\nimpl ElementData {\n    fn new(qname: QualName, flags: ElementFlags) -> Self {\n        return Self {\n            qname: qname,\n            mathml_annotation_xml_integration_point: flags.mathml_annotation_xml_integration_point,\n        };\n    }\n}\n\npub struct Sink<'arena> {\n    pub ctx: Ref,\n    pub document: Ref,\n    pub arena: Arena<'arena>,\n    pub quirks_mode: Cell<QuirksMode>,\n    pub pop_callback: PopCallback,\n    pub append_callback: AppendCallback,\n    pub get_data_callback: GetDataCallback,\n    pub parse_error_callback: ParseErrorCallback,\n    pub create_element_callback: CreateElementCallback,\n    pub create_comment_callback: CreateCommentCallback,\n    pub create_processing_instruction: CreateProcessingInstruction,\n    pub append_doctype_to_document: AppendDoctypeToDocumentCallback,\n    pub add_attrs_if_missing_callback: AddAttrsIfMissingCallback,\n    pub get_template_contents_callback: GetTemplateContentsCallback,\n    pub remove_from_parent_callback: RemoveFromParentCallback,\n    pub reparent_children_callback: ReparentChildrenCallback,\n    pub append_before_sibling_callback: AppendBeforeSiblingCallback,\n    pub append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,\n}\n\nimpl<'arena> TreeSink for Sink<'arena> {\n    type Handle = *const c_void;\n    type Output = ();\n    type ElemName<'a>\n        = &'a QualName\n    where\n        Self: 'a;\n\n    fn finish(self) -> () {\n        return ();\n    }\n\n    fn parse_error(&self, err: Cow<'static, str>) {\n        unsafe {\n            (self.parse_error_callback)(\n                self.ctx,\n                StringSlice {\n                    ptr: err.as_ptr(),\n                    len: err.len(),\n                },\n            );\n        }\n    }\n\n    fn get_document(&self) -> *const c_void {\n        return self.document;\n    }\n\n    fn set_quirks_mode(&self, mode: QuirksMode) {\n        self.quirks_mode.set(mode);\n    }\n\n    fn same_node(&self, x: &Ref, y: &Ref) -> bool {\n        ptr::eq::<c_void>(*x, *y)\n    }\n\n    fn elem_name(&self, target: &Ref) -> Self::ElemName<'_> {\n        let opaque = unsafe { (self.get_data_callback)(*target) };\n        let data = opaque as *mut ElementData;\n        return unsafe { &(*data).qname };\n    }\n\n    fn get_template_contents(&self, target: &Ref) -> Ref {\n        unsafe {\n            return (self.get_template_contents_callback)(self.ctx, *target);\n        }\n    }\n\n    fn is_mathml_annotation_xml_integration_point(&self, target: &Ref) -> bool {\n        let opaque = unsafe { (self.get_data_callback)(*target) };\n        let data = opaque as *mut ElementData;\n        return unsafe { (*data).mathml_annotation_xml_integration_point };\n    }\n\n    fn pop(&self, node: &Ref) {\n        unsafe {\n            (self.pop_callback)(self.ctx, *node);\n        }\n    }\n\n    fn create_element(&self, name: QualName, attrs: Vec<Attribute>, flags: ElementFlags) -> Ref {\n        let data = self.arena.alloc(ElementData::new(name.clone(), flags));\n\n        unsafe {\n            let mut attribute_iterator = CAttributeIterator { vec: attrs, pos: 0 };\n\n            return (self.create_element_callback)(\n                self.ctx,\n                data as *mut _ as *mut c_void,\n                CQualName::create(&name),\n                &mut attribute_iterator as *mut _ as *mut c_void,\n            );\n        }\n    }\n\n    fn create_comment(&self, txt: StrTendril) -> Ref {\n        let str = StringSlice{ ptr: txt.as_ptr(), len: txt.len()};\n        unsafe {\n            return (self.create_comment_callback)(self.ctx, str);\n        }\n    }\n\n    fn create_pi(&self, target: StrTendril, data: StrTendril) -> Ref {\n        let str_target = StringSlice{ ptr: target.as_ptr(), len: target.len()};\n        let str_data = StringSlice{ ptr: data.as_ptr(), len: data.len()};\n        unsafe {\n            return (self.create_processing_instruction)(self.ctx, str_target, str_data);\n        }\n    }\n\n    fn append(&self, parent: &Ref, child: NodeOrText<Ref>) {\n        match child {\n            NodeOrText::AppendText(ref t) => {\n                // The child exists for the duration of the append_callback call,\n                // but sometimes the memory on the Zig side, in append_callback,\n                // is zeroed. If you try to refactor this code a bit, and do:\n                //   unsafe {\n                //       (self.append_callback)(self.ctx, *parent, CNodeOrText::create(child));\n                //   }\n                // Where CNodeOrText::create returns the property CNodeOrText,\n                // you'll occasionally see that zeroed memory. Makes no sense to\n                // me, but a far as I can tell, this version works.\n                let byte_slice = t.as_ref().as_bytes();\n                let static_slice: &'static [u8] = unsafe {\n                    std::mem::transmute(byte_slice)\n                };\n                unsafe {\n                    (self.append_callback)(self.ctx, *parent, CNodeOrText{\n                        tag: 1,\n                        node: ptr::null(),\n                        text: StringSlice { ptr: static_slice.as_ptr(), len: static_slice.len()},\n                     });\n                };\n            },\n            NodeOrText::AppendNode(node) => {\n               unsafe {\n                    (self.append_callback)(self.ctx, *parent, CNodeOrText{\n                        tag: 0,\n                        node: node,\n                        text: StringSlice::default()\n                    });\n                };\n            }\n        }\n    }\n\n    fn append_before_sibling(&self, sibling: &Ref, child: NodeOrText<Ref>) {\n        match child {\n            NodeOrText::AppendText(ref t) => {\n                let byte_slice = t.as_ref().as_bytes();\n                let static_slice: &'static [u8] = unsafe {\n                    std::mem::transmute(byte_slice)\n                };\n                unsafe {\n                    (self.append_before_sibling_callback)(self.ctx, *sibling, CNodeOrText{\n                        tag: 1,\n                        node: ptr::null(),\n                        text: StringSlice { ptr: static_slice.as_ptr(), len: static_slice.len()},\n                     });\n                };\n            },\n            NodeOrText::AppendNode(node) => {\n               unsafe {\n                    (self.append_before_sibling_callback)(self.ctx, *sibling, CNodeOrText{\n                        tag: 0,\n                        node: node,\n                        text: StringSlice::default()\n                    });\n                };\n            }\n        }\n    }\n\n    fn append_based_on_parent_node(\n        &self,\n        element: &Ref,\n        prev_element: &Ref,\n        child: NodeOrText<Ref>,\n    ) {\n        match child {\n            NodeOrText::AppendText(ref t) => {\n                let byte_slice = t.as_ref().as_bytes();\n                let static_slice: &'static [u8] = unsafe {\n                    std::mem::transmute(byte_slice)\n                };\n                unsafe {\n                    (self.append_based_on_parent_node_callback)(self.ctx, *element, *prev_element, CNodeOrText{\n                        tag: 1,\n                        node: ptr::null(),\n                        text: StringSlice { ptr: static_slice.as_ptr(), len: static_slice.len()},\n                     });\n                };\n            },\n            NodeOrText::AppendNode(node) => {\n               unsafe {\n                    (self.append_based_on_parent_node_callback)(self.ctx, *element, *prev_element, CNodeOrText{\n                        tag: 0,\n                        node: node,\n                        text: StringSlice::default()\n                    });\n                };\n            }\n        }\n    }\n\n    fn append_doctype_to_document(\n        &self,\n        name: StrTendril,\n        public_id: StrTendril,\n        system_id: StrTendril,\n    ) {\n        let name_str = StringSlice{ ptr: name.as_ptr(), len: name.len()};\n        let public_id_str = StringSlice{ ptr: public_id.as_ptr(), len: public_id.len()};\n        let system_id_str = StringSlice{ ptr: system_id.as_ptr(), len: system_id.len()};\n        unsafe {\n            (self.append_doctype_to_document)(self.ctx, name_str, public_id_str, system_id_str);\n        }\n    }\n\n    fn add_attrs_if_missing(&self, target: &Ref, attrs: Vec<Attribute>) {\n        unsafe {\n            let mut attribute_iterator = CAttributeIterator { vec: attrs, pos: 0 };\n\n            (self.add_attrs_if_missing_callback)(\n                self.ctx,\n                *target,\n                &mut attribute_iterator as *mut _ as *mut c_void,\n            );\n        }\n    }\n\n    fn remove_from_parent(&self, target: &Ref) {\n        unsafe {\n            (self.remove_from_parent_callback)(self.ctx, *target);\n        }\n    }\n\n    fn reparent_children(&self, node: &Ref, new_parent: &Ref) {\n        unsafe {\n            (self.reparent_children_callback)(self.ctx, *node, *new_parent);\n        }\n    }\n}\n"
  },
  {
    "path": "src/html5ever/types.rs",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nuse std::ptr;\nuse html5ever::{QualName, Attribute};\nuse std::os::raw::{c_uchar, c_void};\n\npub type CreateElementCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    data: *const c_void,\n    name: CQualName,\n    attributes: *mut c_void,\n) -> Ref;\n\npub type CreateCommentCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    str: StringSlice,\n) -> Ref;\n\npub type AppendDoctypeToDocumentCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    name: StringSlice,\n    public_id: StringSlice,\n    system_id: StringSlice,\n) -> ();\n\npub type CreateProcessingInstruction = unsafe extern \"C\" fn(\n    ctx: Ref,\n    target: StringSlice,\n    data: StringSlice,\n) -> Ref;\n\npub type GetDataCallback = unsafe extern \"C\" fn(ctx: Ref) -> *mut c_void;\n\npub type AppendCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    parent: Ref,\n    node_or_text: CNodeOrText\n) -> ();\n\npub type ParseErrorCallback = unsafe extern \"C\" fn(ctx: Ref, str: StringSlice) -> ();\n\npub type PopCallback = unsafe extern \"C\" fn(ctx: Ref, node: Ref) -> ();\n\npub type AddAttrsIfMissingCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    target: Ref,\n    attributes: *mut c_void,\n) -> ();\n\npub type GetTemplateContentsCallback = unsafe extern \"C\" fn(ctx: Ref, target: Ref) -> Ref;\n\npub type RemoveFromParentCallback = unsafe extern \"C\" fn(ctx: Ref, target: Ref) -> ();\n\npub type ReparentChildrenCallback = unsafe extern \"C\" fn(ctx: Ref, node: Ref, new_parent: Ref) -> ();\n\npub type AppendBeforeSiblingCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    sibling: Ref,\n    node_or_text: CNodeOrText\n) -> ();\n\npub type AppendBasedOnParentNodeCallback = unsafe extern \"C\" fn(\n    ctx: Ref,\n    element: Ref,\n    prev_element: Ref,\n    node_or_text: CNodeOrText\n) -> ();\n\npub type Ref = *const c_void;\n\n#[repr(C)]\npub struct CNullable<T> {\n    tag: u8, // 0 = None, 1 = Some\n    value: T,\n}\nimpl<T: Default> CNullable<T> {\n    pub fn none() -> CNullable<T> {\n        return Self{tag: 0, value: T::default()};\n    }\n\n    pub fn some(v: T) -> CNullable<T> {\n        return Self{tag: 1, value: v};\n    }\n}\n\n#[repr(C)]\npub struct Slice<T> {\n    pub ptr: *const T,\n    pub len: usize,\n}\nimpl<T> Default for Slice<T> {\n    fn default() -> Self {\n        return Self{ptr: ptr::null(), len: 0};\n    }\n}\n\npub type StringSlice = Slice<c_uchar>;\n\n#[repr(C)]\npub struct CQualName {\n    prefix: CNullable<StringSlice>,\n    ns: StringSlice,\n    local: StringSlice,\n}\nimpl CQualName {\n    pub fn create(q: &QualName) -> Self {\n        let ns = StringSlice { ptr: q.ns.as_ptr(), len: q.ns.len()};\n        let local = StringSlice { ptr: q.local.as_ptr(), len: q.local.len()};\n        let prefix = match &q.prefix {\n            None => CNullable::<StringSlice>::none(),\n            Some(prefix) => CNullable::<StringSlice>::some(StringSlice { ptr: prefix.as_ptr(), len: prefix.len()}),\n        };\n        return CQualName{\n            // inner: q as *const _ as *const c_void,\n            ns: ns,\n            local: local,\n            prefix: prefix,\n        };\n    }\n}\nimpl Default for CQualName {\n    fn default() -> Self {\n        return Self{\n            prefix: CNullable::<StringSlice>::none(),\n            ns: StringSlice::default(),\n            local: StringSlice::default(),\n        };\n    }\n}\n\n#[repr(C)]\npub struct CAttribute {\n    pub name: CQualName,\n    pub value: StringSlice,\n}\nimpl Default for CAttribute {\n    fn default() -> Self {\n        return Self{name: CQualName::default(), value: StringSlice::default()};\n    }\n}\n\npub struct CAttributeIterator {\n    pub vec: Vec<Attribute>,\n    pub pos: usize,\n}\n\n#[repr(C)]\npub struct CNodeOrText {\n    pub tag: u8, // 0 = node, 1 = text\n    pub node: Ref,\n    pub text: StringSlice,\n}\n"
  },
  {
    "path": "src/id.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\npub fn uuidv4(hex: []u8) void {\n    lp.assert(hex.len == 36, \"uuidv4.len\", .{ .len = hex.len });\n\n    var bin: [16]u8 = undefined;\n    std.crypto.random.bytes(&bin);\n    bin[6] = (bin[6] & 0x0f) | 0x40;\n    bin[8] = (bin[8] & 0x3f) | 0x80;\n\n    const alphabet = \"0123456789abcdef\";\n\n    hex[8] = '-';\n    hex[13] = '-';\n    hex[18] = '-';\n    hex[23] = '-';\n\n    const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 };\n    inline for (encoded_pos, 0..) |i, j| {\n        hex[i + 0] = alphabet[bin[j] >> 4];\n        hex[i + 1] = alphabet[bin[j] & 0x0f];\n    }\n}\n\nconst testing = std.testing;\ntest \"id: uuiv4\" {\n    const expectUUID = struct {\n        fn expect(uuid: [36]u8) !void {\n            for (uuid, 0..) |b, i| {\n                switch (b) {\n                    '0'...'9', 'a'...'z' => {},\n                    '-' => {\n                        if (i != 8 and i != 13 and i != 18 and i != 23) {\n                            return error.InvalidEncoding;\n                        }\n                    },\n                    else => return error.InvalidHexEncoding,\n                }\n            }\n        }\n    }.expect;\n\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var seen = std.StringHashMapUnmanaged(void){};\n    for (0..100) |_| {\n        var hex: [36]u8 = undefined;\n        uuidv4(&hex);\n        try expectUUID(hex);\n        try seen.put(allocator, try allocator.dupe(u8, &hex), {});\n    }\n    try testing.expectEqual(100, seen.count());\n}\n"
  },
  {
    "path": "src/lightpanda.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\npub const App = @import(\"App.zig\");\npub const Network = @import(\"network/Runtime.zig\");\npub const Server = @import(\"Server.zig\");\npub const Config = @import(\"Config.zig\");\npub const URL = @import(\"browser/URL.zig\");\npub const String = @import(\"string.zig\").String;\npub const Page = @import(\"browser/Page.zig\");\npub const Browser = @import(\"browser/Browser.zig\");\npub const Session = @import(\"browser/Session.zig\");\npub const Notification = @import(\"Notification.zig\");\n\npub const log = @import(\"log.zig\");\npub const js = @import(\"browser/js/js.zig\");\npub const dump = @import(\"browser/dump.zig\");\npub const markdown = @import(\"browser/markdown.zig\");\npub const SemanticTree = @import(\"SemanticTree.zig\");\npub const CDPNode = @import(\"cdp/Node.zig\");\npub const interactive = @import(\"browser/interactive.zig\");\npub const actions = @import(\"browser/actions.zig\");\npub const structured_data = @import(\"browser/structured_data.zig\");\npub const mcp = @import(\"mcp.zig\");\npub const build_config = @import(\"build_config\");\npub const crash_handler = @import(\"crash_handler.zig\");\n\npub const HttpClient = @import(\"browser/HttpClient.zig\");\nconst IS_DEBUG = @import(\"builtin\").mode == .Debug;\n\npub const FetchOpts = struct {\n    wait_ms: u32 = 5000,\n    dump: dump.Opts,\n    dump_mode: ?Config.DumpFormat = null,\n    writer: ?*std.Io.Writer = null,\n};\npub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {\n    const http_client = try HttpClient.init(app.allocator, &app.network);\n    defer http_client.deinit();\n\n    const notification = try Notification.init(app.allocator);\n    defer notification.deinit();\n\n    var browser = try Browser.init(app, .{ .http_client = http_client });\n    defer browser.deinit();\n\n    var session = try browser.newSession(notification);\n    const page = try session.createPage();\n\n    // // Comment this out to get a profile of the JS code in v8/profile.json.\n    // // You can open this in Chrome's profiler.\n    // // I've seen it generate invalid JSON, but I'm not sure why. It\n    // // happens rarely, and I manually fix the file.\n    // page.js.startCpuProfiler();\n    // defer {\n    //     if (page.js.stopCpuProfiler()) |profile| {\n    //         std.fs.cwd().writeFile(.{\n    //             .sub_path = \".lp-cache/cpu_profile.json\",\n    //             .data = profile,\n    //         }) catch |err| {\n    //             log.err(.app, \"profile write error\", .{ .err = err });\n    //         };\n    //     } else |err| {\n    //         log.err(.app, \"profile error\", .{ .err = err });\n    //     }\n    // }\n\n    // // Comment this out to get a heap V8 heap profil\n    // page.js.startHeapProfiler();\n    // defer {\n    //     if (page.js.stopHeapProfiler()) |profile| {\n    //         std.fs.cwd().writeFile(.{\n    //             .sub_path = \".lp-cache/allocating.heapprofile\",\n    //             .data = profile.@\"0\",\n    //         }) catch |err| {\n    //             log.err(.app, \"allocating write error\", .{ .err = err });\n    //         };\n    //         std.fs.cwd().writeFile(.{\n    //             .sub_path = \".lp-cache/snapshot.heapsnapshot\",\n    //             .data = profile.@\"1\",\n    //         }) catch |err| {\n    //             log.err(.app, \"heapsnapshot write error\", .{ .err = err });\n    //         };\n    //     } else |err| {\n    //         log.err(.app, \"profile error\", .{ .err = err });\n    //     }\n    // }\n\n    const encoded_url = try URL.ensureEncoded(page.call_arena, url);\n    _ = try page.navigate(encoded_url, .{\n        .reason = .address_bar,\n        .kind = .{ .push = null },\n    });\n    _ = session.wait(opts.wait_ms);\n\n    const writer = opts.writer orelse return;\n    if (opts.dump_mode) |mode| {\n        switch (mode) {\n            .html => try dump.root(page.window._document, opts.dump, writer, page),\n            .markdown => try markdown.dump(page.window._document.asNode(), .{}, writer, page),\n            .semantic_tree, .semantic_tree_text => {\n                var registry = CDPNode.Registry.init(app.allocator);\n                defer registry.deinit();\n\n                const st: SemanticTree = .{\n                    .dom_node = page.window._document.asNode(),\n                    .registry = &registry,\n                    .page = page,\n                    .arena = page.call_arena,\n                    .prune = (mode == .semantic_tree_text),\n                };\n\n                if (mode == .semantic_tree) {\n                    try std.json.Stringify.value(st, .{}, writer);\n                } else {\n                    try st.textStringify(writer);\n                }\n            },\n            .wpt => try dumpWPT(page, writer),\n        }\n    }\n    try writer.flush();\n}\n\nfn dumpWPT(page: *Page, writer: *std.Io.Writer) !void {\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(&ls.local);\n    defer try_catch.deinit();\n\n    // return the detailed result.\n    const dump_script =\n        \\\\ JSON.stringify((() => {\n        \\\\   const statuses = ['Pass', 'Fail', 'Timeout', 'Not Run', 'Optional Feature Unsupported'];\n        \\\\   const parse = (raw) => {\n        \\\\     for (const status of statuses) {\n        \\\\       const idx = raw.indexOf('|' + status);\n        \\\\       if (idx !== -1) {\n        \\\\         const name = raw.slice(0, idx);\n        \\\\         const rest = raw.slice(idx + status.length + 1);\n        \\\\         const message = rest.length > 0 && rest[0] === '|' ? rest.slice(1) : null;\n        \\\\         return { name, status, message };\n        \\\\       }\n        \\\\     }\n        \\\\     return { name: raw, status: 'Unknown', message: null };\n        \\\\   };\n        \\\\   const cases = Object.values(report.cases).map(parse);\n        \\\\   return {\n        \\\\     url: window.location.href,\n        \\\\     status: report.status,\n        \\\\     message: report.message,\n        \\\\     summary: {\n        \\\\       total: cases.length,\n        \\\\       passed: cases.filter(c => c.status === 'Pass').length,\n        \\\\       failed: cases.filter(c => c.status === 'Fail').length,\n        \\\\       timeout: cases.filter(c => c.status === 'Timeout').length,\n        \\\\       notrun: cases.filter(c => c.status === 'Not Run').length,\n        \\\\       unsupported: cases.filter(c => c.status === 'Optional Feature Unsupported').length\n        \\\\     },\n        \\\\     cases\n        \\\\   };\n        \\\\ })(), null, 2)\n    ;\n    const value = ls.local.exec(dump_script, \"dump_script\") catch |err| {\n        const caught = try_catch.caughtOrError(page.call_arena, err);\n        return writer.print(\"Caught error trying to access WPT's report: {f}\\n\", .{caught});\n    };\n    try writer.writeAll(\"== WPT Results==\\n\");\n    try writer.writeAll(try value.toStringSliceWithAlloc(page.call_arena));\n}\n\npub inline fn assert(ok: bool, comptime ctx: []const u8, args: anytype) void {\n    if (!ok) {\n        if (comptime IS_DEBUG) {\n            unreachable;\n        }\n        assertionFailure(ctx, args);\n    }\n}\n\nnoinline fn assertionFailure(comptime ctx: []const u8, args: anytype) noreturn {\n    @branchHint(.cold);\n    if (@inComptime()) {\n        @compileError(std.fmt.comptimePrint(\"assertion failure: \" ++ ctx, args));\n    }\n    @import(\"crash_handler.zig\").crash(ctx, args, @returnAddress());\n}\n\ntest {\n    std.testing.refAllDecls(@This());\n}\n"
  },
  {
    "path": "src/log.zig",
    "content": "// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst Thread = std.Thread;\n\nconst is_debug = builtin.mode == .Debug;\n\npub const Scope = enum {\n    app,\n    dom,\n    bug,\n    browser,\n    cdp,\n    console,\n    http,\n    page,\n    js,\n    event,\n    scheduler,\n    not_implemented,\n    telemetry,\n    unknown_prop,\n    mcp,\n};\n\nconst Opts = struct {\n    format: Format = if (is_debug) .pretty else .logfmt,\n    level: Level = if (is_debug) .info else .warn,\n    filter_scopes: []const Scope = &.{},\n};\n\npub var opts = Opts{};\n\n// synchronizes writes to the output\nvar out_lock: Thread.Mutex = .{};\n\n// synchronizes access to last_log\nvar last_log_lock: Thread.Mutex = .{};\n\npub fn enabled(comptime scope: Scope, level: Level) bool {\n    if (@intFromEnum(level) < @intFromEnum(opts.level)) {\n        return false;\n    }\n\n    if (comptime builtin.mode == .Debug) {\n        for (opts.filter_scopes) |fs| {\n            if (fs == scope) {\n                return false;\n            }\n        }\n    }\n\n    return true;\n}\n\n// Ugliness to support complex debug parameters. Could add better support for\n// this directly in writeValue, but we [currently] only need this in one place\n// and I kind of don't want to encourage / make this easy.\npub fn separator() []const u8 {\n    return if (opts.format == .pretty) \"\\n        \" else \"; \";\n}\n\npub const Level = enum {\n    debug,\n    info,\n    warn,\n    err,\n    fatal,\n};\n\npub const Format = enum {\n    logfmt,\n    pretty,\n};\n\npub fn debug(comptime scope: Scope, comptime msg: []const u8, data: anytype) void {\n    log(scope, .debug, msg, data);\n}\n\npub fn info(comptime scope: Scope, comptime msg: []const u8, data: anytype) void {\n    log(scope, .info, msg, data);\n}\n\npub fn warn(comptime scope: Scope, comptime msg: []const u8, data: anytype) void {\n    log(scope, .warn, msg, data);\n}\n\npub fn err(comptime scope: Scope, comptime msg: []const u8, data: anytype) void {\n    log(scope, .err, msg, data);\n}\n\npub fn fatal(comptime scope: Scope, comptime msg: []const u8, data: anytype) void {\n    log(scope, .fatal, msg, data);\n}\n\npub fn log(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype) void {\n    if (enabled(scope, level) == false) {\n        return;\n    }\n\n    std.debug.lockStdErr();\n    defer std.debug.unlockStdErr();\n\n    var buf: [4096]u8 = undefined;\n    var stderr = std.fs.File.stderr();\n    var writer = stderr.writer(&buf);\n\n    logTo(scope, level, msg, data, &writer.interface) catch |log_err| {\n        std.debug.print(\"$time={d} $level=fatal $scope={s} $msg=\\\"log err\\\" err={s} log_msg=\\\"{s}\\\"\\n\", .{ timestamp(.clock), @errorName(log_err), @tagName(scope), msg });\n    };\n}\n\nfn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, out: *std.Io.Writer) !void {\n    comptime {\n        if (msg.len > 30) {\n            @compileError(\"log msg cannot be more than 30 characters: '\" ++ msg ++ \"'\");\n        }\n        for (msg) |b| {\n            switch (b) {\n                'A'...'Z', 'a'...'z', ' ', '0'...'9', '_', '-', '.', '{', '}' => {},\n                else => @compileError(\"log msg contains an invalid character '\" ++ msg ++ \"'\"),\n            }\n        }\n    }\n    switch (opts.format) {\n        .logfmt => try logLogfmt(scope, level, msg, data, out),\n        .pretty => try logPretty(scope, level, msg, data, out),\n    }\n    out.flush() catch return;\n}\n\nfn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: *std.Io.Writer) !void {\n    try logLogFmtPrefix(scope, level, msg, writer);\n    inline for (@typeInfo(@TypeOf(data)).@\"struct\".fields) |f| {\n        const value = @field(data, f.name);\n        if (std.meta.hasMethod(@TypeOf(value), \"logFmt\")) {\n            try value.logFmt(f.name, LogFormatWriter{ .writer = writer });\n        } else {\n            const key = \" \" ++ f.name ++ \"=\";\n            try writer.writeAll(key);\n            try writeValue(.logfmt, value, writer);\n        }\n    }\n    try writer.writeByte('\\n');\n}\n\nfn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: *std.Io.Writer) !void {\n    try writer.writeAll(\"$time=\");\n    try writer.print(\"{d}\", .{timestamp(.clock)});\n\n    try writer.writeAll(\" $scope=\");\n    try writer.writeAll(@tagName(scope));\n\n    try writer.writeAll(\" $level=\");\n    try writer.writeAll(if (level == .err) \"error\" else @tagName(level));\n\n    const full_msg = comptime blk: {\n        // only wrap msg in quotes if it contains a space\n        const prefix = \" $msg=\";\n        if (std.mem.indexOfScalar(u8, msg, ' ') == null) {\n            break :blk prefix ++ msg;\n        }\n        break :blk prefix ++ \"\\\"\" ++ msg ++ \"\\\"\";\n    };\n    try writer.writeAll(full_msg);\n}\n\nfn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: *std.Io.Writer) !void {\n    try logPrettyPrefix(scope, level, msg, writer);\n    inline for (@typeInfo(@TypeOf(data)).@\"struct\".fields) |f| {\n        const key = \"      \" ++ f.name ++ \" = \";\n        try writer.writeAll(key);\n        try writeValue(.pretty, @field(data, f.name), writer);\n        try writer.writeByte('\\n');\n    }\n    try writer.writeByte('\\n');\n}\n\nfn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: *std.Io.Writer) !void {\n    if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, \"lightpanda\")) {\n        try writer.writeAll(\"\\x1b[0;104mWARN  \");\n    } else {\n        try writer.writeAll(switch (level) {\n            .debug => \"\\x1b[0;36mDEBUG\\x1b[0m \",\n            .info => \"\\x1b[0;32mINFO\\x1b[0m  \",\n            .warn => \"\\x1b[0;33mWARN\\x1b[0m  \",\n            .err => \"\\x1b[0;31mERROR \",\n            .fatal => \"\\x1b[0;35mFATAL \",\n        });\n    }\n\n    const prefix = @tagName(scope) ++ \" : \" ++ msg;\n    try writer.writeAll(prefix);\n\n    {\n        // msg.len cannot be > 30, and @tagName(scope).len cannot be > 15\n        // so this is safe\n        const padding = 55 - prefix.len;\n        for (0..padding / 2) |_| {\n            try writer.writeAll(\" .\");\n        }\n        if (@mod(padding, 2) == 1) {\n            try writer.writeByte(' ');\n        }\n        const el = elapsed();\n        try writer.print(\" \\x1b[0m[+{d}{s}]\", .{ el.time, el.unit });\n        try writer.writeByte('\\n');\n    }\n}\n\npub fn writeValue(comptime format: Format, value: anytype, writer: *std.Io.Writer) !void {\n    const T = @TypeOf(value);\n    if (std.meta.hasMethod(T, \"format\")) {\n        return writer.print(\"{f}\", .{value});\n    }\n\n    switch (@typeInfo(T)) {\n        .optional => {\n            if (value) |v| {\n                return writeValue(format, v, writer);\n            }\n            return writer.writeAll(\"null\");\n        },\n        .comptime_int, .int, .comptime_float, .float => {\n            return writer.print(\"{d}\", .{value});\n        },\n        .bool => {\n            return writer.writeAll(if (value) \"true\" else \"false\");\n        },\n        .error_set => return writer.writeAll(@errorName(value)),\n        .@\"enum\" => return writer.writeAll(@tagName(value)),\n        .array => return writeValue(format, &value, writer),\n        .pointer => |ptr| switch (ptr.size) {\n            .slice => switch (ptr.child) {\n                u8 => return writeString(format, value, writer),\n                else => {},\n            },\n            .one => switch (@typeInfo(ptr.child)) {\n                .array => |arr| if (arr.child == u8) {\n                    return writeString(format, value, writer);\n                },\n                else => return writer.print(\"{f}\", .{value}),\n            },\n            else => {},\n        },\n        .@\"union\" => return writer.print(\"{}\", .{value}),\n        .@\"struct\" => return writer.print(\"{}\", .{value}),\n        else => {},\n    }\n\n    @compileError(\"cannot log a: \" ++ @typeName(T));\n}\n\nfn writeString(comptime format: Format, value: []const u8, writer: *std.Io.Writer) !void {\n    if (format == .pretty) {\n        return writer.writeAll(value);\n    }\n\n    var space_count: usize = 0;\n    var escape_count: usize = 0;\n    var binary_count: usize = 0;\n\n    for (value) |b| {\n        switch (b) {\n            '\\r', '\\n', '\"' => escape_count += 1,\n            ' ' => space_count += 1,\n            '\\t', '!', '#'...'~' => {}, // printable characters\n            else => binary_count += 1,\n        }\n    }\n\n    if (binary_count > 0) {\n        // TODO: use a different encoding if the ratio of binary data / printable is low\n        return std.base64.standard_no_pad.Encoder.encodeWriter(writer, value);\n    }\n\n    if (escape_count == 0) {\n        if (space_count == 0) {\n            return writer.writeAll(value);\n        }\n        try writer.writeByte('\"');\n        try writer.writeAll(value);\n        try writer.writeByte('\"');\n        return;\n    }\n\n    try writer.writeByte('\"');\n\n    var rest = value;\n    while (rest.len > 0) {\n        const pos = std.mem.indexOfAny(u8, rest, \"\\r\\n\\\"\") orelse {\n            try writer.writeAll(rest);\n            break;\n        };\n        try writer.writeAll(rest[0..pos]);\n        try writer.writeByte('\\\\');\n        switch (rest[pos]) {\n            '\"' => try writer.writeByte('\"'),\n            '\\r' => try writer.writeByte('r'),\n            '\\n' => try writer.writeByte('n'),\n            else => unreachable,\n        }\n        rest = rest[pos + 1 ..];\n    }\n    return writer.writeByte('\"');\n}\n\npub const LogFormatWriter = struct {\n    writer: *std.Io.Writer,\n\n    pub fn write(self: LogFormatWriter, key: []const u8, value: anytype) !void {\n        const writer = self.writer;\n        try writer.print(\" {s}=\", .{key});\n        try writeValue(.logfmt, value, writer);\n    }\n};\n\nvar first_log: u64 = 0;\nfn elapsed() struct { time: f64, unit: []const u8 } {\n    const now = timestamp(.monotonic);\n\n    last_log_lock.lock();\n    defer last_log_lock.unlock();\n\n    if (first_log == 0) {\n        first_log = now;\n    }\n\n    const e = now - first_log;\n    if (e < 10_000) {\n        return .{ .time = @floatFromInt(e), .unit = \"ms\" };\n    }\n    return .{ .time = @as(f64, @floatFromInt(e)) / @as(f64, 1000), .unit = \"s\" };\n}\n\nconst datetime = @import(\"datetime.zig\");\nfn timestamp(comptime mode: datetime.TimestampMode) u64 {\n    if (comptime @import(\"builtin\").is_test) {\n        return 1739795092929;\n    }\n    return datetime.milliTimestamp(mode);\n}\n\nconst testing = @import(\"testing.zig\");\ntest \"log: data\" {\n    opts.format = .logfmt;\n    defer opts.format = .pretty;\n\n    var aw = std.Io.Writer.Allocating.init(testing.allocator);\n    defer aw.deinit();\n\n    {\n        try logTo(.browser, .err, \"nope\", .{}, &aw.writer);\n        try testing.expectEqual(\"$time=1739795092929 $scope=browser $level=error $msg=nope\\n\", aw.written());\n    }\n\n    {\n        aw.clearRetainingCapacity();\n        const string = try testing.allocator.dupe(u8, \"spice_must_flow\");\n        defer testing.allocator.free(string);\n\n        try logTo(.page, .warn, \"a msg\", .{\n            .cint = 5,\n            .cfloat = 3.43,\n            .int = @as(i16, -49),\n            .float = @as(f32, 0.0003232),\n            .bt = true,\n            .bf = false,\n            .nn = @as(?i32, 33),\n            .n = @as(?i32, null),\n            .lit = \"over9000!\",\n            .slice = string,\n            .err = error.Nope,\n            .level = Level.warn,\n        }, &aw.writer);\n\n        try testing.expectEqual(\"$time=1739795092929 $scope=page $level=warn $msg=\\\"a msg\\\" \" ++\n            \"cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false \" ++\n            \"nn=33 n=null lit=over9000! slice=spice_must_flow \" ++\n            \"err=Nope level=warn\\n\", aw.written());\n    }\n}\n\ntest \"log: string escape\" {\n    opts.format = .logfmt;\n    defer opts.format = .pretty;\n\n    var aw = std.Io.Writer.Allocating.init(testing.allocator);\n    defer aw.deinit();\n\n    const prefix = \"$time=1739795092929 $scope=app $level=error $msg=test \";\n    {\n        try logTo(.app, .err, \"test\", .{ .string = \"hello world\" }, &aw.writer);\n        try testing.expectEqual(prefix ++ \"string=\\\"hello world\\\"\\n\", aw.written());\n    }\n\n    {\n        aw.clearRetainingCapacity();\n        try logTo(.app, .err, \"test\", .{ .string = \"\\n \\thi  \\\" \\\" \" }, &aw.writer);\n        try testing.expectEqual(prefix ++ \"string=\\\"\\\\n \\thi  \\\\\\\" \\\\\\\" \\\"\\n\", aw.written());\n    }\n}\n"
  },
  {
    "path": "src/main.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst builtin = @import(\"builtin\");\nconst Allocator = std.mem.Allocator;\n\nconst log = lp.log;\nconst App = lp.App;\nconst Config = lp.Config;\nconst SigHandler = @import(\"Sighandler.zig\");\npub const panic = lp.crash_handler.panic;\n\npub fn main() !void {\n    // allocator\n    // - in Debug mode we use the General Purpose Allocator to detect memory leaks\n    // - in Release mode we use the c allocator\n    var gpa_instance: std.heap.DebugAllocator(.{ .stack_trace_frames = 10 }) = .init;\n    const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;\n\n    defer if (builtin.mode == .Debug) {\n        if (gpa_instance.detectLeaks()) std.posix.exit(1);\n    };\n\n    // arena for main-specific allocations\n    var main_arena_instance = std.heap.ArenaAllocator.init(gpa);\n    const main_arena = main_arena_instance.allocator();\n    defer main_arena_instance.deinit();\n\n    run(gpa, main_arena) catch |err| {\n        log.fatal(.app, \"exit\", .{ .err = err });\n        std.posix.exit(1);\n    };\n}\n\nfn run(allocator: Allocator, main_arena: Allocator) !void {\n    const args = try Config.parseArgs(main_arena);\n    defer args.deinit(main_arena);\n\n    switch (args.mode) {\n        .help => {\n            args.printUsageAndExit(args.mode.help);\n            return std.process.cleanExit();\n        },\n        .version => {\n            if (lp.build_config.git_version) |version| {\n                std.debug.print(\"{s} ({s})\\n\", .{ version, lp.build_config.git_commit });\n            } else {\n                std.debug.print(\"{s}\\n\", .{lp.build_config.git_commit});\n            }\n            return std.process.cleanExit();\n        },\n        else => {},\n    }\n\n    if (args.logLevel()) |ll| {\n        log.opts.level = ll;\n    }\n    if (args.logFormat()) |lf| {\n        log.opts.format = lf;\n    }\n    if (args.logFilterScopes()) |lfs| {\n        log.opts.filter_scopes = lfs;\n    }\n\n    // must be installed before any other threads\n    const sighandler = try main_arena.create(SigHandler);\n    sighandler.* = .{ .arena = main_arena };\n    try sighandler.install();\n\n    // _app is global to handle graceful shutdown.\n    var app = try App.init(allocator, &args);\n    defer app.deinit();\n\n    try sighandler.on(lp.Network.stop, .{&app.network});\n\n    app.telemetry.record(.{ .run = {} });\n\n    switch (args.mode) {\n        .serve => |opts| {\n            log.debug(.app, \"startup\", .{ .mode = \"serve\", .snapshot = app.snapshot.fromEmbedded() });\n            const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {\n                log.fatal(.app, \"invalid server address\", .{ .err = err, .host = opts.host, .port = opts.port });\n                return args.printUsageAndExit(false);\n            };\n\n            var server = lp.Server.init(app, address) catch |err| {\n                if (err == error.AddressInUse) {\n                    log.fatal(.app, \"address already in use\", .{\n                        .host = opts.host,\n                        .port = opts.port,\n                        .hint = \"Another process is already listening on this address. \" ++\n                            \"Stop the other process or use --port to choose a different port.\",\n                    });\n                } else {\n                    log.fatal(.app, \"server run error\", .{ .err = err });\n                }\n                return err;\n            };\n            defer server.deinit();\n\n            try sighandler.on(lp.Server.shutdown, .{server});\n\n            app.network.run();\n        },\n        .fetch => |opts| {\n            const url = opts.url;\n            log.debug(.app, \"startup\", .{ .mode = \"fetch\", .dump_mode = opts.dump_mode, .url = url, .snapshot = app.snapshot.fromEmbedded() });\n\n            var fetch_opts = lp.FetchOpts{\n                .wait_ms = 5000,\n                .dump_mode = opts.dump_mode,\n                .dump = .{\n                    .strip = opts.strip,\n                    .with_base = opts.with_base,\n                    .with_frames = opts.with_frames,\n                },\n            };\n\n            var stdout = std.fs.File.stdout();\n            var writer = stdout.writer(&.{});\n            if (opts.dump_mode != null) {\n                fetch_opts.writer = &writer.interface;\n            }\n\n            var worker_thread = try std.Thread.spawn(.{}, fetchThread, .{ app, url, fetch_opts });\n            defer worker_thread.join();\n\n            app.network.run();\n        },\n        .mcp => {\n            log.info(.mcp, \"starting server\", .{});\n\n            log.opts.format = .logfmt;\n\n            var stdout = std.fs.File.stdout().writer(&.{});\n\n            var mcp_server: *lp.mcp.Server = try .init(allocator, app, &stdout.interface);\n            defer mcp_server.deinit();\n\n            var worker_thread = try std.Thread.spawn(.{}, mcpThread, .{ mcp_server, app });\n            defer worker_thread.join();\n\n            app.network.run();\n        },\n        else => unreachable,\n    }\n}\n\nfn fetchThread(app: *App, url: [:0]const u8, fetch_opts: lp.FetchOpts) void {\n    defer app.network.stop();\n    lp.fetch(app, url, fetch_opts) catch |err| {\n        log.fatal(.app, \"fetch error\", .{ .err = err, .url = url });\n    };\n}\n\nfn mcpThread(mcp_server: *lp.mcp.Server, app: *App) void {\n    defer app.network.stop();\n    var stdin_buf: [64 * 1024]u8 = undefined;\n    var stdin = std.fs.File.stdin().reader(&stdin_buf);\n    lp.mcp.router.processRequests(mcp_server, &stdin.interface) catch |err| {\n        log.fatal(.mcp, \"mcp error\", .{ .err = err });\n    };\n}\n"
  },
  {
    "path": "src/main_legacy_test.zig",
    "content": "const std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\nconst Allocator = std.mem.Allocator;\n\n// used in custom panic handler\nvar current_test: ?[]const u8 = null;\n\npub fn main() !void {\n    var gpa: std.heap.DebugAllocator(.{}) = .init;\n    defer _ = gpa.deinit();\n\n    const allocator = gpa.allocator();\n\n    var args = try std.process.argsWithAllocator(allocator);\n    defer args.deinit();\n    _ = args.next(); // executable name\n\n    var filter: ?[]const u8 = null;\n    if (args.next()) |n| {\n        filter = n;\n    }\n\n    var http_server = try TestHTTPServer.init();\n    defer http_server.deinit();\n\n    {\n        var wg: std.Thread.WaitGroup = .{};\n        wg.startMany(1);\n        var thrd = try std.Thread.spawn(.{}, TestHTTPServer.run, .{ &http_server, &wg });\n        thrd.detach();\n        wg.wait();\n    }\n    lp.log.opts.level = .warn;\n    const config = try lp.Config.init(allocator, \"legacy-test\", .{ .serve = .{\n        .common = .{\n            .tls_verify_host = false,\n            .user_agent_suffix = \"internal-tester\",\n        },\n    } });\n    defer config.deinit(allocator);\n\n    var app = try lp.App.init(allocator, &config);\n    defer app.deinit();\n\n    var test_arena = std.heap.ArenaAllocator.init(allocator);\n    defer test_arena.deinit();\n\n    const http_client = try lp.HttpClient.init(allocator, &app.network);\n    defer http_client.deinit();\n\n    var browser = try lp.Browser.init(app, .{ .http_client = http_client });\n    defer browser.deinit();\n\n    const notification = try lp.Notification.init(allocator);\n    defer notification.deinit();\n\n    const session = try browser.newSession(notification);\n    defer session.deinit();\n\n    var dir = try std.fs.cwd().openDir(\"src/browser/tests/legacy/\", .{ .iterate = true, .no_follow = true });\n    defer dir.close();\n\n    var walker = try dir.walk(allocator);\n    defer walker.deinit();\n\n    while (try walker.next()) |entry| {\n        _ = test_arena.reset(.retain_capacity);\n        if (entry.kind != .file) {\n            continue;\n        }\n\n        if (!std.mem.endsWith(u8, entry.basename, \".html\")) {\n            continue;\n        }\n\n        if (std.mem.endsWith(u8, entry.basename, \".skip.html\")) {\n            continue;\n        }\n\n        if (filter) |f| {\n            if (std.mem.indexOf(u8, entry.path, f) == null) {\n                continue;\n            }\n        }\n        std.debug.print(\"\\n===={s}====\\n\", .{entry.path});\n        current_test = entry.path;\n        run(test_arena.allocator(), entry.path, session) catch |err| {\n            std.debug.print(\"Failure: {s} - {any}\\n\", .{ entry.path, err });\n        };\n    }\n}\n\npub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void {\n    const url = try std.fmt.allocPrintSentinel(allocator, \"http://localhost:9589/{s}\", .{file}, 0);\n\n    const page = try session.createPage();\n    defer session.removePage();\n\n    var ls: lp.js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var try_catch: lp.js.TryCatch = undefined;\n    try_catch.init(&ls.local);\n    defer try_catch.deinit();\n\n    try page.navigate(url, .{});\n    _ = session.wait(2000);\n\n    ls.local.eval(\"testing.assertOk()\", \"testing.assertOk()\") catch |err| {\n        const caught = try_catch.caughtOrError(allocator, err);\n        std.debug.print(\"{s}: test failure\\nError: {f}\\n\", .{ file, caught });\n        return err;\n    };\n}\n\nconst TestHTTPServer = struct {\n    shutdown: bool,\n    dir: std.fs.Dir,\n    listener: ?std.net.Server,\n\n    pub fn init() !TestHTTPServer {\n        return .{\n            .dir = try std.fs.cwd().openDir(\"src/browser/tests/legacy/\", .{}),\n            .shutdown = true,\n            .listener = null,\n        };\n    }\n\n    pub fn deinit(self: *TestHTTPServer) void {\n        self.shutdown = true;\n        if (self.listener) |*listener| {\n            listener.deinit();\n        }\n        self.dir.close();\n    }\n\n    pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {\n        const address = try std.net.Address.parseIp(\"127.0.0.1\", 9589);\n\n        self.listener = try address.listen(.{ .reuse_address = true });\n        var listener = &self.listener.?;\n\n        wg.finish();\n\n        while (true) {\n            const conn = listener.accept() catch |err| {\n                if (self.shutdown) {\n                    return;\n                }\n                return err;\n            };\n            const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });\n            thrd.detach();\n        }\n    }\n\n    fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {\n        defer conn.stream.close();\n\n        var req_buf: [2048]u8 = undefined;\n        var conn_reader = conn.stream.reader(&req_buf);\n        var conn_writer = conn.stream.writer(&req_buf);\n\n        var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);\n\n        while (true) {\n            var req = http_server.receiveHead() catch |err| switch (err) {\n                error.ReadFailed => continue,\n                error.HttpConnectionClosing => continue,\n                else => {\n                    std.debug.print(\"Test HTTP Server error: {}\\n\", .{err});\n                    return err;\n                },\n            };\n\n            self.handler(&req) catch |err| {\n                std.debug.print(\"test http error '{s}': {}\\n\", .{ req.head.target, err });\n                try req.respond(\"server error\", .{ .status = .internal_server_error });\n                return;\n            };\n        }\n    }\n\n    fn handler(server: *TestHTTPServer, req: *std.http.Server.Request) !void {\n        const path = req.head.target;\n\n        if (std.mem.eql(u8, path, \"/xhr\")) {\n            return req.respond(\"1234567890\" ** 10, .{\n                .extra_headers = &.{\n                    .{ .name = \"Content-Type\", .value = \"text/html; charset=utf-8\" },\n                },\n            });\n        }\n\n        if (std.mem.eql(u8, path, \"/xhr/json\")) {\n            return req.respond(\"{\\\"over\\\":\\\"9000!!!\\\"}\", .{\n                .extra_headers = &.{\n                    .{ .name = \"Content-Type\", .value = \"application/json\" },\n                },\n            });\n        }\n\n        // strip out leading '/' to make the path relative\n        const file = try server.dir.openFile(path[1..], .{});\n        defer file.close();\n\n        const stat = try file.stat();\n        var send_buffer: [4096]u8 = undefined;\n\n        var res = try req.respondStreaming(&send_buffer, .{\n            .content_length = stat.size,\n            .respond_options = .{\n                .extra_headers = &.{\n                    .{ .name = \"content-type\", .value = getContentType(path) },\n                },\n            },\n        });\n\n        var read_buffer: [4096]u8 = undefined;\n        var reader = file.reader(&read_buffer);\n        _ = try res.writer.sendFileAll(&reader, .unlimited);\n        try res.writer.flush();\n        try res.end();\n    }\n\n    pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {\n        var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {\n            error.FileNotFound => return req.respond(\"server error\", .{ .status = .not_found }),\n            else => return err,\n        };\n        defer file.close();\n\n        const stat = try file.stat();\n        var send_buffer: [4096]u8 = undefined;\n\n        var res = try req.respondStreaming(&send_buffer, .{\n            .content_length = stat.size,\n            .respond_options = .{\n                .extra_headers = &.{\n                    .{ .name = \"content-type\", .value = getContentType(file_path) },\n                },\n            },\n        });\n\n        var read_buffer: [4096]u8 = undefined;\n        var reader = file.reader(&read_buffer);\n        _ = try res.writer.sendFileAll(&reader, .unlimited);\n        try res.writer.flush();\n        try res.end();\n    }\n\n    fn getContentType(file_path: []const u8) []const u8 {\n        if (std.mem.endsWith(u8, file_path, \".js\")) {\n            return \"application/json\";\n        }\n\n        if (std.mem.endsWith(u8, file_path, \".html\")) {\n            return \"text/html\";\n        }\n\n        if (std.mem.endsWith(u8, file_path, \".htm\")) {\n            return \"text/html\";\n        }\n\n        if (std.mem.endsWith(u8, file_path, \".xml\")) {\n            // some wpt tests do this\n            return \"text/xml\";\n        }\n\n        std.debug.print(\"TestHTTPServer asked to serve an unknown file type: {s}\\n\", .{file_path});\n        return \"text/html\";\n    }\n};\n\npub const panic = std.debug.FullPanic(struct {\n    pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {\n        if (current_test) |ct| {\n            std.debug.print(\"===panic running: {s}===\\n\", .{ct});\n        }\n        std.debug.defaultPanic(msg, first_trace_addr);\n    }\n}.panicFn);\n"
  },
  {
    "path": "src/main_snapshot_creator.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\n\npub fn main() !void {\n    const allocator = std.heap.c_allocator;\n\n    var platform = try lp.js.Platform.init();\n    defer platform.deinit();\n\n    const snapshot = try lp.js.Snapshot.create();\n    defer snapshot.deinit();\n\n    var is_stdout = true;\n    var file = std.fs.File.stdout();\n    var args = try std.process.argsWithAllocator(allocator);\n    _ = args.next(); // executable name\n    if (args.next()) |n| {\n        is_stdout = false;\n        file = try std.fs.cwd().createFile(n, .{});\n    }\n    defer if (!is_stdout) {\n        file.close();\n    };\n\n    var buffer: [4096]u8 = undefined;\n    var writer = file.writer(&buffer);\n    try snapshot.write(&writer.interface);\n    try writer.end();\n}\n"
  },
  {
    "path": "src/mcp/Server.zig",
    "content": "const std = @import(\"std\");\n\nconst lp = @import(\"lightpanda\");\n\nconst App = @import(\"../App.zig\");\nconst HttpClient = @import(\"../browser/HttpClient.zig\");\nconst testing = @import(\"../testing.zig\");\nconst protocol = @import(\"protocol.zig\");\nconst router = @import(\"router.zig\");\nconst CDPNode = @import(\"../cdp/Node.zig\");\n\nconst Self = @This();\n\nallocator: std.mem.Allocator,\napp: *App,\n\nhttp_client: *HttpClient,\nnotification: *lp.Notification,\nbrowser: lp.Browser,\nsession: *lp.Session,\nnode_registry: CDPNode.Registry,\n\nwriter: *std.io.Writer,\nmutex: std.Thread.Mutex = .{},\naw: std.io.Writer.Allocating,\n\npub fn init(allocator: std.mem.Allocator, app: *App, writer: *std.io.Writer) !*Self {\n    const http_client = try HttpClient.init(allocator, &app.network);\n    errdefer http_client.deinit();\n\n    const notification = try lp.Notification.init(allocator);\n    errdefer notification.deinit();\n\n    const self = try allocator.create(Self);\n    errdefer allocator.destroy(self);\n\n    var browser = try lp.Browser.init(app, .{ .http_client = http_client });\n    errdefer browser.deinit();\n\n    self.* = .{\n        .allocator = allocator,\n        .app = app,\n        .writer = writer,\n        .browser = browser,\n        .aw = .init(allocator),\n        .http_client = http_client,\n        .notification = notification,\n        .session = undefined,\n        .node_registry = CDPNode.Registry.init(allocator),\n    };\n\n    self.session = try self.browser.newSession(self.notification);\n    return self;\n}\n\npub fn deinit(self: *Self) void {\n    self.node_registry.deinit();\n    self.aw.deinit();\n    self.browser.deinit();\n    self.notification.deinit();\n    self.http_client.deinit();\n\n    self.allocator.destroy(self);\n}\n\npub fn sendResponse(self: *Self, response: anytype) !void {\n    self.mutex.lock();\n    defer self.mutex.unlock();\n\n    self.aw.clearRetainingCapacity();\n    try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &self.aw.writer);\n    try self.aw.writer.writeByte('\\n');\n    try self.writer.writeAll(self.aw.writer.buffered());\n    try self.writer.flush();\n}\n\npub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void {\n    const GenericResponse = struct {\n        jsonrpc: []const u8 = \"2.0\",\n        id: std.json.Value,\n        result: @TypeOf(result),\n    };\n    try self.sendResponse(GenericResponse{\n        .id = id,\n        .result = result,\n    });\n}\n\npub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void {\n    try self.sendResponse(.{\n        .id = id,\n        .@\"error\" = protocol.Error{\n            .code = @intFromEnum(code),\n            .message = message,\n        },\n    });\n}\n\ntest \"MCP.Server - Integration: synchronous smoke test\" {\n    defer testing.reset();\n    const allocator = testing.allocator;\n    const app = testing.test_app;\n\n    const input =\n        \\\\{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test-client\",\"version\":\"1.0.0\"}}}\n    ;\n\n    var in_reader: std.io.Reader = .fixed(input);\n    var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);\n    defer out_alloc.deinit();\n\n    var server = try Self.init(allocator, app, &out_alloc.writer);\n    defer server.deinit();\n\n    try router.processRequests(server, &in_reader);\n\n    try testing.expectJson(.{ .id = 1 }, out_alloc.writer.buffered());\n}\n\ntest \"MCP.Server - Integration: ping request returns an empty result\" {\n    defer testing.reset();\n    const allocator = testing.allocator;\n    const app = testing.test_app;\n\n    const input =\n        \\\\{\"jsonrpc\":\"2.0\",\"id\":\"ping-1\",\"method\":\"ping\"}\n    ;\n\n    var in_reader: std.io.Reader = .fixed(input);\n    var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);\n    defer out_alloc.deinit();\n\n    var server = try Self.init(allocator, app, &out_alloc.writer);\n    defer server.deinit();\n\n    try router.processRequests(server, &in_reader);\n\n    try testing.expectJson(.{ .id = \"ping-1\", .result = .{} }, out_alloc.writer.buffered());\n}\n"
  },
  {
    "path": "src/mcp/protocol.zig",
    "content": "const std = @import(\"std\");\n\npub const Request = struct {\n    jsonrpc: []const u8 = \"2.0\",\n    id: ?std.json.Value = null,\n    method: []const u8,\n    params: ?std.json.Value = null,\n};\n\npub const Response = struct {\n    jsonrpc: []const u8 = \"2.0\",\n    id: std.json.Value,\n    result: ?std.json.Value = null,\n    @\"error\": ?Error = null,\n};\n\npub const Error = struct {\n    code: i64,\n    message: []const u8,\n    data: ?std.json.Value = null,\n};\n\npub const ErrorCode = enum(i64) {\n    ParseError = -32700,\n    InvalidRequest = -32600,\n    MethodNotFound = -32601,\n    InvalidParams = -32602,\n    InternalError = -32603,\n    PageNotLoaded = -32604,\n};\n\npub const Notification = struct {\n    jsonrpc: []const u8 = \"2.0\",\n    method: []const u8,\n    params: ?std.json.Value = null,\n};\n\n// Core MCP Types mapping to official specification\npub const InitializeRequest = struct {\n    jsonrpc: []const u8 = \"2.0\",\n    id: std.json.Value,\n    method: []const u8 = \"initialize\",\n    params: InitializeParams,\n};\n\npub const InitializeParams = struct {\n    protocolVersion: []const u8,\n    capabilities: Capabilities,\n    clientInfo: Implementation,\n};\n\npub const Capabilities = struct {\n    experimental: ?std.json.Value = null,\n    roots: ?RootsCapability = null,\n    sampling: ?SamplingCapability = null,\n};\n\npub const RootsCapability = struct {\n    listChanged: ?bool = null,\n};\n\npub const SamplingCapability = struct {};\n\npub const Implementation = struct {\n    name: []const u8,\n    version: []const u8,\n};\n\npub const InitializeResult = struct {\n    protocolVersion: []const u8,\n    capabilities: ServerCapabilities,\n    serverInfo: Implementation,\n};\n\npub const ServerCapabilities = struct {\n    experimental: ?std.json.Value = null,\n    logging: ?LoggingCapability = null,\n    prompts: ?PromptsCapability = null,\n    resources: ?ResourcesCapability = null,\n    tools: ?ToolsCapability = null,\n};\n\npub const LoggingCapability = struct {};\npub const PromptsCapability = struct {\n    listChanged: ?bool = null,\n};\npub const ResourcesCapability = struct {\n    subscribe: ?bool = null,\n    listChanged: ?bool = null,\n};\npub const ToolsCapability = struct {\n    listChanged: ?bool = null,\n};\n\npub const Tool = struct {\n    name: []const u8,\n    description: ?[]const u8 = null,\n    inputSchema: []const u8,\n\n    pub fn jsonStringify(self: @This(), jw: anytype) !void {\n        try jw.beginObject();\n        try jw.objectField(\"name\");\n        try jw.write(self.name);\n        if (self.description) |d| {\n            try jw.objectField(\"description\");\n            try jw.write(d);\n        }\n        try jw.objectField(\"inputSchema\");\n        _ = try jw.beginWriteRaw();\n        try jw.writer.writeAll(self.inputSchema);\n        jw.endWriteRaw();\n        try jw.endObject();\n    }\n};\n\npub fn minify(comptime json: []const u8) []const u8 {\n    @setEvalBranchQuota(100000);\n    return comptime blk: {\n        var res: []const u8 = \"\";\n        var in_string = false;\n        var escaped = false;\n        for (json) |c| {\n            if (in_string) {\n                res = res ++ [1]u8{c};\n                if (escaped) {\n                    escaped = false;\n                } else if (c == '\\\\') {\n                    escaped = true;\n                } else if (c == '\"') {\n                    in_string = false;\n                }\n            } else {\n                switch (c) {\n                    ' ', '\\n', '\\r', '\\t' => continue,\n                    '\"' => {\n                        in_string = true;\n                        res = res ++ [1]u8{c};\n                    },\n                    else => res = res ++ [1]u8{c},\n                }\n            }\n        }\n        break :blk res;\n    };\n}\n\npub const Resource = struct {\n    uri: []const u8,\n    name: []const u8,\n    description: ?[]const u8 = null,\n    mimeType: ?[]const u8 = null,\n};\n\npub fn TextContent(comptime T: type) type {\n    return struct {\n        type: []const u8 = \"text\",\n        text: T,\n    };\n}\n\npub fn CallToolResult(comptime T: type) type {\n    return struct {\n        content: []const TextContent(T),\n        isError: bool = false,\n    };\n}\n\npub const JsonEscapingWriter = struct {\n    inner_writer: *std.Io.Writer,\n    writer: std.Io.Writer,\n\n    pub fn init(inner_writer: *std.Io.Writer) JsonEscapingWriter {\n        return .{\n            .inner_writer = inner_writer,\n            .writer = .{\n                .vtable = &vtable,\n                .buffer = &.{},\n            },\n        };\n    }\n\n    const vtable = std.Io.Writer.VTable{\n        .drain = drain,\n    };\n\n    fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {\n        const self: *JsonEscapingWriter = @alignCast(@fieldParentPtr(\"writer\", w));\n        var total: usize = 0;\n        for (data[0 .. data.len - 1]) |slice| {\n            std.json.Stringify.encodeJsonStringChars(slice, .{}, self.inner_writer) catch return error.WriteFailed;\n            total += slice.len;\n        }\n        const pattern = data[data.len - 1];\n        for (0..splat) |_| {\n            std.json.Stringify.encodeJsonStringChars(pattern, .{}, self.inner_writer) catch return error.WriteFailed;\n            total += pattern.len;\n        }\n        return total;\n    }\n};\n\nconst testing = @import(\"../testing.zig\");\n\ntest \"MCP.protocol - request parsing\" {\n    defer testing.reset();\n    const raw_json =\n        \\\\{\n        \\\\  \"jsonrpc\": \"2.0\",\n        \\\\  \"id\": 1,\n        \\\\  \"method\": \"initialize\",\n        \\\\  \"params\": {\n        \\\\    \"protocolVersion\": \"2024-11-05\",\n        \\\\    \"capabilities\": {},\n        \\\\    \"clientInfo\": {\n        \\\\      \"name\": \"test-client\",\n        \\\\      \"version\": \"1.0.0\"\n        \\\\    }\n        \\\\  }\n        \\\\}\n    ;\n\n    const parsed = try std.json.parseFromSlice(Request, testing.arena_allocator, raw_json, .{ .ignore_unknown_fields = true });\n    defer parsed.deinit();\n\n    const req = parsed.value;\n    try testing.expectString(\"2.0\", req.jsonrpc);\n    try testing.expectString(\"initialize\", req.method);\n    try testing.expect(req.id.? == .integer);\n    try testing.expectEqual(@as(i64, 1), req.id.?.integer);\n    try testing.expect(req.params != null);\n\n    // Test nested parsing of InitializeParams\n    const init_params = try std.json.parseFromValue(InitializeParams, testing.arena_allocator, req.params.?, .{ .ignore_unknown_fields = true });\n    defer init_params.deinit();\n\n    try testing.expectString(\"2024-11-05\", init_params.value.protocolVersion);\n    try testing.expectString(\"test-client\", init_params.value.clientInfo.name);\n    try testing.expectString(\"1.0.0\", init_params.value.clientInfo.version);\n}\n\ntest \"MCP.protocol - ping request parsing\" {\n    defer testing.reset();\n    const raw_json =\n        \\\\{\n        \\\\  \"jsonrpc\": \"2.0\",\n        \\\\  \"id\": \"123\",\n        \\\\  \"method\": \"ping\"\n        \\\\}\n    ;\n\n    const parsed = try std.json.parseFromSlice(Request, testing.arena_allocator, raw_json, .{ .ignore_unknown_fields = true });\n    defer parsed.deinit();\n\n    const req = parsed.value;\n    try testing.expectString(\"2.0\", req.jsonrpc);\n    try testing.expectString(\"ping\", req.method);\n    try testing.expect(req.id.? == .string);\n    try testing.expectString(\"123\", req.id.?.string);\n    try testing.expectEqual(null, req.params);\n}\n\ntest \"MCP.protocol - response formatting\" {\n    defer testing.reset();\n    const response = Response{\n        .id = .{ .integer = 42 },\n        .result = .{ .string = \"success\" },\n    };\n\n    var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);\n    defer aw.deinit();\n    try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &aw.writer);\n\n    try testing.expectString(\"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":42,\\\"result\\\":\\\"success\\\"}\", aw.written());\n}\n\ntest \"MCP.protocol - error formatting\" {\n    defer testing.reset();\n    const response = Response{\n        .id = .{ .string = \"abc\" },\n        .@\"error\" = .{\n            .code = @intFromEnum(ErrorCode.MethodNotFound),\n            .message = \"Method not found\",\n        },\n    };\n\n    var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);\n    defer aw.deinit();\n    try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &aw.writer);\n\n    try testing.expectString(\"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":\\\"abc\\\",\\\"error\\\":{\\\"code\\\":-32601,\\\"message\\\":\\\"Method not found\\\"}}\", aw.written());\n}\n\ntest \"MCP.protocol - JsonEscapingWriter\" {\n    defer testing.reset();\n    var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);\n    defer aw.deinit();\n\n    var escaping_writer = JsonEscapingWriter.init(&aw.writer);\n\n    // test newlines and quotes\n    try escaping_writer.writer.writeAll(\"hello\\n\\\"world\\\"\");\n\n    // the writer outputs escaped string chars without surrounding quotes\n    try testing.expectString(\"hello\\\\n\\\\\\\"world\\\\\\\"\", aw.written());\n}\n\ntest \"MCP.protocol - Tool serialization\" {\n    defer testing.reset();\n    const t = Tool{\n        .name = \"test\",\n        .inputSchema = minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"foo\": { \"type\": \"string\" }\n            \\\\  }\n            \\\\}\n        ),\n    };\n\n    var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);\n    defer aw.deinit();\n\n    try std.json.Stringify.value(t, .{}, &aw.writer);\n\n    try testing.expectString(\"{\\\"name\\\":\\\"test\\\",\\\"inputSchema\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"foo\\\":{\\\"type\\\":\\\"string\\\"}}}}\", aw.written());\n}\n"
  },
  {
    "path": "src/mcp/resources.zig",
    "content": "const std = @import(\"std\");\n\nconst lp = @import(\"lightpanda\");\nconst log = lp.log;\n\nconst protocol = @import(\"protocol.zig\");\nconst Server = @import(\"Server.zig\");\n\npub const resource_list = [_]protocol.Resource{\n    .{\n        .uri = \"mcp://page/html\",\n        .name = \"Page HTML\",\n        .description = \"The serialized HTML DOM of the current page\",\n        .mimeType = \"text/html\",\n    },\n    .{\n        .uri = \"mcp://page/markdown\",\n        .name = \"Page Markdown\",\n        .description = \"The token-efficient markdown representation of the current page\",\n        .mimeType = \"text/markdown\",\n    },\n};\n\npub fn handleList(server: *Server, req: protocol.Request) !void {\n    try server.sendResult(req.id.?, .{ .resources = &resource_list });\n}\n\nconst ReadParams = struct {\n    uri: []const u8,\n};\n\nconst ResourceStreamingResult = struct {\n    contents: []const struct {\n        uri: []const u8,\n        mimeType: []const u8,\n        text: StreamingText,\n    },\n\n    const StreamingText = struct {\n        page: *lp.Page,\n        format: enum { html, markdown },\n\n        pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {\n            try jw.beginWriteRaw();\n            try jw.writer.writeByte('\"');\n            var escaped = protocol.JsonEscapingWriter.init(jw.writer);\n            switch (self.format) {\n                .html => lp.dump.root(self.page.document, .{}, &escaped.writer, self.page) catch |err| {\n                    log.err(.mcp, \"html dump failed\", .{ .err = err });\n                },\n                .markdown => lp.markdown.dump(self.page.document.asNode(), .{}, &escaped.writer, self.page) catch |err| {\n                    log.err(.mcp, \"markdown dump failed\", .{ .err = err });\n                },\n            }\n            try jw.writer.writeByte('\"');\n            jw.endWriteRaw();\n        }\n    };\n};\n\nconst ResourceUri = enum {\n    @\"mcp://page/html\",\n    @\"mcp://page/markdown\",\n};\n\nconst resource_map = std.StaticStringMap(ResourceUri).initComptime(.{\n    .{ \"mcp://page/html\", .@\"mcp://page/html\" },\n    .{ \"mcp://page/markdown\", .@\"mcp://page/markdown\" },\n});\n\npub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {\n    if (req.params == null or req.id == null) {\n        return server.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, \"Missing params\");\n    }\n    const req_id = req.id.?;\n\n    const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch {\n        return server.sendError(req_id, .InvalidParams, \"Invalid params\");\n    };\n\n    const uri = resource_map.get(params.uri) orelse {\n        return server.sendError(req_id, .InvalidRequest, \"Resource not found\");\n    };\n\n    const page = server.session.currentPage() orelse {\n        return server.sendError(req_id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    switch (uri) {\n        .@\"mcp://page/html\" => {\n            const result: ResourceStreamingResult = .{\n                .contents = &.{.{\n                    .uri = params.uri,\n                    .mimeType = \"text/html\",\n                    .text = .{ .page = page, .format = .html },\n                }},\n            };\n            try server.sendResult(req_id, result);\n        },\n        .@\"mcp://page/markdown\" => {\n            const result: ResourceStreamingResult = .{\n                .contents = &.{.{\n                    .uri = params.uri,\n                    .mimeType = \"text/markdown\",\n                    .text = .{ .page = page, .format = .markdown },\n                }},\n            };\n            try server.sendResult(req_id, result);\n        },\n    }\n}\n\nconst testing = @import(\"../testing.zig\");\n"
  },
  {
    "path": "src/mcp/router.zig",
    "content": "const std = @import(\"std\");\nconst lp = @import(\"lightpanda\");\nconst protocol = @import(\"protocol.zig\");\nconst resources = @import(\"resources.zig\");\nconst Server = @import(\"Server.zig\");\nconst tools = @import(\"tools.zig\");\n\npub fn processRequests(server: *Server, reader: *std.io.Reader) !void {\n    var arena: std.heap.ArenaAllocator = .init(server.allocator);\n    defer arena.deinit();\n\n    while (true) {\n        _ = arena.reset(.retain_capacity);\n        const aa = arena.allocator();\n\n        const buffered_line = reader.takeDelimiter('\\n') catch |err| switch (err) {\n            error.StreamTooLong => {\n                log.err(.mcp, \"Message too long\", .{});\n                continue;\n            },\n            else => return err,\n        } orelse break;\n\n        const trimmed = std.mem.trim(u8, buffered_line, \" \\r\\t\");\n        if (trimmed.len > 0) {\n            handleMessage(server, aa, trimmed) catch |err| {\n                log.err(.mcp, \"Failed to handle message\", .{ .err = err, .msg = trimmed });\n            };\n        }\n    }\n}\n\nconst log = @import(\"../log.zig\");\n\nconst Method = enum {\n    initialize,\n    ping,\n    @\"notifications/initialized\",\n    @\"tools/list\",\n    @\"tools/call\",\n    @\"resources/list\",\n    @\"resources/read\",\n};\n\nconst method_map = std.StaticStringMap(Method).initComptime(.{\n    .{ \"initialize\", .initialize },\n    .{ \"ping\", .ping },\n    .{ \"notifications/initialized\", .@\"notifications/initialized\" },\n    .{ \"tools/list\", .@\"tools/list\" },\n    .{ \"tools/call\", .@\"tools/call\" },\n    .{ \"resources/list\", .@\"resources/list\" },\n    .{ \"resources/read\", .@\"resources/read\" },\n});\n\npub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) !void {\n    const req = std.json.parseFromSliceLeaky(protocol.Request, arena, msg, .{\n        .ignore_unknown_fields = true,\n    }) catch |err| {\n        log.warn(.mcp, \"JSON Parse Error\", .{ .err = err, .msg = msg });\n        try server.sendError(.null, .ParseError, \"Parse error\");\n        return;\n    };\n\n    const method = method_map.get(req.method) orelse {\n        if (req.id != null) {\n            try server.sendError(req.id.?, .MethodNotFound, \"Method not found\");\n        }\n        return;\n    };\n\n    switch (method) {\n        .initialize => try handleInitialize(server, req),\n        .ping => try handlePing(server, req),\n        .@\"notifications/initialized\" => {},\n        .@\"tools/list\" => try tools.handleList(server, arena, req),\n        .@\"tools/call\" => try tools.handleCall(server, arena, req),\n        .@\"resources/list\" => try resources.handleList(server, req),\n        .@\"resources/read\" => try resources.handleRead(server, arena, req),\n    }\n}\n\nfn handleInitialize(server: *Server, req: protocol.Request) !void {\n    const result = protocol.InitializeResult{\n        .protocolVersion = \"2025-11-25\",\n        .capabilities = .{\n            .resources = .{},\n            .tools = .{},\n        },\n        .serverInfo = .{\n            .name = \"lightpanda\",\n            .version = \"0.1.0\",\n        },\n    };\n\n    try server.sendResult(req.id.?, result);\n}\n\nfn handlePing(server: *Server, req: protocol.Request) !void {\n    const id = req.id orelse return;\n    try server.sendResult(id, .{});\n}\n\nconst testing = @import(\"../testing.zig\");\n\ntest \"MCP.router - handleMessage - synchronous unit tests\" {\n    defer testing.reset();\n    const allocator = testing.allocator;\n    const app = testing.test_app;\n\n    var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);\n    defer out_alloc.deinit();\n\n    var server = try Server.init(allocator, app, &out_alloc.writer);\n    defer server.deinit();\n\n    const aa = testing.arena_allocator;\n\n    // 1. Valid handshake\n    try handleMessage(server, aa,\n        \\\\{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test-client\",\"version\":\"1.0.0\"}}}\n    );\n    try testing.expectJson(\n        \\\\{ \"id\": 1, \"result\": { \"capabilities\": { \"tools\": {} } } }\n    , out_alloc.writer.buffered());\n    out_alloc.writer.end = 0;\n\n    // 2. Ping\n    try handleMessage(server, aa,\n        \\\\{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}\n    );\n    try testing.expectJson(.{ .id = 2, .result = .{} }, out_alloc.writer.buffered());\n    out_alloc.writer.end = 0;\n\n    // 3. Tools list\n    try handleMessage(server, aa,\n        \\\\{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/list\"}\n    );\n    try testing.expectJson(.{ .id = 3 }, out_alloc.writer.buffered());\n    try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), \"\\\"name\\\":\\\"goto\\\"\") != null);\n    out_alloc.writer.end = 0;\n\n    // 4. Method not found\n    try handleMessage(server, aa,\n        \\\\{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"unknown_method\"}\n    );\n    try testing.expectJson(.{ .id = 4, .@\"error\" = .{ .code = -32601 } }, out_alloc.writer.buffered());\n    out_alloc.writer.end = 0;\n\n    // 5. Parse error\n    {\n        const filter: testing.LogFilter = .init(&.{.mcp});\n        defer filter.deinit();\n\n        try handleMessage(server, aa, \"invalid json\");\n        try testing.expectJson(\"{\\\"id\\\": null, \\\"error\\\": {\\\"code\\\": -32700}}\", out_alloc.writer.buffered());\n    }\n}\n"
  },
  {
    "path": "src/mcp/tools.zig",
    "content": "const std = @import(\"std\");\n\nconst lp = @import(\"lightpanda\");\nconst log = lp.log;\nconst js = lp.js;\n\nconst Element = @import(\"../browser/webapi/Element.zig\");\nconst DOMNode = @import(\"../browser/webapi/Node.zig\");\nconst Selector = @import(\"../browser/webapi/selector/Selector.zig\");\nconst protocol = @import(\"protocol.zig\");\nconst Server = @import(\"Server.zig\");\nconst CDPNode = @import(\"../cdp/Node.zig\");\n\npub const tool_list = [_]protocol.Tool{\n    .{\n        .name = \"goto\",\n        .description = \"Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"The URL to navigate to, must be a valid URL.\" }\n            \\\\  },\n            \\\\  \"required\": [\"url\"]\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"markdown\",\n        .description = \"Get the page content in markdown format. If a url is provided, it navigates to that url first.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"Optional URL to navigate to before fetching markdown.\" }\n            \\\\  }\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"links\",\n        .description = \"Extract all links in the opened page. If a url is provided, it navigates to that url first.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"Optional URL to navigate to before extracting links.\" }\n            \\\\  }\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"evaluate\",\n        .description = \"Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"script\": { \"type\": \"string\" },\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"Optional URL to navigate to before evaluating.\" }\n            \\\\  },\n            \\\\  \"required\": [\"script\"]\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"semantic_tree\",\n        .description = \"Get the page content as a simplified semantic DOM tree for AI reasoning. If a url is provided, it navigates to that url first.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"Optional URL to navigate to before fetching the semantic tree.\" },\n            \\\\    \"backendNodeId\": { \"type\": \"integer\", \"description\": \"Optional backend node ID to get the tree for a specific element instead of the document root.\" },\n            \\\\    \"maxDepth\": { \"type\": \"integer\", \"description\": \"Optional maximum depth of the tree to return. Useful for exploring high-level structure first.\" }\n            \\\\  }\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"interactiveElements\",\n        .description = \"Extract interactive elements from the opened page. If a url is provided, it navigates to that url first.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"Optional URL to navigate to before extracting interactive elements.\" }\n            \\\\  }\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"structuredData\",\n        .description = \"Extract structured data (like JSON-LD, OpenGraph, etc) from the opened page. If a url is provided, it navigates to that url first.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"url\": { \"type\": \"string\", \"description\": \"Optional URL to navigate to before extracting structured data.\" }\n            \\\\  }\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"click\",\n        .description = \"Click on an interactive element.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"backendNodeId\": { \"type\": \"integer\", \"description\": \"The backend node ID of the element to click.\" }\n            \\\\  },\n            \\\\  \"required\": [\"backendNodeId\"]\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"fill\",\n        .description = \"Fill text into an input element.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"backendNodeId\": { \"type\": \"integer\", \"description\": \"The backend node ID of the input element to fill.\" },\n            \\\\    \"text\": { \"type\": \"string\", \"description\": \"The text to fill into the input element.\" }\n            \\\\  },\n            \\\\  \"required\": [\"backendNodeId\", \"text\"]\n            \\\\}\n        ),\n    },\n    .{\n        .name = \"scroll\",\n        .description = \"Scroll the page or a specific element.\",\n        .inputSchema = protocol.minify(\n            \\\\{\n            \\\\  \"type\": \"object\",\n            \\\\  \"properties\": {\n            \\\\    \"backendNodeId\": { \"type\": \"integer\", \"description\": \"Optional: The backend node ID of the element to scroll. If omitted, scrolls the window.\" },\n            \\\\    \"x\": { \"type\": \"integer\", \"description\": \"Optional: The horizontal scroll offset.\" },\n            \\\\    \"y\": { \"type\": \"integer\", \"description\": \"Optional: The vertical scroll offset.\" }\n            \\\\  }\n            \\\\}\n        ),\n    },\n};\n\npub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {\n    _ = arena;\n    try server.sendResult(req.id.?, .{ .tools = &tool_list });\n}\n\nconst GotoParams = struct {\n    url: [:0]const u8,\n};\n\nconst EvaluateParams = struct {\n    script: [:0]const u8,\n    url: ?[:0]const u8 = null,\n};\n\nconst ToolStreamingText = struct {\n    page: *lp.Page,\n    action: enum { markdown, links, semantic_tree },\n    registry: ?*CDPNode.Registry = null,\n    arena: ?std.mem.Allocator = null,\n    backendNodeId: ?u32 = null,\n    maxDepth: ?u32 = null,\n\n    pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {\n        try jw.beginWriteRaw();\n        try jw.writer.writeByte('\"');\n        var escaped: protocol.JsonEscapingWriter = .init(jw.writer);\n        const w = &escaped.writer;\n\n        switch (self.action) {\n            .markdown => lp.markdown.dump(self.page.document.asNode(), .{}, w, self.page) catch |err| {\n                log.err(.mcp, \"markdown dump failed\", .{ .err = err });\n            },\n            .links => {\n                if (Selector.querySelectorAll(self.page.document.asNode(), \"a[href]\", self.page)) |list| {\n                    defer list.deinit(self.page._session);\n\n                    var first = true;\n                    for (list._nodes) |node| {\n                        if (node.is(Element.Html.Anchor)) |anchor| {\n                            const href = anchor.getHref(self.page) catch |err| {\n                                log.err(.mcp, \"resolve href failed\", .{ .err = err });\n                                continue;\n                            };\n\n                            if (href.len > 0) {\n                                if (!first) try w.writeByte('\\n');\n                                try w.writeAll(href);\n                                first = false;\n                            }\n                        }\n                    }\n                } else |err| {\n                    log.err(.mcp, \"query links failed\", .{ .err = err });\n                }\n            },\n            .semantic_tree => {\n                var root_node = self.page.document.asNode();\n                if (self.backendNodeId) |node_id| {\n                    if (self.registry) |registry| {\n                        if (registry.lookup_by_id.get(node_id)) |n| {\n                            root_node = n.dom;\n                        } else {\n                            log.warn(.mcp, \"semantic_tree id missing\", .{ .id = node_id });\n                        }\n                    }\n                }\n\n                const st = lp.SemanticTree{\n                    .dom_node = root_node,\n                    .registry = self.registry.?,\n                    .page = self.page,\n                    .arena = self.arena.?,\n                    .prune = true,\n                    .max_depth = self.maxDepth orelse std.math.maxInt(u32) - 1,\n                };\n\n                st.textStringify(w) catch |err| {\n                    log.err(.mcp, \"semantic tree dump failed\", .{ .err = err });\n                };\n            },\n        }\n\n        try jw.writer.writeByte('\"');\n        jw.endWriteRaw();\n    }\n};\n\nconst ToolAction = enum {\n    goto,\n    navigate,\n    markdown,\n    links,\n    interactiveElements,\n    structuredData,\n    evaluate,\n    semantic_tree,\n    click,\n    fill,\n    scroll,\n};\n\nconst tool_map = std.StaticStringMap(ToolAction).initComptime(.{\n    .{ \"goto\", .goto },\n    .{ \"navigate\", .navigate },\n    .{ \"markdown\", .markdown },\n    .{ \"links\", .links },\n    .{ \"interactiveElements\", .interactiveElements },\n    .{ \"structuredData\", .structuredData },\n    .{ \"evaluate\", .evaluate },\n    .{ \"semantic_tree\", .semantic_tree },\n    .{ \"click\", .click },\n    .{ \"fill\", .fill },\n    .{ \"scroll\", .scroll },\n});\n\npub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {\n    if (req.params == null or req.id == null) {\n        return server.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, \"Missing params\");\n    }\n\n    const CallParams = struct {\n        name: []const u8,\n        arguments: ?std.json.Value = null,\n    };\n\n    const call_params = std.json.parseFromValueLeaky(CallParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch {\n        return server.sendError(req.id.?, .InvalidParams, \"Invalid params\");\n    };\n\n    const action = tool_map.get(call_params.name) orelse {\n        return server.sendError(req.id.?, .MethodNotFound, \"Tool not found\");\n    };\n\n    switch (action) {\n        .goto, .navigate => try handleGoto(server, arena, req.id.?, call_params.arguments),\n        .markdown => try handleMarkdown(server, arena, req.id.?, call_params.arguments),\n        .links => try handleLinks(server, arena, req.id.?, call_params.arguments),\n        .interactiveElements => try handleInteractiveElements(server, arena, req.id.?, call_params.arguments),\n        .structuredData => try handleStructuredData(server, arena, req.id.?, call_params.arguments),\n        .evaluate => try handleEvaluate(server, arena, req.id.?, call_params.arguments),\n        .semantic_tree => try handleSemanticTree(server, arena, req.id.?, call_params.arguments),\n        .click => try handleClick(server, arena, req.id.?, call_params.arguments),\n        .fill => try handleFill(server, arena, req.id.?, call_params.arguments),\n        .scroll => try handleScroll(server, arena, req.id.?, call_params.arguments),\n    }\n}\n\nfn handleGoto(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const args = try parseArguments(GotoParams, arena, arguments, server, id, \"goto\");\n    try performGoto(server, args.url, id);\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = \"Navigated successfully.\" }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\n\nfn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const MarkdownParams = struct {\n        url: ?[:0]const u8 = null,\n    };\n    if (arguments) |args_raw| {\n        if (std.json.parseFromValueLeaky(MarkdownParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {\n            if (args.url) |u| {\n                try performGoto(server, u, id);\n            }\n        } else |_| {}\n    }\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const content = [_]protocol.TextContent(ToolStreamingText){.{\n        .text = .{ .page = page, .action = .markdown },\n    }};\n    try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });\n}\n\nfn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const LinksParams = struct {\n        url: ?[:0]const u8 = null,\n    };\n    if (arguments) |args_raw| {\n        if (std.json.parseFromValueLeaky(LinksParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {\n            if (args.url) |u| {\n                try performGoto(server, u, id);\n            }\n        } else |_| {}\n    }\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const content = [_]protocol.TextContent(ToolStreamingText){.{\n        .text = .{ .page = page, .action = .links },\n    }};\n    try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });\n}\n\nfn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const TreeParams = struct {\n        url: ?[:0]const u8 = null,\n        backendNodeId: ?u32 = null,\n        maxDepth: ?u32 = null,\n    };\n    var tree_args: TreeParams = .{};\n    if (arguments) |args_raw| {\n        if (std.json.parseFromValueLeaky(TreeParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {\n            tree_args = args;\n            if (args.url) |u| {\n                try performGoto(server, u, id);\n            }\n        } else |_| {}\n    }\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const content = [_]protocol.TextContent(ToolStreamingText){.{\n        .text = .{ .page = page, .action = .semantic_tree, .registry = &server.node_registry, .arena = arena, .backendNodeId = tree_args.backendNodeId, .maxDepth = tree_args.maxDepth },\n    }};\n    try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });\n}\n\nfn handleInteractiveElements(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const Params = struct {\n        url: ?[:0]const u8 = null,\n    };\n    if (arguments) |args_raw| {\n        if (std.json.parseFromValueLeaky(Params, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {\n            if (args.url) |u| {\n                try performGoto(server, u, id);\n            }\n        } else |_| {}\n    }\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const elements = lp.interactive.collectInteractiveElements(page.document.asNode(), arena, page) catch |err| {\n        log.err(.mcp, \"elements collection failed\", .{ .err = err });\n        return server.sendError(id, .InternalError, \"Failed to collect interactive elements\");\n    };\n    var aw: std.Io.Writer.Allocating = .init(arena);\n    try std.json.Stringify.value(elements, .{}, &aw.writer);\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = aw.written() }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\n\nfn handleStructuredData(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const Params = struct {\n        url: ?[:0]const u8 = null,\n    };\n    if (arguments) |args_raw| {\n        if (std.json.parseFromValueLeaky(Params, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {\n            if (args.url) |u| {\n                try performGoto(server, u, id);\n            }\n        } else |_| {}\n    }\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const data = lp.structured_data.collectStructuredData(page.document.asNode(), arena, page) catch |err| {\n        log.err(.mcp, \"struct data collection failed\", .{ .err = err });\n        return server.sendError(id, .InternalError, \"Failed to collect structured data\");\n    };\n    var aw: std.Io.Writer.Allocating = .init(arena);\n    try std.json.Stringify.value(data, .{}, &aw.writer);\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = aw.written() }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\n\nfn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const args = try parseArguments(EvaluateParams, arena, arguments, server, id, \"evaluate\");\n\n    if (args.url) |url| {\n        try performGoto(server, url, id);\n    }\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(&ls.local);\n    defer try_catch.deinit();\n\n    const js_result = ls.local.compileAndRun(args.script, null) catch |err| {\n        const caught = try_catch.caughtOrError(arena, err);\n        var aw: std.Io.Writer.Allocating = .init(arena);\n        try caught.format(&aw.writer);\n\n        const content = [_]protocol.TextContent([]const u8){.{ .text = aw.written() }};\n        return server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content, .isError = true });\n    };\n\n    const str_result = js_result.toStringSliceWithAlloc(arena) catch \"undefined\";\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = str_result }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\n\nfn handleClick(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const ClickParams = struct {\n        backendNodeId: CDPNode.Id,\n    };\n    const args = try parseArguments(ClickParams, arena, arguments, server, id, \"click\");\n\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {\n        return server.sendError(id, .InvalidParams, \"Node not found\");\n    };\n\n    lp.actions.click(node.dom, page) catch |err| {\n        if (err == error.InvalidNodeType) {\n            return server.sendError(id, .InvalidParams, \"Node is not an HTML element\");\n        }\n        return server.sendError(id, .InternalError, \"Failed to click element\");\n    };\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = \"Clicked successfully.\" }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\n\nfn handleFill(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const FillParams = struct {\n        backendNodeId: CDPNode.Id,\n        text: []const u8,\n    };\n    const args = try parseArguments(FillParams, arena, arguments, server, id, \"fill\");\n\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {\n        return server.sendError(id, .InvalidParams, \"Node not found\");\n    };\n\n    lp.actions.fill(node.dom, args.text, page) catch |err| {\n        if (err == error.InvalidNodeType) {\n            return server.sendError(id, .InvalidParams, \"Node is not an input, textarea or select\");\n        }\n        return server.sendError(id, .InternalError, \"Failed to fill element\");\n    };\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = \"Filled successfully.\" }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\n\nfn handleScroll(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {\n    const ScrollParams = struct {\n        backendNodeId: ?CDPNode.Id = null,\n        x: ?i32 = null,\n        y: ?i32 = null,\n    };\n    const args = try parseArguments(ScrollParams, arena, arguments, server, id, \"scroll\");\n\n    const page = server.session.currentPage() orelse {\n        return server.sendError(id, .PageNotLoaded, \"Page not loaded\");\n    };\n\n    var target_node: ?*DOMNode = null;\n    if (args.backendNodeId) |node_id| {\n        const node = server.node_registry.lookup_by_id.get(node_id) orelse {\n            return server.sendError(id, .InvalidParams, \"Node not found\");\n        };\n        target_node = node.dom;\n    }\n\n    lp.actions.scroll(target_node, args.x, args.y, page) catch |err| {\n        if (err == error.InvalidNodeType) {\n            return server.sendError(id, .InvalidParams, \"Node is not an element\");\n        }\n        return server.sendError(id, .InternalError, \"Failed to scroll\");\n    };\n\n    const content = [_]protocol.TextContent([]const u8){.{ .text = \"Scrolled successfully.\" }};\n    try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });\n}\nfn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T {\n    if (arguments == null) {\n        try server.sendError(id, .InvalidParams, \"Missing arguments\");\n        return error.InvalidParams;\n    }\n    return std.json.parseFromValueLeaky(T, arena, arguments.?, .{ .ignore_unknown_fields = true }) catch {\n        const msg = std.fmt.allocPrint(arena, \"Invalid arguments for {s}\", .{tool_name}) catch \"Invalid arguments\";\n        try server.sendError(id, .InvalidParams, msg);\n        return error.InvalidParams;\n    };\n}\n\nfn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {\n    const session = server.session;\n    if (session.page != null) {\n        session.removePage();\n    }\n    const page = try session.createPage();\n    page.navigate(url, .{\n        .reason = .address_bar,\n        .kind = .{ .push = null },\n    }) catch {\n        try server.sendError(id, .InternalError, \"Internal error during navigation\");\n        return error.NavigationFailed;\n    };\n\n    _ = server.session.wait(5000);\n}\n\nconst testing = @import(\"../testing.zig\");\nconst router = @import(\"router.zig\");\n\ntest \"MCP - evaluate error reporting\" {\n    defer testing.reset();\n    const allocator = testing.allocator;\n    const app = testing.test_app;\n\n    var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);\n    defer out_alloc.deinit();\n\n    var server = try Server.init(allocator, app, &out_alloc.writer);\n    defer server.deinit();\n    _ = try server.session.createPage();\n\n    const aa = testing.arena_allocator;\n\n    // Call evaluate with a script that throws an error\n    const msg =\n        \\\\{\n        \\\\  \"jsonrpc\": \"2.0\",\n        \\\\  \"id\": 1,\n        \\\\  \"method\": \"tools/call\",\n        \\\\  \"params\": {\n        \\\\    \"name\": \"evaluate\",\n        \\\\    \"arguments\": {\n        \\\\      \"script\": \"throw new Error('test error')\"\n        \\\\    }\n        \\\\  }\n        \\\\}\n    ;\n\n    try router.handleMessage(server, aa, msg);\n\n    try testing.expectJson(\n        \\\\{\n        \\\\  \"id\": 1,\n        \\\\  \"result\": {\n        \\\\    \"isError\": true,\n        \\\\    \"content\": [\n        \\\\      { \"type\": \"text\" }\n        \\\\    ]\n        \\\\  }\n        \\\\}\n    , out_alloc.writer.buffered());\n}\n\ntest \"MCP - Actions: click, fill, scroll\" {\n    defer testing.reset();\n    const allocator = testing.allocator;\n    const app = testing.test_app;\n\n    var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);\n    defer out_alloc.deinit();\n\n    var server = try Server.init(allocator, app, &out_alloc.writer);\n    defer server.deinit();\n\n    const aa = testing.arena_allocator;\n    const page = try server.session.createPage();\n    const url = \"http://localhost:9582/src/browser/tests/mcp_actions.html\";\n    try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });\n    _ = server.session.wait(5000);\n\n    // Test Click\n    const btn = page.document.getElementById(\"btn\", page).?.asNode();\n    const btn_id = (try server.node_registry.register(btn)).id;\n    var btn_id_buf: [12]u8 = undefined;\n    const btn_id_str = std.fmt.bufPrint(&btn_id_buf, \"{d}\", .{btn_id}) catch unreachable;\n    const click_msg = try std.mem.concat(aa, u8, &.{ \"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":1,\\\"method\\\":\\\"tools/call\\\",\\\"params\\\":{\\\"name\\\":\\\"click\\\",\\\"arguments\\\":{\\\"backendNodeId\\\":\", btn_id_str, \"}}}\" });\n    try router.handleMessage(server, aa, click_msg);\n\n    // Test Fill Input\n    const inp = page.document.getElementById(\"inp\", page).?.asNode();\n    const inp_id = (try server.node_registry.register(inp)).id;\n    var inp_id_buf: [12]u8 = undefined;\n    const inp_id_str = std.fmt.bufPrint(&inp_id_buf, \"{d}\", .{inp_id}) catch unreachable;\n    const fill_msg = try std.mem.concat(aa, u8, &.{ \"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":2,\\\"method\\\":\\\"tools/call\\\",\\\"params\\\":{\\\"name\\\":\\\"fill\\\",\\\"arguments\\\":{\\\"backendNodeId\\\":\", inp_id_str, \",\\\"text\\\":\\\"hello\\\"}}}\" });\n    try router.handleMessage(server, aa, fill_msg);\n\n    // Test Fill Select\n    const sel = page.document.getElementById(\"sel\", page).?.asNode();\n    const sel_id = (try server.node_registry.register(sel)).id;\n    var sel_id_buf: [12]u8 = undefined;\n    const sel_id_str = std.fmt.bufPrint(&sel_id_buf, \"{d}\", .{sel_id}) catch unreachable;\n    const fill_sel_msg = try std.mem.concat(aa, u8, &.{ \"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":3,\\\"method\\\":\\\"tools/call\\\",\\\"params\\\":{\\\"name\\\":\\\"fill\\\",\\\"arguments\\\":{\\\"backendNodeId\\\":\", sel_id_str, \",\\\"text\\\":\\\"opt2\\\"}}}\" });\n    try router.handleMessage(server, aa, fill_sel_msg);\n\n    // Test Scroll\n    const scrollbox = page.document.getElementById(\"scrollbox\", page).?.asNode();\n    const scrollbox_id = (try server.node_registry.register(scrollbox)).id;\n    var scroll_id_buf: [12]u8 = undefined;\n    const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, \"{d}\", .{scrollbox_id}) catch unreachable;\n    const scroll_msg = try std.mem.concat(aa, u8, &.{ \"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":4,\\\"method\\\":\\\"tools/call\\\",\\\"params\\\":{\\\"name\\\":\\\"scroll\\\",\\\"arguments\\\":{\\\"backendNodeId\\\":\", scroll_id_str, \",\\\"y\\\":50}}}\" });\n    try router.handleMessage(server, aa, scroll_msg);\n\n    // Evaluate assertions\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(&ls.local);\n    defer try_catch.deinit();\n\n    const result = try ls.local.compileAndRun(\"window.clicked === true && window.inputVal === 'hello' && window.changed === true && window.selChanged === 'opt2' && window.scrolled === true\", null);\n\n    try testing.expect(result.isTrue());\n}\n"
  },
  {
    "path": "src/mcp.zig",
    "content": "const std = @import(\"std\");\n\npub const protocol = @import(\"mcp/protocol.zig\");\npub const router = @import(\"mcp/router.zig\");\npub const Server = @import(\"mcp/Server.zig\");\n\ntest {\n    std.testing.refAllDecls(@This());\n}\n"
  },
  {
    "path": "src/network/Robots.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst builtin = @import(\"builtin\");\nconst std = @import(\"std\");\nconst log = @import(\"../log.zig\");\n\npub const CompiledPattern = struct {\n    pattern: []const u8,\n    ty: enum {\n        prefix, // \"/admin/\" - prefix match\n        exact, // \"/admin$\" - exact match\n        wildcard, // any pattern that contains *\n    },\n\n    fn compile(pattern: []const u8) CompiledPattern {\n        if (pattern.len == 0) {\n            return .{\n                .pattern = pattern,\n                .ty = .prefix,\n            };\n        }\n\n        const is_wildcard = std.mem.indexOfScalar(u8, pattern, '*') != null;\n\n        if (is_wildcard) {\n            return .{\n                .pattern = pattern,\n                .ty = .wildcard,\n            };\n        }\n\n        const has_end_anchor = pattern[pattern.len - 1] == '$';\n        return .{\n            .pattern = pattern,\n            .ty = if (has_end_anchor) .exact else .prefix,\n        };\n    }\n};\n\npub const Rule = union(enum) {\n    allow: CompiledPattern,\n    disallow: CompiledPattern,\n\n    fn allowRule(pattern: []const u8) Rule {\n        return .{ .allow = CompiledPattern.compile(pattern) };\n    }\n\n    fn disallowRule(pattern: []const u8) Rule {\n        return .{ .disallow = CompiledPattern.compile(pattern) };\n    }\n};\n\npub const Key = enum {\n    @\"user-agent\",\n    allow,\n    disallow,\n};\n\n/// https://www.rfc-editor.org/rfc/rfc9309.html\npub const Robots = @This();\npub const empty: Robots = .{ .rules = &.{} };\n\npub const RobotStore = struct {\n    const RobotsEntry = union(enum) {\n        present: Robots,\n        absent,\n    };\n\n    pub const RobotsMap = std.HashMapUnmanaged([]const u8, RobotsEntry, struct {\n        const Context = @This();\n\n        pub fn hash(_: Context, value: []const u8) u32 {\n            var key = value;\n            var buf: [128]u8 = undefined;\n            var h = std.hash.Wyhash.init(value.len);\n\n            while (key.len >= 128) {\n                const lower = std.ascii.lowerString(buf[0..], key[0..128]);\n                h.update(lower);\n                key = key[128..];\n            }\n\n            if (key.len > 0) {\n                const lower = std.ascii.lowerString(buf[0..key.len], key);\n                h.update(lower);\n            }\n\n            return @truncate(h.final());\n        }\n\n        pub fn eql(_: Context, a: []const u8, b: []const u8) bool {\n            return std.ascii.eqlIgnoreCase(a, b);\n        }\n    }, 80);\n\n    allocator: std.mem.Allocator,\n    map: RobotsMap,\n    mutex: std.Thread.Mutex = .{},\n\n    pub fn init(allocator: std.mem.Allocator) RobotStore {\n        return .{ .allocator = allocator, .map = .empty };\n    }\n\n    pub fn deinit(self: *RobotStore) void {\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        var iter = self.map.iterator();\n\n        while (iter.next()) |entry| {\n            self.allocator.free(entry.key_ptr.*);\n\n            switch (entry.value_ptr.*) {\n                .present => |*robots| robots.deinit(self.allocator),\n                .absent => {},\n            }\n        }\n\n        self.map.deinit(self.allocator);\n    }\n\n    pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry {\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        return self.map.get(url);\n    }\n\n    pub fn robotsFromBytes(self: *RobotStore, user_agent: []const u8, bytes: []const u8) !Robots {\n        return try Robots.fromBytes(self.allocator, user_agent, bytes);\n    }\n\n    pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void {\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        const duped = try self.allocator.dupe(u8, url);\n        try self.map.put(self.allocator, duped, .{ .present = robots });\n    }\n\n    pub fn putAbsent(self: *RobotStore, url: []const u8) !void {\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        const duped = try self.allocator.dupe(u8, url);\n        try self.map.put(self.allocator, duped, .absent);\n    }\n};\n\nrules: []const Rule,\n\nconst State = struct {\n    entry: enum {\n        not_in_entry,\n        in_other_entry,\n        in_our_entry,\n        in_wildcard_entry,\n    },\n    has_rules: bool = false,\n};\n\nfn freeRulesInList(allocator: std.mem.Allocator, rules: []const Rule) void {\n    for (rules) |rule| {\n        switch (rule) {\n            .allow => |compiled| allocator.free(compiled.pattern),\n            .disallow => |compiled| allocator.free(compiled.pattern),\n        }\n    }\n}\n\nfn parseRulesWithUserAgent(\n    allocator: std.mem.Allocator,\n    user_agent: []const u8,\n    raw_bytes: []const u8,\n) ![]Rule {\n    var rules: std.ArrayList(Rule) = .empty;\n    defer rules.deinit(allocator);\n\n    var wildcard_rules: std.ArrayList(Rule) = .empty;\n    defer wildcard_rules.deinit(allocator);\n\n    var state: State = .{ .entry = .not_in_entry, .has_rules = false };\n\n    // https://en.wikipedia.org/wiki/Byte_order_mark\n    const UTF8_BOM: []const u8 = &.{ 0xEF, 0xBB, 0xBF };\n\n    // Strip UTF8 BOM\n    const bytes = if (std.mem.startsWith(u8, raw_bytes, UTF8_BOM))\n        raw_bytes[3..]\n    else\n        raw_bytes;\n\n    var iter = std.mem.splitScalar(u8, bytes, '\\n');\n    while (iter.next()) |line| {\n        const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);\n\n        // Skip all comment lines.\n        if (std.mem.startsWith(u8, trimmed, \"#\")) continue;\n\n        // Remove end of line comment.\n        const true_line = if (std.mem.indexOfScalar(u8, trimmed, '#')) |pos|\n            std.mem.trimRight(u8, trimmed[0..pos], &std.ascii.whitespace)\n        else\n            trimmed;\n\n        if (true_line.len == 0) continue;\n\n        const colon_idx = std.mem.indexOfScalar(u8, true_line, ':') orelse {\n            log.warn(.browser, \"robots line missing colon\", .{ .line = line });\n            continue;\n        };\n        const key_str = try std.ascii.allocLowerString(allocator, true_line[0..colon_idx]);\n        defer allocator.free(key_str);\n\n        const key = std.meta.stringToEnum(Key, key_str) orelse continue;\n        const value = std.mem.trim(u8, true_line[colon_idx + 1 ..], &std.ascii.whitespace);\n\n        switch (key) {\n            .@\"user-agent\" => {\n                if (state.has_rules) {\n                    state = .{ .entry = .not_in_entry, .has_rules = false };\n                }\n\n                switch (state.entry) {\n                    .in_other_entry => {\n                        if (std.ascii.eqlIgnoreCase(user_agent, value)) {\n                            state.entry = .in_our_entry;\n                        }\n                    },\n                    .in_our_entry => {},\n                    .in_wildcard_entry => {\n                        if (std.ascii.eqlIgnoreCase(user_agent, value)) {\n                            state.entry = .in_our_entry;\n                        }\n                    },\n                    .not_in_entry => {\n                        if (std.ascii.eqlIgnoreCase(user_agent, value)) {\n                            state.entry = .in_our_entry;\n                        } else if (std.mem.eql(u8, \"*\", value)) {\n                            state.entry = .in_wildcard_entry;\n                        } else {\n                            state.entry = .in_other_entry;\n                        }\n                    },\n                }\n            },\n            .allow => {\n                defer state.has_rules = true;\n\n                switch (state.entry) {\n                    .in_our_entry => {\n                        const duped_value = try allocator.dupe(u8, value);\n                        errdefer allocator.free(duped_value);\n                        try rules.append(allocator, Rule.allowRule(duped_value));\n                    },\n                    .in_other_entry => {},\n                    .in_wildcard_entry => {\n                        const duped_value = try allocator.dupe(u8, value);\n                        errdefer allocator.free(duped_value);\n                        try wildcard_rules.append(allocator, Rule.allowRule(duped_value));\n                    },\n                    .not_in_entry => {\n                        log.warn(.browser, \"robots unexpected rule\", .{ .rule = \"allow\" });\n                        continue;\n                    },\n                }\n            },\n            .disallow => {\n                defer state.has_rules = true;\n\n                switch (state.entry) {\n                    .in_our_entry => {\n                        if (value.len == 0) continue;\n\n                        const duped_value = try allocator.dupe(u8, value);\n                        errdefer allocator.free(duped_value);\n                        try rules.append(allocator, Rule.disallowRule(duped_value));\n                    },\n                    .in_other_entry => {},\n                    .in_wildcard_entry => {\n                        if (value.len == 0) continue;\n\n                        const duped_value = try allocator.dupe(u8, value);\n                        errdefer allocator.free(duped_value);\n                        try wildcard_rules.append(allocator, Rule.disallowRule(duped_value));\n                    },\n                    .not_in_entry => {\n                        log.warn(.browser, \"robots unexpected rule\", .{ .rule = \"disallow\" });\n                        continue;\n                    },\n                }\n            },\n        }\n    }\n\n    // If we have rules for our specific User-Agent, we will use those rules.\n    // If we don't have any rules, we fallback to using the wildcard (\"*\") rules.\n    if (rules.items.len > 0) {\n        freeRulesInList(allocator, wildcard_rules.items);\n        return try rules.toOwnedSlice(allocator);\n    } else {\n        freeRulesInList(allocator, rules.items);\n        return try wildcard_rules.toOwnedSlice(allocator);\n    }\n}\n\npub fn fromBytes(allocator: std.mem.Allocator, user_agent: []const u8, bytes: []const u8) !Robots {\n    const rules = try parseRulesWithUserAgent(allocator, user_agent, bytes);\n\n    // sort by order once.\n    std.mem.sort(Rule, rules, {}, struct {\n        fn lessThan(_: void, a: Rule, b: Rule) bool {\n            const a_len = switch (a) {\n                .allow => |p| p.pattern.len,\n                .disallow => |p| p.pattern.len,\n            };\n\n            const b_len = switch (b) {\n                .allow => |p| p.pattern.len,\n                .disallow => |p| p.pattern.len,\n            };\n\n            // Sort by length first.\n            if (a_len != b_len) {\n                return a_len > b_len;\n            }\n\n            // Otherwise, allow should beat disallow.\n            const a_is_allow = switch (a) {\n                .allow => true,\n                .disallow => false,\n            };\n            const b_is_allow = switch (b) {\n                .allow => true,\n                .disallow => false,\n            };\n\n            return a_is_allow and !b_is_allow;\n        }\n    }.lessThan);\n\n    return .{ .rules = rules };\n}\n\npub fn deinit(self: *Robots, allocator: std.mem.Allocator) void {\n    freeRulesInList(allocator, self.rules);\n    allocator.free(self.rules);\n}\n\n/// There are rules for how the pattern in robots.txt should be matched.\n///\n/// * should match 0 or more of any character.\n/// $ should signify the end of a path, making it exact.\n/// otherwise, it is a prefix path.\nfn matchPattern(compiled: CompiledPattern, path: []const u8) bool {\n    switch (compiled.ty) {\n        .prefix => return std.mem.startsWith(u8, path, compiled.pattern),\n        .exact => {\n            const pattern = compiled.pattern;\n            return std.mem.eql(u8, path, pattern[0 .. pattern.len - 1]);\n        },\n        .wildcard => {\n            const pattern = compiled.pattern;\n            const exact_match = pattern[pattern.len - 1] == '$';\n            const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;\n            return matchInnerPattern(inner_pattern, path, exact_match);\n        },\n    }\n}\n\nfn matchInnerPattern(pattern: []const u8, path: []const u8, exact_match: bool) bool {\n    var pattern_idx: usize = 0;\n    var path_idx: usize = 0;\n\n    var star_pattern_idx: ?usize = null;\n    var star_path_idx: ?usize = null;\n\n    while (pattern_idx < pattern.len or path_idx < path.len) {\n        // 1: If pattern is consumed and we are doing prefix match, we matched.\n        if (pattern_idx >= pattern.len and !exact_match) {\n            return true;\n        }\n\n        // 2: Current character is a wildcard\n        if (pattern_idx < pattern.len and pattern[pattern_idx] == '*') {\n            star_pattern_idx = pattern_idx;\n            star_path_idx = path_idx;\n            pattern_idx += 1;\n            continue;\n        }\n\n        // 3: Characters match, advance both heads.\n        if (pattern_idx < pattern.len and path_idx < path.len and pattern[pattern_idx] == path[path_idx]) {\n            pattern_idx += 1;\n            path_idx += 1;\n            continue;\n        }\n\n        // 4: we have a previous wildcard, backtrack and try matching more.\n        if (star_pattern_idx) |star_p_idx| {\n            // if we have exhausted the path,\n            // we know we haven't matched.\n            if (star_path_idx.? > path.len) {\n                return false;\n            }\n\n            pattern_idx = star_p_idx + 1;\n            path_idx = star_path_idx.?;\n            star_path_idx.? += 1;\n            continue;\n        }\n\n        // Fallthrough: No match and no backtracking.\n        return false;\n    }\n\n    // Handle trailing widlcards that can match 0 characters.\n    while (pattern_idx < pattern.len and pattern[pattern_idx] == '*') {\n        pattern_idx += 1;\n    }\n\n    if (exact_match) {\n        // Both must be fully consumed.\n        return pattern_idx == pattern.len and path_idx == path.len;\n    }\n\n    // For prefix match, pattern must be completed.\n    return pattern_idx == pattern.len;\n}\n\npub fn isAllowed(self: *const Robots, path: []const u8) bool {\n    for (self.rules) |rule| {\n        switch (rule) {\n            .allow => |compiled| if (matchPattern(compiled, path)) return true,\n            .disallow => |compiled| if (matchPattern(compiled, path)) return false,\n        }\n    }\n\n    return true;\n}\n\nfn testMatch(pattern: []const u8, path: []const u8) bool {\n    comptime if (!builtin.is_test) unreachable;\n\n    return matchPattern(CompiledPattern.compile(pattern), path);\n}\n\ntest \"Robots: simple robots.txt\" {\n    const allocator = std.testing.allocator;\n\n    const file =\n        \\\\User-agent: *\n        \\\\Disallow: /private/\n        \\\\Allow: /public/\n        \\\\\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /admin/\n        \\\\\n    ;\n\n    const rules = try parseRulesWithUserAgent(allocator, \"GoogleBot\", file);\n    defer {\n        freeRulesInList(allocator, rules);\n        allocator.free(rules);\n    }\n\n    try std.testing.expectEqual(1, rules.len);\n    try std.testing.expectEqualStrings(\"/admin/\", rules[0].disallow.pattern);\n}\n\ntest \"Robots: matchPattern - simple prefix\" {\n    try std.testing.expect(testMatch(\"/admin\", \"/admin/page\"));\n    try std.testing.expect(testMatch(\"/admin\", \"/admin\"));\n    try std.testing.expect(!testMatch(\"/admin\", \"/other\"));\n    try std.testing.expect(!testMatch(\"/admin/page\", \"/admin\"));\n}\n\ntest \"Robots: matchPattern - single wildcard\" {\n    try std.testing.expect(testMatch(\"/admin/*\", \"/admin/\"));\n    try std.testing.expect(testMatch(\"/admin/*\", \"/admin/page\"));\n    try std.testing.expect(testMatch(\"/admin/*\", \"/admin/page/subpage\"));\n    try std.testing.expect(!testMatch(\"/admin/*\", \"/other/page\"));\n}\n\ntest \"Robots: matchPattern - wildcard in middle\" {\n    try std.testing.expect(testMatch(\"/abc/*/xyz\", \"/abc/def/xyz\"));\n    try std.testing.expect(testMatch(\"/abc/*/xyz\", \"/abc/def/ghi/xyz\"));\n    try std.testing.expect(!testMatch(\"/abc/*/xyz\", \"/abc/def\"));\n    try std.testing.expect(!testMatch(\"/abc/*/xyz\", \"/other/def/xyz\"));\n}\n\ntest \"Robots: matchPattern - complex wildcard case\" {\n    try std.testing.expect(testMatch(\"/abc/*/def/xyz\", \"/abc/def/def/xyz\"));\n    try std.testing.expect(testMatch(\"/abc/*/def/xyz\", \"/abc/ANYTHING/def/xyz\"));\n}\n\ntest \"Robots: matchPattern - multiple wildcards\" {\n    try std.testing.expect(testMatch(\"/a/*/b/*/c\", \"/a/x/b/y/c\"));\n    try std.testing.expect(testMatch(\"/a/*/b/*/c\", \"/a/x/y/b/z/w/c\"));\n    try std.testing.expect(testMatch(\"/*.php\", \"/index.php\"));\n    try std.testing.expect(testMatch(\"/*.php\", \"/admin/index.php\"));\n}\n\ntest \"Robots: matchPattern - end anchor\" {\n    try std.testing.expect(testMatch(\"/*.php$\", \"/index.php\"));\n    try std.testing.expect(!testMatch(\"/*.php$\", \"/index.php?param=value\"));\n    try std.testing.expect(testMatch(\"/admin$\", \"/admin\"));\n    try std.testing.expect(!testMatch(\"/admin$\", \"/admin/\"));\n    try std.testing.expect(testMatch(\"/fish$\", \"/fish\"));\n    try std.testing.expect(!testMatch(\"/fish$\", \"/fishheads\"));\n}\n\ntest \"Robots: matchPattern - wildcard with extension\" {\n    try std.testing.expect(testMatch(\"/fish*.php\", \"/fish.php\"));\n    try std.testing.expect(testMatch(\"/fish*.php\", \"/fishheads.php\"));\n    try std.testing.expect(testMatch(\"/fish*.php\", \"/fish/salmon.php\"));\n    try std.testing.expect(!testMatch(\"/fish*.php\", \"/fish.asp\"));\n}\n\ntest \"Robots: matchPattern - empty and edge cases\" {\n    try std.testing.expect(testMatch(\"\", \"/anything\"));\n    try std.testing.expect(testMatch(\"/\", \"/\"));\n    try std.testing.expect(testMatch(\"*\", \"/anything\"));\n    try std.testing.expect(testMatch(\"/*\", \"/anything\"));\n    try std.testing.expect(testMatch(\"$\", \"\"));\n}\n\ntest \"Robots: matchPattern - real world examples\" {\n    try std.testing.expect(testMatch(\"/\", \"/anything\"));\n\n    try std.testing.expect(testMatch(\"/admin/\", \"/admin/page\"));\n    try std.testing.expect(!testMatch(\"/admin/\", \"/public/page\"));\n\n    try std.testing.expect(testMatch(\"/*.pdf$\", \"/document.pdf\"));\n    try std.testing.expect(!testMatch(\"/*.pdf$\", \"/document.pdf.bak\"));\n\n    try std.testing.expect(testMatch(\"/*?\", \"/page?param=value\"));\n    try std.testing.expect(!testMatch(\"/*?\", \"/page\"));\n}\n\ntest \"Robots: isAllowed - basic allow/disallow\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"MyBot\",\n        \\\\User-agent: MyBot\n        \\\\Disallow: /admin/\n        \\\\Allow: /public/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/\") == true);\n    try std.testing.expect(robots.isAllowed(\"/public/page\") == true);\n    try std.testing.expect(robots.isAllowed(\"/admin/secret\") == false);\n    try std.testing.expect(robots.isAllowed(\"/other/page\") == true);\n}\n\ntest \"Robots: isAllowed - longest match wins\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"TestBot\",\n        \\\\User-agent: TestBot\n        \\\\Disallow: /admin/\n        \\\\Allow: /admin/public/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/admin/secret\") == false);\n    try std.testing.expect(robots.isAllowed(\"/admin/public/page\") == true);\n    try std.testing.expect(robots.isAllowed(\"/admin/public/\") == true);\n}\n\ntest \"Robots: isAllowed - specific user-agent vs wildcard\" {\n    const allocator = std.testing.allocator;\n\n    var robots1 = try Robots.fromBytes(allocator, \"Googlebot\",\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /private/\n        \\\\\n        \\\\User-agent: *\n        \\\\Disallow: /admin/\n        \\\\\n    );\n    defer robots1.deinit(allocator);\n\n    try std.testing.expect(robots1.isAllowed(\"/private/page\") == false);\n    try std.testing.expect(robots1.isAllowed(\"/admin/page\") == true);\n\n    // Test with other bot (should use wildcard)\n    var robots2 = try Robots.fromBytes(allocator, \"OtherBot\",\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /private/\n        \\\\\n        \\\\User-agent: *\n        \\\\Disallow: /admin/\n        \\\\\n    );\n    defer robots2.deinit(allocator);\n\n    try std.testing.expect(robots2.isAllowed(\"/private/page\") == true);\n    try std.testing.expect(robots2.isAllowed(\"/admin/page\") == false);\n}\n\ntest \"Robots: isAllowed - case insensitive user-agent\" {\n    const allocator = std.testing.allocator;\n\n    var robots1 = try Robots.fromBytes(allocator, \"googlebot\",\n        \\\\User-agent: GoogleBot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots1.deinit(allocator);\n    try std.testing.expect(robots1.isAllowed(\"/private/\") == false);\n\n    var robots2 = try Robots.fromBytes(allocator, \"GOOGLEBOT\",\n        \\\\User-agent: GoogleBot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots2.deinit(allocator);\n    try std.testing.expect(robots2.isAllowed(\"/private/\") == false);\n\n    var robots3 = try Robots.fromBytes(allocator, \"GoOgLeBoT\",\n        \\\\User-agent: GoogleBot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots3.deinit(allocator);\n    try std.testing.expect(robots3.isAllowed(\"/private/\") == false);\n}\n\ntest \"Robots: isAllowed - merged rules for same agent\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Googlebot\",\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /admin/\n        \\\\\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/admin/page\") == false);\n    try std.testing.expect(robots.isAllowed(\"/private/page\") == false);\n    try std.testing.expect(robots.isAllowed(\"/public/page\") == true);\n}\n\ntest \"Robots: isAllowed - wildcards in patterns\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Bot\",\n        \\\\User-agent: Bot\n        \\\\Disallow: /*.php$\n        \\\\Allow: /index.php$\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/page.php\") == false);\n    try std.testing.expect(robots.isAllowed(\"/index.php\") == true);\n    try std.testing.expect(robots.isAllowed(\"/page.php?param=1\") == true);\n    try std.testing.expect(robots.isAllowed(\"/page.html\") == true);\n}\n\ntest \"Robots: isAllowed - empty disallow allows everything\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Bot\",\n        \\\\User-agent: Bot\n        \\\\Disallow:\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/anything\") == true);\n    try std.testing.expect(robots.isAllowed(\"/\") == true);\n}\n\ntest \"Robots: isAllowed - no rules\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Bot\", \"\");\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/anything\") == true);\n}\n\ntest \"Robots: isAllowed - disallow all\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Bot\",\n        \\\\User-agent: Bot\n        \\\\Disallow: /\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/\") == false);\n    try std.testing.expect(robots.isAllowed(\"/anything\") == false);\n    try std.testing.expect(robots.isAllowed(\"/admin/page\") == false);\n}\n\ntest \"Robots: isAllowed - multiple user-agents in same entry\" {\n    const allocator = std.testing.allocator;\n\n    var robots1 = try Robots.fromBytes(allocator, \"Googlebot\",\n        \\\\User-agent: Googlebot\n        \\\\User-agent: Bingbot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots1.deinit(allocator);\n    try std.testing.expect(robots1.isAllowed(\"/private/\") == false);\n\n    var robots2 = try Robots.fromBytes(allocator, \"Bingbot\",\n        \\\\User-agent: Googlebot\n        \\\\User-agent: Bingbot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots2.deinit(allocator);\n    try std.testing.expect(robots2.isAllowed(\"/private/\") == false);\n\n    var robots3 = try Robots.fromBytes(allocator, \"OtherBot\",\n        \\\\User-agent: Googlebot\n        \\\\User-agent: Bingbot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots3.deinit(allocator);\n    try std.testing.expect(robots3.isAllowed(\"/private/\") == true);\n}\n\ntest \"Robots: isAllowed - wildcard fallback\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"UnknownBot\",\n        \\\\User-agent: *\n        \\\\Disallow: /admin/\n        \\\\Allow: /admin/public/\n        \\\\\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /private/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/admin/secret\") == false);\n    try std.testing.expect(robots.isAllowed(\"/admin/public/page\") == true);\n    try std.testing.expect(robots.isAllowed(\"/private/\") == true);\n}\n\ntest \"Robots: isAllowed - complex real-world example\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"MyBot\",\n        \\\\User-agent: *\n        \\\\Disallow: /cgi-bin/\n        \\\\Disallow: /tmp/\n        \\\\Disallow: /private/\n        \\\\\n        \\\\User-agent: MyBot\n        \\\\Disallow: /admin/\n        \\\\Disallow: /*.pdf$\n        \\\\Allow: /public/*.pdf$\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/\") == true);\n    try std.testing.expect(robots.isAllowed(\"/admin/dashboard\") == false);\n    try std.testing.expect(robots.isAllowed(\"/docs/guide.pdf\") == false);\n    try std.testing.expect(robots.isAllowed(\"/public/manual.pdf\") == true);\n    try std.testing.expect(robots.isAllowed(\"/page.html\") == true);\n    try std.testing.expect(robots.isAllowed(\"/cgi-bin/script.sh\") == true);\n}\n\ntest \"Robots: isAllowed - order doesn't matter + allow wins\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Bot\",\n        \\\\User-agent: Bot\n        \\\\ # WOW!!\n        \\\\Allow: /page\n        \\\\Disallow: /page\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/page\") == true);\n}\n\ntest \"Robots: isAllowed - empty file uses wildcard defaults\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"MyBot\",\n        \\\\User-agent: * # ABCDEF!!!\n        \\\\Disallow: /admin/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/admin/\") == false);\n    try std.testing.expect(robots.isAllowed(\"/public/\") == true);\n}\ntest \"Robots: isAllowed - wildcard entry with multiple user-agents including specific\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Googlebot\",\n        \\\\User-agent: *\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /shared/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/shared/\") == false);\n    try std.testing.expect(robots.isAllowed(\"/other/\") == true);\n\n    var robots2 = try Robots.fromBytes(allocator, \"Bingbot\",\n        \\\\User-agent: *\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /shared/\n        \\\\\n    );\n    defer robots2.deinit(allocator);\n\n    try std.testing.expect(robots2.isAllowed(\"/shared/\") == false);\n}\n\ntest \"Robots: isAllowed - specific agent appears after wildcard in entry\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"MyBot\",\n        \\\\User-agent: *\n        \\\\User-agent: MyBot\n        \\\\User-agent: Bingbot\n        \\\\Disallow: /admin/\n        \\\\Allow: /admin/public/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/admin/secret\") == false);\n    try std.testing.expect(robots.isAllowed(\"/admin/public/page\") == true);\n}\n\ntest \"Robots: isAllowed - wildcard should not override specific entry\" {\n    const allocator = std.testing.allocator;\n\n    var robots = try Robots.fromBytes(allocator, \"Googlebot\",\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /private/\n        \\\\\n        \\\\User-agent: *\n        \\\\User-agent: Googlebot\n        \\\\Disallow: /admin/\n        \\\\\n    );\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/private/\") == false);\n    try std.testing.expect(robots.isAllowed(\"/admin/\") == false);\n}\n\ntest \"Robots: isAllowed - Google's real robots.txt\" {\n    const allocator = std.testing.allocator;\n\n    // Simplified version of google.com/robots.txt\n    const google_robots =\n        \\\\User-agent: *\n        \\\\User-agent: Yandex\n        \\\\Disallow: /search\n        \\\\Allow: /search/about\n        \\\\Allow: /search/howsearchworks\n        \\\\Disallow: /imgres\n        \\\\Disallow: /m?\n        \\\\Disallow: /m/\n        \\\\Allow:    /m/finance\n        \\\\Disallow: /maps/\n        \\\\Allow: /maps/$\n        \\\\Allow: /maps/@\n        \\\\Allow: /maps/dir/\n        \\\\Disallow: /shopping?\n        \\\\Allow: /shopping?udm=28$\n        \\\\\n        \\\\User-agent: AdsBot-Google\n        \\\\Disallow: /maps/api/js/\n        \\\\Allow: /maps/api/js\n        \\\\Disallow: /maps/api/staticmap\n        \\\\\n        \\\\User-agent: Yandex\n        \\\\Disallow: /about/careers/applications/jobs/results\n        \\\\\n        \\\\User-agent: facebookexternalhit\n        \\\\User-agent: Twitterbot\n        \\\\Allow: /imgres\n        \\\\Allow: /search\n        \\\\Disallow: /groups\n        \\\\Disallow: /m/\n        \\\\\n    ;\n\n    var regular_bot = try Robots.fromBytes(allocator, \"Googlebot\", google_robots);\n    defer regular_bot.deinit(allocator);\n\n    try std.testing.expect(regular_bot.isAllowed(\"/\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/search\") == false);\n    try std.testing.expect(regular_bot.isAllowed(\"/search/about\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/search/howsearchworks\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/imgres\") == false);\n    try std.testing.expect(regular_bot.isAllowed(\"/m/finance\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/m/other\") == false);\n    try std.testing.expect(regular_bot.isAllowed(\"/maps/\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/maps/@\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/shopping?udm=28\") == true);\n    try std.testing.expect(regular_bot.isAllowed(\"/shopping?udm=28&extra\") == false);\n\n    var adsbot = try Robots.fromBytes(allocator, \"AdsBot-Google\", google_robots);\n    defer adsbot.deinit(allocator);\n\n    try std.testing.expect(adsbot.isAllowed(\"/maps/api/js\") == true);\n    try std.testing.expect(adsbot.isAllowed(\"/maps/api/js/\") == false);\n    try std.testing.expect(adsbot.isAllowed(\"/maps/api/staticmap\") == false);\n\n    var twitterbot = try Robots.fromBytes(allocator, \"Twitterbot\", google_robots);\n    defer twitterbot.deinit(allocator);\n\n    try std.testing.expect(twitterbot.isAllowed(\"/imgres\") == true);\n    try std.testing.expect(twitterbot.isAllowed(\"/search\") == true);\n    try std.testing.expect(twitterbot.isAllowed(\"/groups\") == false);\n    try std.testing.expect(twitterbot.isAllowed(\"/m/\") == false);\n}\n\ntest \"Robots: user-agent after rules starts new entry\" {\n    const allocator = std.testing.allocator;\n\n    const file =\n        \\\\User-agent: Bot1\n        \\\\User-agent: Bot2\n        \\\\Disallow: /admin/\n        \\\\Allow: /public/\n        \\\\User-agent: Bot3\n        \\\\Disallow: /private/\n        \\\\\n    ;\n\n    var robots1 = try Robots.fromBytes(allocator, \"Bot1\", file);\n    defer robots1.deinit(allocator);\n    try std.testing.expect(robots1.isAllowed(\"/admin/\") == false);\n    try std.testing.expect(robots1.isAllowed(\"/public/\") == true);\n    try std.testing.expect(robots1.isAllowed(\"/private/\") == true);\n\n    var robots2 = try Robots.fromBytes(allocator, \"Bot2\", file);\n    defer robots2.deinit(allocator);\n    try std.testing.expect(robots2.isAllowed(\"/admin/\") == false);\n    try std.testing.expect(robots2.isAllowed(\"/public/\") == true);\n    try std.testing.expect(robots2.isAllowed(\"/private/\") == true);\n\n    var robots3 = try Robots.fromBytes(allocator, \"Bot3\", file);\n    defer robots3.deinit(allocator);\n    try std.testing.expect(robots3.isAllowed(\"/admin/\") == true);\n    try std.testing.expect(robots3.isAllowed(\"/public/\") == true);\n    try std.testing.expect(robots3.isAllowed(\"/private/\") == false);\n}\n\ntest \"Robots: blank lines don't end entries\" {\n    const allocator = std.testing.allocator;\n\n    const file =\n        \\\\User-agent: MyBot\n        \\\\Disallow: /admin/\n        \\\\\n        \\\\\n        \\\\Allow: /public/\n        \\\\\n    ;\n\n    var robots = try Robots.fromBytes(allocator, \"MyBot\", file);\n    defer robots.deinit(allocator);\n\n    try std.testing.expect(robots.isAllowed(\"/admin/\") == false);\n    try std.testing.expect(robots.isAllowed(\"/public/\") == true);\n}\n"
  },
  {
    "path": "src/network/Runtime.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst net = std.net;\nconst posix = std.posix;\nconst Allocator = std.mem.Allocator;\n\nconst lp = @import(\"lightpanda\");\nconst Config = @import(\"../Config.zig\");\nconst libcurl = @import(\"../sys/libcurl.zig\");\n\nconst net_http = @import(\"http.zig\");\nconst RobotStore = @import(\"Robots.zig\").RobotStore;\nconst WebBotAuth = @import(\"WebBotAuth.zig\");\n\nconst Runtime = @This();\n\nconst Listener = struct {\n    socket: posix.socket_t,\n    ctx: *anyopaque,\n    onAccept: *const fn (ctx: *anyopaque, socket: posix.socket_t) void,\n};\n\n// Number of fixed pollfds entries (wakeup pipe + listener).\nconst PSEUDO_POLLFDS = 2;\n\nconst MAX_TICK_CALLBACKS = 16;\n\nallocator: Allocator,\n\nconfig: *const Config,\nca_blob: ?net_http.Blob,\nrobot_store: RobotStore,\nweb_bot_auth: ?WebBotAuth,\n\nconnections: []net_http.Connection,\navailable: std.DoublyLinkedList = .{},\nconn_mutex: std.Thread.Mutex = .{},\n\npollfds: []posix.pollfd,\nlistener: ?Listener = null,\n\n// Wakeup pipe: workers write to [1], main thread polls [0]\nwakeup_pipe: [2]posix.fd_t = .{ -1, -1 },\n\nshutdown: std.atomic.Value(bool) = .init(false),\n\n// Multi is a heavy structure that can consume up to 2MB of RAM.\n// Currently, Runtime is used sparingly, and we only create it on demand.\n// When Runtime becomes truly shared, it should become a regular field.\nmulti: ?*libcurl.CurlM = null,\nsubmission_mutex: std.Thread.Mutex = .{},\nsubmission_queue: std.DoublyLinkedList = .{},\n\ncallbacks: [MAX_TICK_CALLBACKS]TickCallback = undefined,\ncallbacks_len: usize = 0,\ncallbacks_mutex: std.Thread.Mutex = .{},\n\nconst TickCallback = struct {\n    ctx: *anyopaque,\n    fun: *const fn (*anyopaque) void,\n};\n\nconst ZigToCurlAllocator = struct {\n    // C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64).\n    // We match this guarantee since libcurl expects malloc-compatible alignment.\n    const alignment = 16;\n\n    const Block = extern struct {\n        size: usize = 0,\n        _padding: [alignment - @sizeOf(usize)]u8 = .{0} ** (alignment - @sizeOf(usize)),\n\n        inline fn fullsize(bytes: usize) usize {\n            return alignment + bytes;\n        }\n\n        inline fn fromPtr(ptr: *anyopaque) *Block {\n            const raw: [*]u8 = @ptrCast(ptr);\n            return @ptrCast(@alignCast(raw - @sizeOf(Block)));\n        }\n\n        inline fn data(self: *Block) [*]u8 {\n            const ptr: [*]u8 = @ptrCast(self);\n            return ptr + @sizeOf(Block);\n        }\n\n        inline fn slice(self: *Block) []align(alignment) u8 {\n            const base: [*]align(alignment) u8 = @ptrCast(@alignCast(self));\n            return base[0 .. alignment + self.size];\n        }\n    };\n\n    comptime {\n        std.debug.assert(@sizeOf(Block) == alignment);\n    }\n\n    var instance: ?ZigToCurlAllocator = null;\n\n    allocator: Allocator,\n\n    pub fn init(allocator: Allocator) void {\n        lp.assert(instance == null, \"Initialization of curl must happen only once\", .{});\n        instance = .{ .allocator = allocator };\n    }\n\n    pub fn interface() libcurl.CurlAllocator {\n        return .{\n            .free = free,\n            .strdup = strdup,\n            .malloc = malloc,\n            .calloc = calloc,\n            .realloc = realloc,\n        };\n    }\n\n    fn _allocBlock(size: usize) ?*Block {\n        const slice = instance.?.allocator.alignedAlloc(u8, .fromByteUnits(alignment), Block.fullsize(size)) catch return null;\n        const block: *Block = @ptrCast(@alignCast(slice.ptr));\n        block.size = size;\n        return block;\n    }\n\n    fn _freeBlock(header: *Block) void {\n        instance.?.allocator.free(header.slice());\n    }\n\n    fn malloc(size: usize) ?*anyopaque {\n        const block = _allocBlock(size) orelse return null;\n        return @ptrCast(block.data());\n    }\n\n    fn calloc(nmemb: usize, size: usize) ?*anyopaque {\n        const total = nmemb * size;\n        const block = _allocBlock(total) orelse return null;\n        const ptr = block.data();\n        @memset(ptr[0..total], 0); // for historical reasons, calloc zeroes memory, but malloc does not.\n        return @ptrCast(ptr);\n    }\n\n    fn realloc(ptr: ?*anyopaque, size: usize) ?*anyopaque {\n        const p = ptr orelse return malloc(size);\n        const block = Block.fromPtr(p);\n\n        const old_size = block.size;\n        if (size == old_size) return ptr;\n\n        if (instance.?.allocator.resize(block.slice(), alignment + size)) {\n            block.size = size;\n            return ptr;\n        }\n\n        const copy_size = @min(old_size, size);\n        const new_block = _allocBlock(size) orelse return null;\n        @memcpy(new_block.data()[0..copy_size], block.data()[0..copy_size]);\n        _freeBlock(block);\n        return @ptrCast(new_block.data());\n    }\n\n    fn free(ptr: ?*anyopaque) void {\n        const p = ptr orelse return;\n        _freeBlock(Block.fromPtr(p));\n    }\n\n    fn strdup(str: [*:0]const u8) ?[*:0]u8 {\n        const len = std.mem.len(str);\n        const header = _allocBlock(len + 1) orelse return null;\n        const ptr = header.data();\n        @memcpy(ptr[0..len], str[0..len]);\n        ptr[len] = 0;\n        return ptr[0..len :0];\n    }\n};\n\nfn globalInit(allocator: Allocator) void {\n    ZigToCurlAllocator.init(allocator);\n\n    libcurl.curl_global_init(.{ .ssl = true }, ZigToCurlAllocator.interface()) catch |err| {\n        lp.assert(false, \"curl global init\", .{ .err = err });\n    };\n}\n\nfn globalDeinit() void {\n    libcurl.curl_global_cleanup();\n}\n\npub fn init(allocator: Allocator, config: *const Config) !Runtime {\n    globalInit(allocator);\n    errdefer globalDeinit();\n\n    const pipe = try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });\n\n    // 0 is wakeup, 1 is listener, rest for curl fds\n    const pollfds = try allocator.alloc(posix.pollfd, PSEUDO_POLLFDS + config.httpMaxConcurrent());\n    errdefer allocator.free(pollfds);\n\n    @memset(pollfds, .{ .fd = -1, .events = 0, .revents = 0 });\n    pollfds[0] = .{ .fd = pipe[0], .events = posix.POLL.IN, .revents = 0 };\n\n    var ca_blob: ?net_http.Blob = null;\n    if (config.tlsVerifyHost()) {\n        ca_blob = try loadCerts(allocator);\n    }\n\n    const count: usize = config.httpMaxConcurrent();\n    const connections = try allocator.alloc(net_http.Connection, count);\n    errdefer allocator.free(connections);\n\n    var available: std.DoublyLinkedList = .{};\n    for (0..count) |i| {\n        connections[i] = try net_http.Connection.init(ca_blob, config);\n        available.append(&connections[i].node);\n    }\n\n    const web_bot_auth = if (config.webBotAuth()) |wba_cfg|\n        try WebBotAuth.fromConfig(allocator, &wba_cfg)\n    else\n        null;\n\n    return .{\n        .allocator = allocator,\n        .config = config,\n        .ca_blob = ca_blob,\n\n        .pollfds = pollfds,\n        .wakeup_pipe = pipe,\n\n        .available = available,\n        .connections = connections,\n\n        .robot_store = RobotStore.init(allocator),\n        .web_bot_auth = web_bot_auth,\n    };\n}\n\npub fn deinit(self: *Runtime) void {\n    if (self.multi) |multi| {\n        libcurl.curl_multi_cleanup(multi) catch {};\n    }\n\n    for (&self.wakeup_pipe) |*fd| {\n        if (fd.* >= 0) {\n            posix.close(fd.*);\n            fd.* = -1;\n        }\n    }\n\n    self.allocator.free(self.pollfds);\n\n    if (self.ca_blob) |ca_blob| {\n        const data: [*]u8 = @ptrCast(ca_blob.data);\n        self.allocator.free(data[0..ca_blob.len]);\n    }\n\n    for (self.connections) |*conn| {\n        conn.deinit();\n    }\n    self.allocator.free(self.connections);\n\n    self.robot_store.deinit();\n    if (self.web_bot_auth) |wba| {\n        wba.deinit(self.allocator);\n    }\n\n    globalDeinit();\n}\n\npub fn bind(\n    self: *Runtime,\n    address: net.Address,\n    ctx: *anyopaque,\n    on_accept: *const fn (ctx: *anyopaque, socket: posix.socket_t) void,\n) !void {\n    const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK;\n    const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);\n    errdefer posix.close(listener);\n\n    try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));\n    if (@hasDecl(posix.TCP, \"NODELAY\")) {\n        try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1)));\n    }\n\n    try posix.bind(listener, &address.any, address.getOsSockLen());\n    try posix.listen(listener, self.config.maxPendingConnections());\n\n    if (self.listener != null) return error.TooManyListeners;\n\n    self.listener = .{\n        .socket = listener,\n        .ctx = ctx,\n        .onAccept = on_accept,\n    };\n    self.pollfds[1] = .{\n        .fd = listener,\n        .events = posix.POLL.IN,\n        .revents = 0,\n    };\n}\n\npub fn onTick(self: *Runtime, ctx: *anyopaque, callback: *const fn (*anyopaque) void) void {\n    self.callbacks_mutex.lock();\n    defer self.callbacks_mutex.unlock();\n\n    lp.assert(self.callbacks_len < MAX_TICK_CALLBACKS, \"too many ticks\", .{});\n\n    self.callbacks[self.callbacks_len] = .{\n        .ctx = ctx,\n        .fun = callback,\n    };\n    self.callbacks_len += 1;\n\n    self.wakeupPoll();\n}\n\npub fn fireTicks(self: *Runtime) void {\n    self.callbacks_mutex.lock();\n    defer self.callbacks_mutex.unlock();\n\n    for (self.callbacks[0..self.callbacks_len]) |*callback| {\n        callback.fun(callback.ctx);\n    }\n}\n\npub fn run(self: *Runtime) void {\n    var drain_buf: [64]u8 = undefined;\n    var running_handles: c_int = 0;\n\n    const poll_fd = &self.pollfds[0];\n    const listen_fd = &self.pollfds[1];\n\n    // Please note that receiving a shutdown command does not terminate all connections.\n    // When gracefully shutting down a server, we at least want to send the remaining\n    // telemetry, but we stop accepting new connections. It is the responsibility\n    // of external code to terminate its requests upon shutdown.\n    while (true) {\n        self.drainQueue();\n\n        if (self.multi) |multi| {\n            // Kickstart newly added handles (DNS/connect) so that\n            // curl registers its sockets before we poll.\n            libcurl.curl_multi_perform(multi, &running_handles) catch |err| {\n                lp.log.err(.app, \"curl perform\", .{ .err = err });\n            };\n\n            self.preparePollFds(multi);\n        }\n\n        // for ontick to work, you need to wake up periodically\n        const timeout = blk: {\n            const min_timeout = 250; // 250ms\n            if (self.multi == null) {\n                break :blk min_timeout;\n            }\n\n            const curl_timeout = self.getCurlTimeout();\n            if (curl_timeout == 0) {\n                break :blk min_timeout;\n            }\n\n            break :blk @min(min_timeout, curl_timeout);\n        };\n\n        _ = posix.poll(self.pollfds, timeout) catch |err| {\n            lp.log.err(.app, \"poll\", .{ .err = err });\n            continue;\n        };\n\n        // check wakeup pipe\n        if (poll_fd.revents != 0) {\n            poll_fd.revents = 0;\n            while (true)\n                _ = posix.read(self.wakeup_pipe[0], &drain_buf) catch break;\n        }\n\n        // accept new connections\n        if (listen_fd.revents != 0) {\n            listen_fd.revents = 0;\n            self.acceptConnections();\n        }\n\n        if (self.multi) |multi| {\n            // Drive transfers and process completions.\n            libcurl.curl_multi_perform(multi, &running_handles) catch |err| {\n                lp.log.err(.app, \"curl perform\", .{ .err = err });\n            };\n            self.processCompletions(multi);\n        }\n\n        self.fireTicks();\n\n        if (self.shutdown.load(.acquire) and running_handles == 0) {\n            // Check if fireTicks submitted new requests (e.g. telemetry flush).\n            // If so, continue the loop to drain and send them before exiting.\n            self.submission_mutex.lock();\n            const has_pending = self.submission_queue.first != null;\n            self.submission_mutex.unlock();\n            if (!has_pending) break;\n        }\n    }\n\n    if (self.listener) |listener| {\n        posix.shutdown(listener.socket, .both) catch |err| blk: {\n            if (err == error.SocketNotConnected and builtin.os.tag != .linux) {\n                // This error is normal/expected on BSD/MacOS. We probably\n                // shouldn't bother calling shutdown at all, but I guess this\n                // is safer.\n                break :blk;\n            }\n            lp.log.warn(.app, \"listener shutdown\", .{ .err = err });\n        };\n        posix.close(listener.socket);\n    }\n}\n\npub fn submitRequest(self: *Runtime, conn: *net_http.Connection) void {\n    self.submission_mutex.lock();\n    self.submission_queue.append(&conn.node);\n    self.submission_mutex.unlock();\n    self.wakeupPoll();\n}\n\nfn wakeupPoll(self: *Runtime) void {\n    _ = posix.write(self.wakeup_pipe[1], &.{1}) catch {};\n}\n\nfn drainQueue(self: *Runtime) void {\n    self.submission_mutex.lock();\n    defer self.submission_mutex.unlock();\n\n    if (self.submission_queue.first == null) return;\n\n    const multi = self.multi orelse blk: {\n        const m = libcurl.curl_multi_init() orelse {\n            lp.assert(false, \"curl multi init failed\", .{});\n            unreachable;\n        };\n        self.multi = m;\n        break :blk m;\n    };\n\n    while (self.submission_queue.popFirst()) |node| {\n        const conn: *net_http.Connection = @fieldParentPtr(\"node\", node);\n        conn.setPrivate(conn) catch |err| {\n            lp.log.err(.app, \"curl set private\", .{ .err = err });\n            self.releaseConnection(conn);\n            continue;\n        };\n        libcurl.curl_multi_add_handle(multi, conn.easy) catch |err| {\n            lp.log.err(.app, \"curl multi add\", .{ .err = err });\n            self.releaseConnection(conn);\n        };\n    }\n}\n\npub fn stop(self: *Runtime) void {\n    self.shutdown.store(true, .release);\n    self.wakeupPoll();\n}\n\nfn acceptConnections(self: *Runtime) void {\n    if (self.shutdown.load(.acquire)) {\n        return;\n    }\n    const listener = self.listener orelse return;\n\n    while (true) {\n        const socket = posix.accept(listener.socket, null, null, posix.SOCK.NONBLOCK) catch |err| {\n            switch (err) {\n                error.WouldBlock => break,\n                error.SocketNotListening => {\n                    self.pollfds[1] = .{ .fd = -1, .events = 0, .revents = 0 };\n                    self.listener = null;\n                    return;\n                },\n                error.ConnectionAborted => {\n                    lp.log.warn(.app, \"accept connection aborted\", .{});\n                    continue;\n                },\n                else => {\n                    lp.log.err(.app, \"accept error\", .{ .err = err });\n                    continue;\n                },\n            }\n        };\n\n        listener.onAccept(listener.ctx, socket);\n    }\n}\n\nfn preparePollFds(self: *Runtime, multi: *libcurl.CurlM) void {\n    const curl_fds = self.pollfds[PSEUDO_POLLFDS..];\n    @memset(curl_fds, .{ .fd = -1, .events = 0, .revents = 0 });\n\n    var fd_count: c_uint = 0;\n    const wait_fds: []libcurl.CurlWaitFd = @ptrCast(curl_fds);\n    libcurl.curl_multi_waitfds(multi, wait_fds, &fd_count) catch |err| {\n        lp.log.err(.app, \"curl waitfds\", .{ .err = err });\n    };\n}\n\nfn getCurlTimeout(self: *Runtime) i32 {\n    const multi = self.multi orelse return -1;\n    var timeout_ms: c_long = -1;\n    libcurl.curl_multi_timeout(multi, &timeout_ms) catch return -1;\n    return @intCast(@min(timeout_ms, std.math.maxInt(i32)));\n}\n\nfn processCompletions(self: *Runtime, multi: *libcurl.CurlM) void {\n    var msgs_in_queue: c_int = 0;\n    while (libcurl.curl_multi_info_read(multi, &msgs_in_queue)) |msg| {\n        switch (msg.data) {\n            .done => |maybe_err| {\n                if (maybe_err) |err| {\n                    lp.log.warn(.app, \"curl transfer error\", .{ .err = err });\n                }\n            },\n            else => continue,\n        }\n\n        const easy: *libcurl.Curl = msg.easy_handle;\n        var ptr: *anyopaque = undefined;\n        libcurl.curl_easy_getinfo(easy, .private, &ptr) catch\n            lp.assert(false, \"curl getinfo private\", .{});\n        const conn: *net_http.Connection = @ptrCast(@alignCast(ptr));\n\n        libcurl.curl_multi_remove_handle(multi, easy) catch {};\n        self.releaseConnection(conn);\n    }\n}\n\ncomptime {\n    if (@sizeOf(posix.pollfd) != @sizeOf(libcurl.CurlWaitFd)) {\n        @compileError(\"pollfd and CurlWaitFd size mismatch\");\n    }\n    if (@offsetOf(posix.pollfd, \"fd\") != @offsetOf(libcurl.CurlWaitFd, \"fd\") or\n        @offsetOf(posix.pollfd, \"events\") != @offsetOf(libcurl.CurlWaitFd, \"events\") or\n        @offsetOf(posix.pollfd, \"revents\") != @offsetOf(libcurl.CurlWaitFd, \"revents\"))\n    {\n        @compileError(\"pollfd and CurlWaitFd layout mismatch\");\n    }\n}\n\npub fn getConnection(self: *Runtime) ?*net_http.Connection {\n    self.conn_mutex.lock();\n    defer self.conn_mutex.unlock();\n\n    const node = self.available.popFirst() orelse return null;\n    return @fieldParentPtr(\"node\", node);\n}\n\npub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void {\n    conn.reset() catch |err| {\n        lp.assert(false, \"couldn't reset curl easy\", .{ .err = err });\n    };\n\n    self.conn_mutex.lock();\n    defer self.conn_mutex.unlock();\n\n    self.available.append(&conn.node);\n}\n\npub fn newConnection(self: *Runtime) !net_http.Connection {\n    return net_http.Connection.init(self.ca_blob, self.config);\n}\n\n// Wraps lines @ 64 columns. A PEM is basically a base64 encoded DER (which is\n// what Zig has), with lines wrapped at 64 characters and with a basic header\n// and footer\nconst LineWriter = struct {\n    col: usize = 0,\n    inner: std.ArrayList(u8).Writer,\n\n    pub fn writeAll(self: *LineWriter, data: []const u8) !void {\n        var writer = self.inner;\n\n        var col = self.col;\n        const len = 64 - col;\n\n        var remain = data;\n        if (remain.len > len) {\n            col = 0;\n            try writer.writeAll(data[0..len]);\n            try writer.writeByte('\\n');\n            remain = data[len..];\n        }\n\n        while (remain.len > 64) {\n            try writer.writeAll(remain[0..64]);\n            try writer.writeByte('\\n');\n            remain = data[len..];\n        }\n        try writer.writeAll(remain);\n        self.col = col + remain.len;\n    }\n};\n\n// TODO: on BSD / Linux, we could just read the PEM file directly.\n// This whole rescan + decode is really just needed for MacOS. On Linux\n// bundle.rescan does find the .pem file(s) which could be in a few different\n// places, so it's still useful, just not efficient.\nfn loadCerts(allocator: Allocator) !libcurl.CurlBlob {\n    var bundle: std.crypto.Certificate.Bundle = .{};\n    try bundle.rescan(allocator);\n    defer bundle.deinit(allocator);\n\n    const bytes = bundle.bytes.items;\n    if (bytes.len == 0) {\n        lp.log.warn(.app, \"No system certificates\", .{});\n        return .{\n            .len = 0,\n            .flags = 0,\n            .data = bytes.ptr,\n        };\n    }\n\n    const encoder = std.base64.standard.Encoder;\n    var arr: std.ArrayList(u8) = .empty;\n\n    const encoded_size = encoder.calcSize(bytes.len);\n    const buffer_size = encoded_size +\n        (bundle.map.count() * 75) + // start / end per certificate + extra, just in case\n        (encoded_size / 64) // newline per 64 characters\n    ;\n    try arr.ensureTotalCapacity(allocator, buffer_size);\n    errdefer arr.deinit(allocator);\n    var writer = arr.writer(allocator);\n\n    var it = bundle.map.valueIterator();\n    while (it.next()) |index| {\n        const cert = try std.crypto.Certificate.der.Element.parse(bytes, index.*);\n\n        try writer.writeAll(\"-----BEGIN CERTIFICATE-----\\n\");\n        var line_writer = LineWriter{ .inner = writer };\n        try encoder.encodeWriter(&line_writer, bytes[index.*..cert.slice.end]);\n        try writer.writeAll(\"\\n-----END CERTIFICATE-----\\n\");\n    }\n\n    // Final encoding should not be larger than our initial size estimate\n    lp.assert(buffer_size > arr.items.len, \"Http loadCerts\", .{ .estimate = buffer_size, .len = arr.items.len });\n\n    // Allocate exactly the size needed and copy the data\n    const result = try allocator.dupe(u8, arr.items);\n    // Free the original oversized allocation\n    arr.deinit(allocator);\n\n    return .{\n        .len = result.len,\n        .data = result.ptr,\n        .flags = 0,\n    };\n}\n"
  },
  {
    "path": "src/network/WebBotAuth.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst crypto = @import(\"../crypto.zig\");\n\nconst Http = @import(\"../network/http.zig\");\n\nconst WebBotAuth = @This();\n\npkey: *crypto.EVP_PKEY,\nkeyid: []const u8,\ndirectory_url: [:0]const u8,\n\npub const Config = struct {\n    key_file: []const u8,\n    keyid: []const u8,\n    domain: []const u8,\n};\n\nfn parsePemPrivateKey(pem: []const u8) !*crypto.EVP_PKEY {\n    const begin = \"-----BEGIN PRIVATE KEY-----\";\n    const end = \"-----END PRIVATE KEY-----\";\n    const start_idx = std.mem.indexOf(u8, pem, begin) orelse return error.InvalidPem;\n    const end_idx = std.mem.indexOf(u8, pem, end) orelse return error.InvalidPem;\n\n    const b64 = std.mem.trim(u8, pem[start_idx + begin.len .. end_idx], &std.ascii.whitespace);\n\n    // decode base64 into 48-byte DER buffer\n    var der: [48]u8 = undefined;\n    try std.base64.standard.Decoder.decode(der[0..48], b64);\n\n    // Ed25519 PKCS#8 structure always places the 32-byte raw private key at offset 16.\n    const key_bytes = der[16..48];\n\n    const pkey = crypto.EVP_PKEY_new_raw_private_key(crypto.EVP_PKEY_ED25519, null, key_bytes.ptr, 32);\n    return pkey orelse error.InvalidKey;\n}\n\nfn signEd25519(pkey: *crypto.EVP_PKEY, message: []const u8, out: *[64]u8) !void {\n    const ctx = crypto.EVP_MD_CTX_new() orelse return error.OutOfMemory;\n    defer crypto.EVP_MD_CTX_free(ctx);\n\n    if (crypto.EVP_DigestSignInit(ctx, null, null, null, pkey) != 1)\n        return error.SignInit;\n\n    var sig_len: usize = 64;\n    if (crypto.EVP_DigestSign(ctx, out.ptr, &sig_len, message.ptr, message.len) != 1)\n        return error.SignFailed;\n}\n\npub fn fromConfig(allocator: std.mem.Allocator, config: *const Config) !WebBotAuth {\n    const pem = try std.fs.cwd().readFileAlloc(allocator, config.key_file, 1024 * 4);\n    defer allocator.free(pem);\n\n    const pkey = try parsePemPrivateKey(pem);\n    errdefer crypto.EVP_PKEY_free(pkey);\n\n    const directory_url = try std.fmt.allocPrintSentinel(\n        allocator,\n        \"https://{s}/.well-known/http-message-signatures-directory\",\n        .{config.domain},\n        0,\n    );\n    errdefer allocator.free(directory_url);\n\n    return .{\n        .pkey = pkey,\n        // Owned by the Config so it's okay.\n        .keyid = config.keyid,\n        .directory_url = directory_url,\n    };\n}\n\npub fn signRequest(\n    self: *const WebBotAuth,\n    allocator: std.mem.Allocator,\n    headers: *Http.Headers,\n    authority: []const u8,\n) !void {\n    const now = std.time.timestamp();\n    const expires = now + 60;\n\n    // build the signature-input value (without the sig1= label)\n    const sig_input_value = try std.fmt.allocPrint(\n        allocator,\n        \"(\\\"@authority\\\" \\\"signature-agent\\\");created={d};expires={d};keyid=\\\"{s}\\\";alg=\\\"ed25519\\\";tag=\\\"web-bot-auth\\\"\",\n        .{ now, expires, self.keyid },\n    );\n    defer allocator.free(sig_input_value);\n\n    // build the canonical string to sign\n    const canonical = try std.fmt.allocPrint(\n        allocator,\n        \"\\\"@authority\\\": {s}\\n\\\"signature-agent\\\": \\\"{s}\\\"\\n\\\"@signature-params\\\": {s}\",\n        .{ authority, self.directory_url, sig_input_value },\n    );\n    defer allocator.free(canonical);\n\n    // sign it\n    var sig: [64]u8 = undefined;\n    try signEd25519(self.pkey, canonical, &sig);\n\n    // base64 encode\n    const encoded_len = std.base64.standard.Encoder.calcSize(sig.len);\n    const encoded = try allocator.alloc(u8, encoded_len);\n    defer allocator.free(encoded);\n    _ = std.base64.standard.Encoder.encode(encoded, &sig);\n\n    // build the 3 headers and add them\n    const sig_agent = try std.fmt.allocPrintSentinel(\n        allocator,\n        \"Signature-Agent: \\\"{s}\\\"\",\n        .{self.directory_url},\n        0,\n    );\n    defer allocator.free(sig_agent);\n\n    const sig_input = try std.fmt.allocPrintSentinel(\n        allocator,\n        \"Signature-Input: sig1={s}\",\n        .{sig_input_value},\n        0,\n    );\n    defer allocator.free(sig_input);\n\n    const signature = try std.fmt.allocPrintSentinel(\n        allocator,\n        \"Signature: sig1=:{s}:\",\n        .{encoded},\n        0,\n    );\n    defer allocator.free(signature);\n\n    try headers.add(sig_agent);\n    try headers.add(sig_input);\n    try headers.add(signature);\n}\n\npub fn deinit(self: WebBotAuth, allocator: std.mem.Allocator) void {\n    crypto.EVP_PKEY_free(self.pkey);\n    allocator.free(self.directory_url);\n}\n\ntest \"parsePemPrivateKey: valid Ed25519 PKCS#8 PEM\" {\n    const pem =\n        \\\\-----BEGIN PRIVATE KEY-----\n        \\\\MC4CAQAwBQYDK2VwBCIEIBuCRBIEFNtXcMBsyOOkFBFTJcEWTkbgSwKExhOjKFHT\n        \\\\-----END PRIVATE KEY-----\n        \\\\\n    ;\n\n    const pkey = try parsePemPrivateKey(pem);\n    defer crypto.EVP_PKEY_free(pkey);\n}\n\ntest \"parsePemPrivateKey: missing BEGIN marker returns error\" {\n    const bad_pem = \"-----END PRIVATE KEY-----\\n\";\n    try std.testing.expectError(error.InvalidPem, parsePemPrivateKey(bad_pem));\n}\n\ntest \"parsePemPrivateKey: missing END marker returns error\" {\n    const bad_pem = \"-----BEGIN PRIVATE KEY-----\\nMC4CAQA=\\n\";\n    try std.testing.expectError(error.InvalidPem, parsePemPrivateKey(bad_pem));\n}\n\ntest \"signEd25519: signature length is always 64 bytes\" {\n    const pem =\n        \\\\-----BEGIN PRIVATE KEY-----\n        \\\\MC4CAQAwBQYDK2VwBCIEIBuCRBIEFNtXcMBsyOOkFBFTJcEWTkbgSwKExhOjKFHT\n        \\\\-----END PRIVATE KEY-----\n        \\\\\n    ;\n    const pkey = try parsePemPrivateKey(pem);\n    defer crypto.EVP_PKEY_free(pkey);\n\n    var sig: [64]u8 = @splat(0);\n    try signEd25519(pkey, \"hello world\", &sig);\n\n    var all_zero = true;\n    for (sig) |b| if (b != 0) {\n        all_zero = false;\n        break;\n    };\n    try std.testing.expect(!all_zero);\n}\n\ntest \"signEd25519: same key + message produces same signature (deterministic)\" {\n    const pem =\n        \\\\-----BEGIN PRIVATE KEY-----\n        \\\\MC4CAQAwBQYDK2VwBCIEIBuCRBIEFNtXcMBsyOOkFBFTJcEWTkbgSwKExhOjKFHT\n        \\\\-----END PRIVATE KEY-----\n        \\\\\n    ;\n    const pkey = try parsePemPrivateKey(pem);\n    defer crypto.EVP_PKEY_free(pkey);\n\n    var sig1: [64]u8 = undefined;\n    var sig2: [64]u8 = undefined;\n    try signEd25519(pkey, \"deterministic test\", &sig1);\n    try signEd25519(pkey, \"deterministic test\", &sig2);\n\n    try std.testing.expectEqualSlices(u8, &sig1, &sig2);\n}\n\ntest \"signEd25519: same key + diff message produces different signature (deterministic)\" {\n    const pem =\n        \\\\-----BEGIN PRIVATE KEY-----\n        \\\\MC4CAQAwBQYDK2VwBCIEIBuCRBIEFNtXcMBsyOOkFBFTJcEWTkbgSwKExhOjKFHT\n        \\\\-----END PRIVATE KEY-----\n        \\\\\n    ;\n    const pkey = try parsePemPrivateKey(pem);\n    defer crypto.EVP_PKEY_free(pkey);\n\n    var sig1: [64]u8 = undefined;\n    var sig2: [64]u8 = undefined;\n    try signEd25519(pkey, \"msg 1\", &sig1);\n    try signEd25519(pkey, \"msg 2\", &sig2);\n\n    try std.testing.expect(!std.mem.eql(u8, &sig1, &sig2));\n}\n\ntest \"signRequest: adds headers with correct names\" {\n    const allocator = std.testing.allocator;\n\n    const pem =\n        \\\\-----BEGIN PRIVATE KEY-----\n        \\\\MC4CAQAwBQYDK2VwBCIEIBuCRBIEFNtXcMBsyOOkFBFTJcEWTkbgSwKExhOjKFHT\n        \\\\-----END PRIVATE KEY-----\n        \\\\\n    ;\n    const pkey = try parsePemPrivateKey(pem);\n\n    const directory_url = try allocator.dupeZ(\n        u8,\n        \"https://example.com/.well-known/http-message-signatures-directory\",\n    );\n\n    var auth = WebBotAuth{\n        .pkey = pkey,\n        .keyid = \"test-key-id\",\n        .directory_url = directory_url,\n    };\n    defer auth.deinit(allocator);\n\n    var headers = try Http.Headers.init(\"User-Agent: Test-Agent\");\n    defer headers.deinit();\n\n    try auth.signRequest(allocator, &headers, \"example.com\");\n\n    var it = headers.iterator();\n    var found_sig_agent = false;\n    var found_sig_input = false;\n    var found_signature = false;\n    var count: usize = 0;\n\n    while (it.next()) |h| {\n        count += 1;\n        if (std.ascii.eqlIgnoreCase(h.name, \"Signature-Agent\")) found_sig_agent = true;\n        if (std.ascii.eqlIgnoreCase(h.name, \"Signature-Input\")) found_sig_input = true;\n        if (std.ascii.eqlIgnoreCase(h.name, \"Signature\")) found_signature = true;\n    }\n\n    try std.testing.expect(count >= 3);\n    try std.testing.expect(found_sig_agent);\n    try std.testing.expect(found_sig_input);\n    try std.testing.expect(found_signature);\n}\n"
  },
  {
    "path": "src/network/http.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst posix = std.posix;\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst Config = @import(\"../Config.zig\");\nconst libcurl = @import(\"../sys/libcurl.zig\");\n\nconst log = @import(\"lightpanda\").log;\nconst assert = @import(\"lightpanda\").assert;\n\npub const ENABLE_DEBUG = false;\nconst IS_DEBUG = builtin.mode == .Debug;\n\npub const Blob = libcurl.CurlBlob;\npub const WaitFd = libcurl.CurlWaitFd;\npub const writefunc_error = libcurl.curl_writefunc_error;\n\nconst Error = libcurl.Error;\nconst ErrorMulti = libcurl.ErrorMulti;\nconst errorFromCode = libcurl.errorFromCode;\nconst errorMFromCode = libcurl.errorMFromCode;\nconst errorCheck = libcurl.errorCheck;\nconst errorMCheck = libcurl.errorMCheck;\n\npub fn curl_version() [*c]const u8 {\n    return libcurl.curl_version();\n}\n\npub const Method = enum(u8) {\n    GET = 0,\n    PUT = 1,\n    POST = 2,\n    DELETE = 3,\n    HEAD = 4,\n    OPTIONS = 5,\n    PATCH = 6,\n    PROPFIND = 7,\n};\n\npub const Header = struct {\n    name: []const u8,\n    value: []const u8,\n};\n\npub const Headers = struct {\n    headers: ?*libcurl.CurlSList,\n    cookies: ?[*c]const u8,\n\n    pub fn init(user_agent: [:0]const u8) !Headers {\n        const header_list = libcurl.curl_slist_append(null, user_agent);\n        if (header_list == null) {\n            return error.OutOfMemory;\n        }\n        return .{ .headers = header_list, .cookies = null };\n    }\n\n    pub fn deinit(self: *const Headers) void {\n        if (self.headers) |hdr| {\n            libcurl.curl_slist_free_all(hdr);\n        }\n    }\n\n    pub fn add(self: *Headers, header: [*c]const u8) !void {\n        // Copies the value\n        const updated_headers = libcurl.curl_slist_append(self.headers, header);\n        if (updated_headers == null) {\n            return error.OutOfMemory;\n        }\n\n        self.headers = updated_headers;\n    }\n\n    fn parseHeader(header_str: []const u8) ?Header {\n        const colon_pos = std.mem.indexOfScalar(u8, header_str, ':') orelse return null;\n\n        const name = std.mem.trim(u8, header_str[0..colon_pos], \" \\t\");\n        const value = std.mem.trim(u8, header_str[colon_pos + 1 ..], \" \\t\");\n\n        return .{ .name = name, .value = value };\n    }\n\n    pub fn iterator(self: *Headers) Iterator {\n        return .{\n            .header = self.headers,\n            .cookies = self.cookies,\n        };\n    }\n\n    const Iterator = struct {\n        header: [*c]libcurl.CurlSList,\n        cookies: ?[*c]const u8,\n\n        pub fn next(self: *Iterator) ?Header {\n            const h = self.header orelse {\n                const cookies = self.cookies orelse return null;\n                self.cookies = null;\n                return .{ .name = \"Cookie\", .value = std.mem.span(@as([*:0]const u8, cookies)) };\n            };\n\n            self.header = h.*.next;\n            return parseHeader(std.mem.span(@as([*:0]const u8, @ptrCast(h.*.data))));\n        }\n    };\n};\n\n// In normal cases, the header iterator comes from the curl linked list.\n// But it's also possible to inject a response, via `transfer.fulfill`. In that\n// case, the resposne headers are a list, []const Http.Header.\n// This union, is an iterator that exposes the same API for either case.\npub const HeaderIterator = union(enum) {\n    curl: CurlHeaderIterator,\n    list: ListHeaderIterator,\n\n    pub fn next(self: *HeaderIterator) ?Header {\n        switch (self.*) {\n            inline else => |*it| return it.next(),\n        }\n    }\n\n    const CurlHeaderIterator = struct {\n        conn: *const Connection,\n        prev: ?*libcurl.CurlHeader = null,\n\n        pub fn next(self: *CurlHeaderIterator) ?Header {\n            const h = libcurl.curl_easy_nextheader(self.conn.easy, .header, -1, self.prev) orelse return null;\n            self.prev = h;\n\n            const header = h.*;\n            return .{\n                .name = std.mem.span(header.name),\n                .value = std.mem.span(header.value),\n            };\n        }\n    };\n\n    const ListHeaderIterator = struct {\n        index: usize = 0,\n        list: []const Header,\n\n        pub fn next(self: *ListHeaderIterator) ?Header {\n            const idx = self.index;\n            if (idx == self.list.len) {\n                return null;\n            }\n            self.index = idx + 1;\n            return self.list[idx];\n        }\n    };\n};\n\nconst HeaderValue = struct {\n    value: []const u8,\n    amount: usize,\n};\n\npub const AuthChallenge = struct {\n    status: u16,\n    source: ?enum { server, proxy },\n    scheme: ?enum { basic, digest },\n    realm: ?[]const u8,\n\n    pub fn parse(status: u16, header: []const u8) !AuthChallenge {\n        var ac: AuthChallenge = .{\n            .status = status,\n            .source = null,\n            .realm = null,\n            .scheme = null,\n        };\n\n        const sep = std.mem.indexOfPos(u8, header, 0, \": \") orelse return error.InvalidHeader;\n        const hname = header[0..sep];\n        const hvalue = header[sep + 2 ..];\n\n        if (std.ascii.eqlIgnoreCase(\"WWW-Authenticate\", hname)) {\n            ac.source = .server;\n        } else if (std.ascii.eqlIgnoreCase(\"Proxy-Authenticate\", hname)) {\n            ac.source = .proxy;\n        } else {\n            return error.InvalidAuthChallenge;\n        }\n\n        const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, hvalue, std.ascii.whitespace[0..]), 0, \" \") orelse hvalue.len;\n        const _scheme = hvalue[0..pos];\n        if (std.ascii.eqlIgnoreCase(_scheme, \"basic\")) {\n            ac.scheme = .basic;\n        } else if (std.ascii.eqlIgnoreCase(_scheme, \"digest\")) {\n            ac.scheme = .digest;\n        } else {\n            return error.UnknownAuthChallengeScheme;\n        }\n\n        return ac;\n    }\n};\n\npub const ResponseHead = struct {\n    pub const MAX_CONTENT_TYPE_LEN = 64;\n\n    status: u16,\n    url: [*c]const u8,\n    redirect_count: u32,\n    _content_type_len: usize = 0,\n    _content_type: [MAX_CONTENT_TYPE_LEN]u8 = undefined,\n    // this is normally an empty list, but if the response is being injected\n    // than it'll be populated. It isn't meant to be used directly, but should\n    // be used through the transfer.responseHeaderIterator() which abstracts\n    // whether the headers are from a live curl easy handle, or injected.\n    _injected_headers: []const Header = &.{},\n\n    pub fn contentType(self: *ResponseHead) ?[]u8 {\n        if (self._content_type_len == 0) {\n            return null;\n        }\n        return self._content_type[0..self._content_type_len];\n    }\n};\n\npub const Connection = struct {\n    easy: *libcurl.Curl,\n    node: std.DoublyLinkedList.Node = .{},\n\n    pub fn init(\n        ca_blob_: ?libcurl.CurlBlob,\n        config: *const Config,\n    ) !Connection {\n        const easy = libcurl.curl_easy_init() orelse return error.FailedToInitializeEasy;\n        errdefer libcurl.curl_easy_cleanup(easy);\n\n        // timeouts\n        try libcurl.curl_easy_setopt(easy, .timeout_ms, config.httpTimeout());\n        try libcurl.curl_easy_setopt(easy, .connect_timeout_ms, config.httpConnectTimeout());\n\n        // redirect behavior\n        try libcurl.curl_easy_setopt(easy, .max_redirs, config.httpMaxRedirects());\n        try libcurl.curl_easy_setopt(easy, .follow_location, 2);\n        try libcurl.curl_easy_setopt(easy, .redir_protocols_str, \"HTTP,HTTPS\"); // remove FTP and FTPS from the default\n\n        // proxy\n        const http_proxy = config.httpProxy();\n        if (http_proxy) |proxy| {\n            try libcurl.curl_easy_setopt(easy, .proxy, proxy.ptr);\n        }\n\n        // tls\n        if (ca_blob_) |ca_blob| {\n            try libcurl.curl_easy_setopt(easy, .ca_info_blob, ca_blob);\n            if (http_proxy != null) {\n                try libcurl.curl_easy_setopt(easy, .proxy_ca_info_blob, ca_blob);\n            }\n        } else {\n            assert(config.tlsVerifyHost() == false, \"Http.init tls_verify_host\", .{});\n\n            try libcurl.curl_easy_setopt(easy, .ssl_verify_host, false);\n            try libcurl.curl_easy_setopt(easy, .ssl_verify_peer, false);\n\n            if (http_proxy != null) {\n                try libcurl.curl_easy_setopt(easy, .proxy_ssl_verify_host, false);\n                try libcurl.curl_easy_setopt(easy, .proxy_ssl_verify_peer, false);\n            }\n        }\n\n        // compression, don't remove this. CloudFront will send gzip content\n        // even if we don't support it, and then it won't be decompressed.\n        // empty string means: use whatever's available\n        try libcurl.curl_easy_setopt(easy, .accept_encoding, \"\");\n\n        // debug\n        if (comptime ENABLE_DEBUG) {\n            try libcurl.curl_easy_setopt(easy, .verbose, true);\n\n            // Sometimes the default debug output hides some useful data. You can\n            // uncomment the following line (BUT KEEP THE LIVE ABOVE AS-IS), to\n            // get more control over the data (specifically, the `CURLINFO_TEXT`\n            // can include useful data).\n\n            // try libcurl.curl_easy_setopt(easy, .debug_function, debugCallback);\n        }\n\n        return .{\n            .easy = easy,\n        };\n    }\n\n    pub fn deinit(self: *const Connection) void {\n        libcurl.curl_easy_cleanup(self.easy);\n    }\n\n    pub fn setURL(self: *const Connection, url: [:0]const u8) !void {\n        try libcurl.curl_easy_setopt(self.easy, .url, url.ptr);\n    }\n\n    // a libcurl request has 2 methods. The first is the method that\n    // controls how libcurl behaves. This specifically influences how redirects\n    // are handled. For example, if you do a POST and get a 301, libcurl will\n    // change that to a GET. But if you do a POST and get a 308, libcurl will\n    // keep the POST (and re-send the body).\n    // The second method is the actual string that's included in the request\n    // headers.\n    // These two methods can be different - you can tell curl to behave as though\n    // you made a GET, but include \"POST\" in the request header.\n    //\n    // Here, we're only concerned about the 2nd method. If we want, we'll set\n    // the first one based on whether or not we have a body.\n    //\n    // It's important that, for each use of this connection, we set the 2nd\n    // method. Else, if we make a HEAD request and re-use the connection, but\n    // DON'T reset this, it'll keep making HEAD requests.\n    // (I don't know if it's as important to reset the 1st method, or if libcurl\n    // can infer that based on the presence of the body, but we also reset it\n    // to be safe);\n    pub fn setMethod(self: *const Connection, method: Method) !void {\n        const easy = self.easy;\n        const m: [:0]const u8 = switch (method) {\n            .GET => \"GET\",\n            .POST => \"POST\",\n            .PUT => \"PUT\",\n            .DELETE => \"DELETE\",\n            .HEAD => \"HEAD\",\n            .OPTIONS => \"OPTIONS\",\n            .PATCH => \"PATCH\",\n            .PROPFIND => \"PROPFIND\",\n        };\n        try libcurl.curl_easy_setopt(easy, .custom_request, m.ptr);\n    }\n\n    pub fn setBody(self: *const Connection, body: []const u8) !void {\n        const easy = self.easy;\n        try libcurl.curl_easy_setopt(easy, .post, true);\n        try libcurl.curl_easy_setopt(easy, .post_field_size, body.len);\n        try libcurl.curl_easy_setopt(easy, .copy_post_fields, body.ptr);\n    }\n\n    pub fn setGetMode(self: *const Connection) !void {\n        try libcurl.curl_easy_setopt(self.easy, .http_get, true);\n    }\n\n    pub fn setHeaders(self: *const Connection, headers: *Headers) !void {\n        try libcurl.curl_easy_setopt(self.easy, .http_header, headers.headers);\n    }\n\n    pub fn setCookies(self: *const Connection, cookies: [*c]const u8) !void {\n        try libcurl.curl_easy_setopt(self.easy, .cookie, cookies);\n    }\n\n    pub fn setPrivate(self: *const Connection, ptr: *anyopaque) !void {\n        try libcurl.curl_easy_setopt(self.easy, .private, ptr);\n    }\n\n    pub fn setProxyCredentials(self: *const Connection, creds: [:0]const u8) !void {\n        try libcurl.curl_easy_setopt(self.easy, .proxy_user_pwd, creds.ptr);\n    }\n\n    pub fn setCredentials(self: *const Connection, creds: [:0]const u8) !void {\n        try libcurl.curl_easy_setopt(self.easy, .user_pwd, creds.ptr);\n    }\n\n    pub fn setCallbacks(\n        self: *const Connection,\n        comptime header_cb: libcurl.CurlHeaderFunction,\n        comptime data_cb: libcurl.CurlWriteFunction,\n    ) !void {\n        try libcurl.curl_easy_setopt(self.easy, .header_data, self.easy);\n        try libcurl.curl_easy_setopt(self.easy, .header_function, header_cb);\n        try libcurl.curl_easy_setopt(self.easy, .write_data, self.easy);\n        try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb);\n    }\n\n    pub fn reset(self: *const Connection) !void {\n        try libcurl.curl_easy_setopt(self.easy, .proxy, null);\n        try libcurl.curl_easy_setopt(self.easy, .http_header, null);\n\n        try libcurl.curl_easy_setopt(self.easy, .header_data, null);\n        try libcurl.curl_easy_setopt(self.easy, .header_function, null);\n\n        try libcurl.curl_easy_setopt(self.easy, .write_data, null);\n        try libcurl.curl_easy_setopt(self.easy, .write_function, discardBody);\n    }\n\n    fn discardBody(_: [*]const u8, count: usize, len: usize, _: ?*anyopaque) usize {\n        return count * len;\n    }\n\n    pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void {\n        try libcurl.curl_easy_setopt(self.easy, .proxy, if (proxy) |p| p.ptr else null);\n    }\n\n    pub fn setTlsVerify(self: *const Connection, verify: bool, use_proxy: bool) !void {\n        try libcurl.curl_easy_setopt(self.easy, .ssl_verify_host, verify);\n        try libcurl.curl_easy_setopt(self.easy, .ssl_verify_peer, verify);\n        if (use_proxy) {\n            try libcurl.curl_easy_setopt(self.easy, .proxy_ssl_verify_host, verify);\n            try libcurl.curl_easy_setopt(self.easy, .proxy_ssl_verify_peer, verify);\n        }\n    }\n\n    pub fn getEffectiveUrl(self: *const Connection) ![*c]const u8 {\n        var url: [*c]u8 = undefined;\n        try libcurl.curl_easy_getinfo(self.easy, .effective_url, &url);\n        return url;\n    }\n\n    pub fn getResponseCode(self: *const Connection) !u16 {\n        var status: c_long = undefined;\n        try libcurl.curl_easy_getinfo(self.easy, .response_code, &status);\n        if (status < 0 or status > std.math.maxInt(u16)) {\n            return 0;\n        }\n        return @intCast(status);\n    }\n\n    pub fn getRedirectCount(self: *const Connection) !u32 {\n        var count: c_long = undefined;\n        try libcurl.curl_easy_getinfo(self.easy, .redirect_count, &count);\n        return @intCast(count);\n    }\n\n    pub fn getResponseHeader(self: *const Connection, name: [:0]const u8, index: usize) ?HeaderValue {\n        var hdr: ?*libcurl.CurlHeader = null;\n        libcurl.curl_easy_header(self.easy, name, index, .header, -1, &hdr) catch |err| {\n            // ErrorHeader includes OutOfMemory — rare but real errors from curl internals.\n            // Logged and returned as null since callers don't expect errors.\n            log.err(.http, \"get response header\", .{\n                .name = name,\n                .err = err,\n            });\n            return null;\n        };\n        const h = hdr orelse return null;\n        return .{\n            .amount = h.amount,\n            .value = std.mem.span(h.value),\n        };\n    }\n\n    pub fn getPrivate(self: *const Connection) !*anyopaque {\n        var private: *anyopaque = undefined;\n        try libcurl.curl_easy_getinfo(self.easy, .private, &private);\n        return private;\n    }\n\n    // These are headers that may not be send to the users for inteception.\n    pub fn secretHeaders(_: *const Connection, headers: *Headers, http_headers: *const Config.HttpHeaders) !void {\n        if (http_headers.proxy_bearer_header) |hdr| {\n            try headers.add(hdr);\n        }\n    }\n\n    pub fn request(self: *const Connection, http_headers: *const Config.HttpHeaders) !u16 {\n        var header_list = try Headers.init(http_headers.user_agent_header);\n        defer header_list.deinit();\n        try self.secretHeaders(&header_list, http_headers);\n        try self.setHeaders(&header_list);\n\n        // Add cookies.\n        if (header_list.cookies) |cookies| {\n            try self.setCookies(cookies);\n        }\n\n        try libcurl.curl_easy_perform(self.easy);\n        return self.getResponseCode();\n    }\n};\n\npub const Handles = struct {\n    multi: *libcurl.CurlM,\n\n    pub fn init(config: *const Config) !Handles {\n        const multi = libcurl.curl_multi_init() orelse return error.FailedToInitializeMulti;\n        errdefer libcurl.curl_multi_cleanup(multi) catch {};\n\n        try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen());\n\n        return .{ .multi = multi };\n    }\n\n    pub fn deinit(self: *Handles) void {\n        libcurl.curl_multi_cleanup(self.multi) catch {};\n    }\n\n    pub fn add(self: *Handles, conn: *const Connection) !void {\n        try libcurl.curl_multi_add_handle(self.multi, conn.easy);\n    }\n\n    pub fn remove(self: *Handles, conn: *const Connection) !void {\n        try libcurl.curl_multi_remove_handle(self.multi, conn.easy);\n    }\n\n    pub fn perform(self: *Handles) !c_int {\n        var running: c_int = undefined;\n        try libcurl.curl_multi_perform(self.multi, &running);\n        return running;\n    }\n\n    pub fn poll(self: *Handles, extra_fds: []libcurl.CurlWaitFd, timeout_ms: c_int) !void {\n        try libcurl.curl_multi_poll(self.multi, extra_fds, timeout_ms, null);\n    }\n\n    pub const MultiMessage = struct {\n        conn: Connection,\n        err: ?Error,\n    };\n\n    pub fn readMessage(self: *Handles) ?MultiMessage {\n        var messages_count: c_int = 0;\n        const msg = libcurl.curl_multi_info_read(self.multi, &messages_count) orelse return null;\n        return switch (msg.data) {\n            .done => |err| .{\n                .conn = .{ .easy = msg.easy_handle },\n                .err = err,\n            },\n            else => unreachable,\n        };\n    }\n};\n\nfn debugCallback(_: *libcurl.Curl, msg_type: libcurl.CurlInfoType, raw: [*c]u8, len: usize, _: *anyopaque) c_int {\n    const data = raw[0..len];\n    switch (msg_type) {\n        .text => std.debug.print(\"libcurl [text]: {s}\\n\", .{data}),\n        .header_out => std.debug.print(\"libcurl [req-h]: {s}\\n\", .{data}),\n        .header_in => std.debug.print(\"libcurl [res-h]: {s}\\n\", .{data}),\n        // .data_in => std.debug.print(\"libcurl [res-b]: {s}\\n\", .{data}),\n        else => std.debug.print(\"libcurl ?? {d}\\n\", .{msg_type}),\n    }\n    return 0;\n}\n"
  },
  {
    "path": "src/network/websocket.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst posix = std.posix;\nconst Allocator = std.mem.Allocator;\nconst ArenaAllocator = std.heap.ArenaAllocator;\n\nconst log = @import(\"lightpanda\").log;\nconst assert = @import(\"lightpanda\").assert;\nconst CDP_MAX_MESSAGE_SIZE = @import(\"../Config.zig\").CDP_MAX_MESSAGE_SIZE;\n\nconst Fragments = struct {\n    type: Message.Type,\n    message: std.ArrayList(u8),\n};\n\npub const Message = struct {\n    type: Type,\n    data: []const u8,\n    cleanup_fragment: bool,\n\n    pub const Type = enum {\n        text,\n        binary,\n        close,\n        ping,\n        pong,\n    };\n};\n\n// These are the only websocket types that we're currently sending\nconst OpCode = enum(u8) {\n    text = 128 | 1,\n    close = 128 | 8,\n    pong = 128 | 10,\n};\n\n// WebSocket message reader. Given websocket message, acts as an iterator that\n// can return zero or more Messages. When next returns null, any incomplete\n// message will remain in reader.data\npub fn Reader(comptime EXPECT_MASK: bool) type {\n    return struct {\n        allocator: Allocator,\n\n        // position in buf of the start of the next message\n        pos: usize = 0,\n\n        // position in buf up until where we have valid data\n        // (any new reads must be placed after this)\n        len: usize = 0,\n\n        // we add 140 to allow 1 control message (ping/pong/close) to be\n        // fragmented into a normal message.\n        buf: []u8,\n\n        fragments: ?Fragments = null,\n\n        const Self = @This();\n\n        pub fn init(allocator: Allocator) !Self {\n            const buf = try allocator.alloc(u8, 16 * 1024);\n            return .{\n                .buf = buf,\n                .allocator = allocator,\n            };\n        }\n\n        pub fn deinit(self: *Self) void {\n            self.cleanup();\n            self.allocator.free(self.buf);\n        }\n\n        pub fn cleanup(self: *Self) void {\n            if (self.fragments) |*f| {\n                f.message.deinit(self.allocator);\n                self.fragments = null;\n            }\n        }\n\n        pub fn readBuf(self: *Self) []u8 {\n            // We might have read a partial http or websocket message.\n            // Subsequent reads must read from where we left off.\n            return self.buf[self.len..];\n        }\n\n        pub fn next(self: *Self) !?Message {\n            LOOP: while (true) {\n                var buf = self.buf[self.pos..self.len];\n\n                const length_of_len, const message_len = extractLengths(buf) orelse {\n                    // we don't have enough bytes\n                    return null;\n                };\n\n                const byte1 = buf[0];\n\n                if (byte1 & 112 != 0) {\n                    return error.ReservedFlags;\n                }\n\n                if (comptime EXPECT_MASK) {\n                    if (buf[1] & 128 != 128) {\n                        // client -> server messages _must_ be masked\n                        return error.NotMasked;\n                    }\n                } else if (buf[1] & 128 != 0) {\n                    // server -> client are never masked\n                    return error.Masked;\n                }\n\n                var is_control = false;\n                var is_continuation = false;\n                var message_type: Message.Type = undefined;\n                switch (byte1 & 15) {\n                    0 => is_continuation = true,\n                    1 => message_type = .text,\n                    2 => message_type = .binary,\n                    8 => {\n                        is_control = true;\n                        message_type = .close;\n                    },\n                    9 => {\n                        is_control = true;\n                        message_type = .ping;\n                    },\n                    10 => {\n                        is_control = true;\n                        message_type = .pong;\n                    },\n                    else => return error.InvalidMessageType,\n                }\n\n                if (is_control) {\n                    if (message_len > 125) {\n                        return error.ControlTooLarge;\n                    }\n                } else if (message_len > CDP_MAX_MESSAGE_SIZE) {\n                    return error.TooLarge;\n                } else if (message_len > self.buf.len) {\n                    const len = self.buf.len;\n                    self.buf = try growBuffer(self.allocator, self.buf, message_len);\n                    buf = self.buf[0..len];\n                    // we need more data\n                    return null;\n                } else if (buf.len < message_len) {\n                    // we need more data\n                    return null;\n                }\n\n                // prefix + length_of_len + mask\n                const header_len = 2 + length_of_len + if (comptime EXPECT_MASK) 4 else 0;\n\n                const payload = buf[header_len..message_len];\n                if (comptime EXPECT_MASK) {\n                    mask(buf[header_len - 4 .. header_len], payload);\n                }\n\n                // whatever happens after this, we know where the next message starts\n                self.pos += message_len;\n\n                const fin = byte1 & 128 == 128;\n\n                if (is_continuation) {\n                    const fragments = &(self.fragments orelse return error.InvalidContinuation);\n                    if (fragments.message.items.len + message_len > CDP_MAX_MESSAGE_SIZE) {\n                        return error.TooLarge;\n                    }\n\n                    try fragments.message.appendSlice(self.allocator, payload);\n\n                    if (fin == false) {\n                        // maybe we have more parts of the message waiting\n                        continue :LOOP;\n                    }\n\n                    // this continuation is done!\n                    return .{\n                        .type = fragments.type,\n                        .data = fragments.message.items,\n                        .cleanup_fragment = true,\n                    };\n                }\n\n                const can_be_fragmented = message_type == .text or message_type == .binary;\n                if (self.fragments != null and can_be_fragmented) {\n                    // if this isn't a continuation, then we can't have fragments\n                    return error.NestedFragementation;\n                }\n\n                if (fin == false) {\n                    if (can_be_fragmented == false) {\n                        return error.InvalidContinuation;\n                    }\n\n                    // not continuation, and not fin. It has to be the first message\n                    // in a fragmented message.\n                    var fragments = Fragments{ .message = .{}, .type = message_type };\n                    try fragments.message.appendSlice(self.allocator, payload);\n                    self.fragments = fragments;\n                    continue :LOOP;\n                }\n\n                return .{\n                    .data = payload,\n                    .type = message_type,\n                    .cleanup_fragment = false,\n                };\n            }\n        }\n\n        fn extractLengths(buf: []const u8) ?struct { usize, usize } {\n            if (buf.len < 2) {\n                return null;\n            }\n\n            const length_of_len: usize = switch (buf[1] & 127) {\n                126 => 2,\n                127 => 8,\n                else => 0,\n            };\n\n            if (buf.len < length_of_len + 2) {\n                // we definitely don't have enough buf yet\n                return null;\n            }\n\n            const message_len = switch (length_of_len) {\n                2 => @as(u16, @intCast(buf[3])) | @as(u16, @intCast(buf[2])) << 8,\n                8 => @as(u64, @intCast(buf[9])) | @as(u64, @intCast(buf[8])) << 8 | @as(u64, @intCast(buf[7])) << 16 | @as(u64, @intCast(buf[6])) << 24 | @as(u64, @intCast(buf[5])) << 32 | @as(u64, @intCast(buf[4])) << 40 | @as(u64, @intCast(buf[3])) << 48 | @as(u64, @intCast(buf[2])) << 56,\n                else => buf[1] & 127,\n            } + length_of_len + 2 + if (comptime EXPECT_MASK) 4 else 0; // +2 for header prefix, +4 for mask;\n\n            return .{ length_of_len, message_len };\n        }\n\n        // This is called after we've processed complete websocket messages (this\n        // only applies to websocket messages).\n        // There are three cases:\n        // 1 - We don't have any incomplete data (for a subsequent message) in buf.\n        //     This is the easier to handle, we can set pos & len to 0.\n        // 2 - We have part of the next message, but we know it'll fit in the\n        //     remaining buf. We don't need to do anything\n        // 3 - We have part of the next message, but either it won't fight into the\n        //     remaining buffer, or we don't know (because we don't have enough\n        //     of the header to tell the length). We need to \"compact\" the buffer\n        fn compact(self: *Self) void {\n            const pos = self.pos;\n            const len = self.len;\n\n            assert(pos <= len, \"Client.Reader.compact precondition\", .{ .pos = pos, .len = len });\n\n            // how many (if any) partial bytes do we have\n            const partial_bytes = len - pos;\n\n            if (partial_bytes == 0) {\n                // We have no partial bytes. Setting these to 0 ensures that we\n                // get the best utilization of our buffer\n                self.pos = 0;\n                self.len = 0;\n                return;\n            }\n\n            const partial = self.buf[pos..len];\n\n            // If we have enough bytes of the next message to tell its length\n            // we'll be able to figure out whether we need to do anything or not.\n            if (extractLengths(partial)) |length_meta| {\n                const next_message_len = length_meta.@\"1\";\n                // if this isn't true, then we have a full message and it\n                // should have been processed.\n                assert(pos <= len, \"Client.Reader.compact postcondition\", .{ .next_len = next_message_len, .partial = partial_bytes });\n\n                const missing_bytes = next_message_len - partial_bytes;\n\n                const free_space = self.buf.len - len;\n                if (missing_bytes < free_space) {\n                    // we have enough space in our buffer, as is,\n                    return;\n                }\n            }\n\n            // We're here because we either don't have enough bytes of the next\n            // message, or we know that it won't fit in our buffer as-is.\n            std.mem.copyForwards(u8, self.buf, partial);\n            self.pos = 0;\n            self.len = partial_bytes;\n        }\n    };\n}\n\npub const WsConnection = struct {\n    // CLOSE, 2 length, code\n    const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000\n    const CLOSE_GOING_AWAY = [_]u8{ 136, 2, 3, 233 }; // code: 1001\n    const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009\n    const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002\n    // \"private-use\" close codes must be from 4000-49999\n    const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000\n\n    socket: posix.socket_t,\n    socket_flags: usize,\n    reader: Reader(true),\n    send_arena: ArenaAllocator,\n    json_version_response: []const u8,\n    timeout_ms: u32,\n\n    pub fn init(socket: posix.socket_t, allocator: Allocator, json_version_response: []const u8, timeout_ms: u32) !WsConnection {\n        const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);\n        const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));\n        assert(socket_flags & nonblocking == nonblocking, \"WsConnection.init blocking\", .{});\n\n        var reader = try Reader(true).init(allocator);\n        errdefer reader.deinit();\n\n        return .{\n            .socket = socket,\n            .socket_flags = socket_flags,\n            .reader = reader,\n            .send_arena = ArenaAllocator.init(allocator),\n            .json_version_response = json_version_response,\n            .timeout_ms = timeout_ms,\n        };\n    }\n\n    pub fn deinit(self: *WsConnection) void {\n        self.reader.deinit();\n        self.send_arena.deinit();\n    }\n\n    pub fn send(self: *WsConnection, data: []const u8) !void {\n        var pos: usize = 0;\n        var changed_to_blocking: bool = false;\n        defer _ = self.send_arena.reset(.{ .retain_with_limit = 1024 * 32 });\n\n        defer if (changed_to_blocking) {\n            // We had to change our socket to blocking me to get our write out\n            // We need to change it back to non-blocking.\n            _ = posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags) catch |err| {\n                log.err(.app, \"ws restore nonblocking\", .{ .err = err });\n            };\n        };\n\n        LOOP: while (pos < data.len) {\n            const written = posix.write(self.socket, data[pos..]) catch |err| switch (err) {\n                error.WouldBlock => {\n                    // self.socket is nonblocking, because we don't want to block\n                    // reads. But our life is a lot easier if we block writes,\n                    // largely, because we don't have to maintain a queue of pending\n                    // writes (which would each need their own allocations). So\n                    // if we get a WouldBlock error, we'll switch the socket to\n                    // blocking and switch it back to non-blocking after the write\n                    // is complete. Doesn't seem particularly efficiently, but\n                    // this should virtually never happen.\n                    assert(changed_to_blocking == false, \"WsConnection.double block\", .{});\n                    changed_to_blocking = true;\n                    _ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true })));\n                    continue :LOOP;\n                },\n                else => return err,\n            };\n\n            if (written == 0) {\n                return error.Closed;\n            }\n            pos += written;\n        }\n    }\n\n    const EMPTY_PONG = [_]u8{ 138, 0 };\n\n    fn sendPong(self: *WsConnection, data: []const u8) !void {\n        if (data.len == 0) {\n            return self.send(&EMPTY_PONG);\n        }\n        var header_buf: [10]u8 = undefined;\n        const header = websocketHeader(&header_buf, .pong, data.len);\n\n        const allocator = self.send_arena.allocator();\n        const framed = try allocator.alloc(u8, header.len + data.len);\n        @memcpy(framed[0..header.len], header);\n        @memcpy(framed[header.len..], data);\n        return self.send(framed);\n    }\n\n    // called by CDP\n    // Websocket frames have a variable length header. For server-client,\n    // it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have\n    // writev, so we need to get creative. We'll JSON serialize to a\n    // buffer, where the first 10 bytes are reserved. We can then backfill\n    // the header and send the slice.\n    pub fn sendJSON(self: *WsConnection, message: anytype, opts: std.json.Stringify.Options) !void {\n        const allocator = self.send_arena.allocator();\n\n        var aw = try std.Io.Writer.Allocating.initCapacity(allocator, 512);\n\n        // reserve space for the maximum possible header\n        try aw.writer.writeAll(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });\n        try std.json.Stringify.value(message, opts, &aw.writer);\n        const framed = fillWebsocketHeader(aw.toArrayList());\n        return self.send(framed);\n    }\n\n    pub fn sendJSONRaw(\n        self: *WsConnection,\n        buf: std.ArrayList(u8),\n    ) !void {\n        // Dangerous API!. We assume the caller has reserved the first 10\n        // bytes in `buf`.\n        const framed = fillWebsocketHeader(buf);\n        return self.send(framed);\n    }\n\n    pub fn read(self: *WsConnection) !usize {\n        const n = try posix.read(self.socket, self.reader.readBuf());\n        self.reader.len += n;\n        return n;\n    }\n\n    pub fn processMessages(self: *WsConnection, handler: anytype) !bool {\n        var reader = &self.reader;\n        while (true) {\n            const msg = reader.next() catch |err| {\n                switch (err) {\n                    error.TooLarge => self.send(&CLOSE_TOO_BIG) catch {},\n                    error.NotMasked => self.send(&CLOSE_PROTOCOL_ERROR) catch {},\n                    error.ReservedFlags => self.send(&CLOSE_PROTOCOL_ERROR) catch {},\n                    error.InvalidMessageType => self.send(&CLOSE_PROTOCOL_ERROR) catch {},\n                    error.ControlTooLarge => self.send(&CLOSE_PROTOCOL_ERROR) catch {},\n                    error.InvalidContinuation => self.send(&CLOSE_PROTOCOL_ERROR) catch {},\n                    error.NestedFragementation => self.send(&CLOSE_PROTOCOL_ERROR) catch {},\n                    error.OutOfMemory => {}, // don't borther trying to send an error in this case\n                }\n                return err;\n            } orelse break;\n\n            switch (msg.type) {\n                .pong => {},\n                .ping => try self.sendPong(msg.data),\n                .close => {\n                    self.send(&CLOSE_NORMAL) catch {};\n                    return false;\n                },\n                .text, .binary => if (handler.handleMessage(msg.data) == false) {\n                    return false;\n                },\n            }\n            if (msg.cleanup_fragment) {\n                reader.cleanup();\n            }\n        }\n\n        // We might have read part of the next message. Our reader potentially\n        // has to move data around in its buffer to make space.\n        reader.compact();\n        return true;\n    }\n\n    pub fn upgrade(self: *WsConnection, request: []u8) !void {\n        // our caller already confirmed that we have a trailing \\r\\n\\r\\n\n        const request_line_end = std.mem.indexOfScalar(u8, request, '\\r') orelse unreachable;\n        const request_line = request[0..request_line_end];\n\n        if (!std.ascii.endsWithIgnoreCase(request_line, \"http/1.1\")) {\n            return error.InvalidProtocol;\n        }\n\n        // we need to extract the sec-websocket-key value\n        var key: []const u8 = \"\";\n\n        // we need to make sure that we got all the necessary headers + values\n        var required_headers: u8 = 0;\n\n        // can't std.mem.split because it forces the iterated value to be const\n        // (we could @constCast...)\n\n        var buf = request[request_line_end + 2 ..];\n\n        while (buf.len > 4) {\n            const index = std.mem.indexOfScalar(u8, buf, '\\r') orelse unreachable;\n            const separator = std.mem.indexOfScalar(u8, buf[0..index], ':') orelse return error.InvalidRequest;\n\n            const name = std.mem.trim(u8, toLower(buf[0..separator]), &std.ascii.whitespace);\n            const value = std.mem.trim(u8, buf[(separator + 1)..index], &std.ascii.whitespace);\n\n            if (std.mem.eql(u8, name, \"upgrade\")) {\n                if (!std.ascii.eqlIgnoreCase(\"websocket\", value)) {\n                    return error.InvalidUpgradeHeader;\n                }\n                required_headers |= 1;\n            } else if (std.mem.eql(u8, name, \"sec-websocket-version\")) {\n                if (value.len != 2 or value[0] != '1' or value[1] != '3') {\n                    return error.InvalidVersionHeader;\n                }\n                required_headers |= 2;\n            } else if (std.mem.eql(u8, name, \"connection\")) {\n                // find if connection header has upgrade in it, example header:\n                // Connection: keep-alive, Upgrade\n                if (std.ascii.indexOfIgnoreCase(value, \"upgrade\") == null) {\n                    return error.InvalidConnectionHeader;\n                }\n                required_headers |= 4;\n            } else if (std.mem.eql(u8, name, \"sec-websocket-key\")) {\n                key = value;\n                required_headers |= 8;\n            }\n\n            const next = index + 2;\n            buf = buf[next..];\n        }\n\n        if (required_headers != 15) {\n            return error.MissingHeaders;\n        }\n\n        // our caller has already made sure this request ended in \\r\\n\\r\\n\n        // so it isn't something we need to check again\n\n        const alloc = self.send_arena.allocator();\n\n        const response = blk: {\n            // Response to an ugprade request is always this, with\n            // the Sec-Websocket-Accept value a spacial sha1 hash of the\n            // request \"sec-websocket-version\" and a magic value.\n\n            const template =\n                \"HTTP/1.1 101 Switching Protocols\\r\\n\" ++\n                \"Upgrade: websocket\\r\\n\" ++\n                \"Connection: upgrade\\r\\n\" ++\n                \"Sec-Websocket-Accept: 0000000000000000000000000000\\r\\n\\r\\n\";\n\n            // The response will be sent via the IO Loop and thus has to have its\n            // own lifetime.\n            const res = try alloc.dupe(u8, template);\n\n            // magic response\n            const key_pos = res.len - 32;\n            var h: [20]u8 = undefined;\n            var hasher = std.crypto.hash.Sha1.init(.{});\n            hasher.update(key);\n            // websocket spec always used this value\n            hasher.update(\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\");\n            hasher.final(&h);\n\n            _ = std.base64.standard.Encoder.encode(res[key_pos .. key_pos + 28], h[0..]);\n\n            break :blk res;\n        };\n\n        return self.send(response);\n    }\n\n    pub fn sendHttpError(self: *WsConnection, comptime status: u16, comptime body: []const u8) void {\n        const response = std.fmt.comptimePrint(\n            \"HTTP/1.1 {d} \\r\\nConnection: Close\\r\\nContent-Length: {d}\\r\\n\\r\\n{s}\",\n            .{ status, body.len, body },\n        );\n\n        // we're going to close this connection anyways, swallowing any\n        // error seems safe\n        self.send(response) catch {};\n    }\n\n    pub fn getAddress(self: *WsConnection) !std.net.Address {\n        var address: std.net.Address = undefined;\n        var socklen: posix.socklen_t = @sizeOf(std.net.Address);\n        try posix.getpeername(self.socket, &address.any, &socklen);\n        return address;\n    }\n\n    pub fn sendClose(self: *WsConnection) void {\n        self.send(&CLOSE_GOING_AWAY) catch {};\n    }\n\n    pub fn shutdown(self: *WsConnection) void {\n        posix.shutdown(self.socket, .recv) catch {};\n    }\n\n    pub fn setBlocking(self: *WsConnection, blocking: bool) !void {\n        if (blocking) {\n            _ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true })));\n        } else {\n            _ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags);\n        }\n    }\n};\n\nfn fillWebsocketHeader(buf: std.ArrayList(u8)) []const u8 {\n    // can't use buf[0..10] here, because the header length\n    // is variable. If it's just 2 bytes, for example, we need the\n    // framed message to be:\n    //     h1, h2, data\n    // If we use buf[0..10], we'd get:\n    //    h1, h2, 0, 0, 0, 0, 0, 0, 0, 0, data\n\n    var header_buf: [10]u8 = undefined;\n\n    // -10 because we reserved 10 bytes for the header above\n    const header = websocketHeader(&header_buf, .text, buf.items.len - 10);\n    const start = 10 - header.len;\n\n    const message = buf.items;\n    @memcpy(message[start..10], header);\n    return message[start..];\n}\n\n// makes the assumption that our caller reserved the first\n// 10 bytes for the header\nfn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 {\n    assert(buf.len == 10, \"Websocket.Header\", .{ .len = buf.len });\n\n    const len = payload_len;\n    buf[0] = 128 | @intFromEnum(op_code); // fin | opcode\n\n    if (len <= 125) {\n        buf[1] = @intCast(len);\n        return buf[0..2];\n    }\n\n    if (len < 65536) {\n        buf[1] = 126;\n        buf[2] = @intCast((len >> 8) & 0xFF);\n        buf[3] = @intCast(len & 0xFF);\n        return buf[0..4];\n    }\n\n    buf[1] = 127;\n    buf[2] = 0;\n    buf[3] = 0;\n    buf[4] = 0;\n    buf[5] = 0;\n    buf[6] = @intCast((len >> 24) & 0xFF);\n    buf[7] = @intCast((len >> 16) & 0xFF);\n    buf[8] = @intCast((len >> 8) & 0xFF);\n    buf[9] = @intCast(len & 0xFF);\n    return buf[0..10];\n}\n\nfn growBuffer(allocator: Allocator, buf: []u8, required_capacity: usize) ![]u8 {\n    // from std.ArrayList\n    var new_capacity = buf.len;\n    while (true) {\n        new_capacity +|= new_capacity / 2 + 8;\n        if (new_capacity >= required_capacity) break;\n    }\n\n    log.debug(.app, \"CDP buffer growth\", .{ .from = buf.len, .to = new_capacity });\n\n    if (allocator.resize(buf, new_capacity)) {\n        return buf.ptr[0..new_capacity];\n    }\n    const new_buffer = try allocator.alloc(u8, new_capacity);\n    @memcpy(new_buffer[0..buf.len], buf);\n    allocator.free(buf);\n    return new_buffer;\n}\n\n// In-place string lowercase\nfn toLower(str: []u8) []u8 {\n    for (str, 0..) |ch, i| {\n        str[i] = std.ascii.toLower(ch);\n    }\n    return str;\n}\n\n// Used when SIMD isn't available, or for any remaining part of the message\n// which is too small to effectively use SIMD.\nfn simpleMask(m: []const u8, payload: []u8) void {\n    for (payload, 0..) |b, i| {\n        payload[i] = b ^ m[i & 3];\n    }\n}\n\n// Zig is in a weird backend transition right now. Need to determine if\n// SIMD is even available.\nconst backend_supports_vectors = switch (builtin.zig_backend) {\n    .stage2_llvm, .stage2_c => true,\n    else => false,\n};\n\n// Websocket messages from client->server are masked using a 4 byte XOR mask\nfn mask(m: []const u8, payload: []u8) void {\n    var data = payload;\n\n    if (!comptime backend_supports_vectors) return simpleMask(m, data);\n\n    const vector_size = std.simd.suggestVectorLength(u8) orelse @sizeOf(usize);\n    if (data.len >= vector_size) {\n        const mask_vector = std.simd.repeat(vector_size, @as(@Vector(4, u8), m[0..4].*));\n        while (data.len >= vector_size) {\n            const slice = data[0..vector_size];\n            const masked_data_slice: @Vector(vector_size, u8) = slice.*;\n            slice.* = masked_data_slice ^ mask_vector;\n            data = data[vector_size..];\n        }\n    }\n    simpleMask(m, data);\n}\n\nconst testing = std.testing;\n\ntest \"mask\" {\n    var buf: [4000]u8 = undefined;\n    const messages = [_][]const u8{ \"1234\", \"1234\" ** 99, \"1234\" ** 999 };\n    for (messages) |message| {\n        // we need the message to be mutable since mask operates in-place\n        const payload = buf[0..message.len];\n        @memcpy(payload, message);\n\n        mask(&.{ 1, 2, 200, 240 }, payload);\n        try testing.expectEqual(false, std.mem.eql(u8, payload, message));\n\n        mask(&.{ 1, 2, 200, 240 }, payload);\n        try testing.expectEqual(true, std.mem.eql(u8, payload, message));\n    }\n}\n"
  },
  {
    "path": "src/slab.zig",
    "content": "const std = @import(\"std\");\nconst assert = std.debug.assert;\n\nconst Allocator = std.mem.Allocator;\nconst Alignment = std.mem.Alignment;\n\nconst Slab = struct {\n    alignment: Alignment,\n    item_size: usize,\n    max_slot_count: usize,\n\n    bitset: std.bit_set.DynamicBitSetUnmanaged,\n    chunks: std.ArrayList([]u8),\n\n    pub fn init(\n        allocator: Allocator,\n        alignment: Alignment,\n        item_size: usize,\n        max_slot_count: usize,\n    ) !Slab {\n        return .{\n            .alignment = alignment,\n            .item_size = item_size,\n            .bitset = try .initFull(allocator, 0),\n            .chunks = .empty,\n            .max_slot_count = max_slot_count,\n        };\n    }\n\n    pub fn deinit(self: *Slab, allocator: Allocator) void {\n        self.bitset.deinit(allocator);\n\n        for (self.chunks.items) |chunk| {\n            allocator.rawFree(chunk, self.alignment, @returnAddress());\n        }\n\n        self.chunks.deinit(allocator);\n    }\n\n    inline fn calculateChunkSize(self: *Slab, chunk_index: usize) usize {\n        const safe_index: u6 = @intCast(@min(std.math.maxInt(u6), chunk_index));\n        const exponential = @as(usize, 1) << safe_index;\n        return @min(exponential, self.max_slot_count);\n    }\n\n    inline fn toBitsetIndex(self: *Slab, chunk_index: usize, slot_index: usize) usize {\n        var offset: usize = 0;\n        for (0..chunk_index) |i| {\n            const chunk_size = self.calculateChunkSize(i);\n            offset += chunk_size;\n        }\n        return offset + slot_index;\n    }\n\n    inline fn toChunkAndSlotIndices(self: *Slab, bitset_index: usize) struct { usize, usize } {\n        var offset: usize = 0;\n        var chunk_index: usize = 0;\n\n        while (chunk_index < self.chunks.items.len) : (chunk_index += 1) {\n            const chunk_size = self.calculateChunkSize(chunk_index);\n            if (bitset_index < offset + chunk_size) {\n                return .{ chunk_index, bitset_index - offset };\n            }\n\n            offset += chunk_size;\n        }\n\n        unreachable;\n    }\n\n    fn alloc(self: *Slab, allocator: Allocator) ![]u8 {\n        if (self.bitset.findFirstSet()) |index| {\n            const chunk_index, const slot_index = self.toChunkAndSlotIndices(index);\n\n            // if we have a free slot\n            self.bitset.unset(index);\n\n            const chunk = self.chunks.items[chunk_index];\n            const offset = slot_index * self.item_size;\n            return chunk.ptr[offset..][0..self.item_size];\n        } else {\n            const old_capacity = self.bitset.bit_length;\n\n            // if we have don't have a free slot\n            try self.allocateChunk(allocator);\n\n            const first_slot_index = old_capacity;\n            self.bitset.unset(first_slot_index);\n\n            const new_chunk = self.chunks.items[self.chunks.items.len - 1];\n            return new_chunk.ptr[0..self.item_size];\n        }\n    }\n\n    fn free(self: *Slab, ptr: [*]u8) void {\n        const addr = @intFromPtr(ptr);\n\n        for (self.chunks.items, 0..) |chunk, i| {\n            const chunk_start = @intFromPtr(chunk.ptr);\n            const chunk_end = chunk_start + chunk.len;\n\n            if (addr >= chunk_start and addr < chunk_end) {\n                const offset = addr - chunk_start;\n                const slot_index = offset / self.item_size;\n\n                const bitset_index = self.toBitsetIndex(i, slot_index);\n                assert(!self.bitset.isSet(bitset_index));\n\n                self.bitset.set(bitset_index);\n                return;\n            }\n        }\n\n        unreachable;\n    }\n\n    fn allocateChunk(self: *Slab, allocator: Allocator) !void {\n        const next_chunk_size = self.calculateChunkSize(self.chunks.items.len);\n        const chunk_len = self.item_size * next_chunk_size;\n\n        const chunk_ptr = allocator.rawAlloc(\n            chunk_len,\n            self.alignment,\n            @returnAddress(),\n        ) orelse return error.FailedChildAllocation;\n\n        const chunk = chunk_ptr[0..chunk_len];\n        try self.chunks.append(allocator, chunk);\n\n        const new_capacity = self.bitset.bit_length + next_chunk_size;\n        try self.bitset.resize(allocator, new_capacity, true);\n    }\n\n    const Stats = struct {\n        key: SlabKey,\n        item_size: usize,\n        chunk_count: usize,\n        total_slots: usize,\n        slots_in_use: usize,\n        slots_free: usize,\n        bytes_allocated: usize,\n        bytes_in_use: usize,\n        bytes_free: usize,\n        utilization_ratio: f64,\n    };\n\n    fn getStats(self: *const Slab, key: SlabKey) Stats {\n        const total_slots = self.bitset.bit_length;\n        const free_slots = self.bitset.count();\n        const used_slots = total_slots - free_slots;\n        const bytes_allocated = total_slots * self.item_size;\n        const bytes_in_use = used_slots * self.item_size;\n\n        const utilization_ratio = if (bytes_allocated > 0)\n            @as(f64, @floatFromInt(bytes_in_use)) / @as(f64, @floatFromInt(bytes_allocated))\n        else\n            0.0;\n\n        return .{\n            .key = key,\n            .item_size = self.item_size,\n            .chunk_count = self.chunks.items.len,\n            .total_slots = total_slots,\n            .slots_in_use = used_slots,\n            .slots_free = free_slots,\n            .bytes_allocated = bytes_allocated,\n            .bytes_in_use = bytes_in_use,\n            .bytes_free = free_slots * self.item_size,\n            .utilization_ratio = utilization_ratio,\n        };\n    }\n};\n\nconst SlabKey = struct {\n    size: usize,\n    alignment: Alignment,\n};\n\npub const SlabAllocator = struct {\n    const Self = @This();\n\n    child_allocator: Allocator,\n    max_slot_count: usize,\n\n    slabs: std.ArrayHashMapUnmanaged(SlabKey, Slab, struct {\n        const Context = @This();\n\n        pub fn hash(_: Context, key: SlabKey) u32 {\n            var hasher = std.hash.Wyhash.init(0);\n            std.hash.autoHash(&hasher, key.size);\n            std.hash.autoHash(&hasher, key.alignment);\n            return @truncate(hasher.final());\n        }\n\n        pub fn eql(_: Context, a: SlabKey, b: SlabKey, _: usize) bool {\n            return a.size == b.size and a.alignment == b.alignment;\n        }\n    }, false) = .empty,\n\n    pub fn init(child_allocator: Allocator, max_slot_count: usize) Self {\n        assert(std.math.isPowerOfTwo(max_slot_count));\n\n        return .{\n            .child_allocator = child_allocator,\n            .slabs = .empty,\n            .max_slot_count = max_slot_count,\n        };\n    }\n\n    pub fn deinit(self: *Self) void {\n        for (self.slabs.values()) |*slab| {\n            slab.deinit(self.child_allocator);\n        }\n\n        self.slabs.deinit(self.child_allocator);\n    }\n\n    pub const ResetKind = enum {\n        /// Free all chunks and release all memory.\n        clear,\n        /// Keep all chunks, reset trees to reuse memory.\n        retain_capacity,\n    };\n\n    /// This clears all of the stored memory, freeing the currently used chunks.\n    pub fn reset(self: *Self, kind: ResetKind) void {\n        switch (kind) {\n            .clear => {\n                for (self.slabs.values()) |*slab| {\n                    for (slab.chunks.items) |chunk| {\n                        self.child_allocator.free(chunk);\n                    }\n\n                    slab.chunks.clearAndFree(self.child_allocator);\n                    slab.bitset.deinit(self.child_allocator);\n                }\n\n                self.slabs.clearAndFree(self.child_allocator);\n            },\n            .retain_capacity => {\n                for (self.slabs.values()) |*slab| {\n                    slab.bitset.setAll();\n                }\n            },\n        }\n    }\n\n    const Stats = struct {\n        total_allocated_bytes: usize,\n        bytes_in_use: usize,\n        bytes_free: usize,\n        slab_count: usize,\n        total_chunks: usize,\n        total_slots: usize,\n        slots_in_use: usize,\n        slots_free: usize,\n        fragmentation_ratio: f64,\n        utilization_ratio: f64,\n        slabs: []const Slab.Stats,\n\n        pub fn print(self: *const Stats, stream: *std.io.Writer) !void {\n            try stream.print(\"\\n\", .{});\n            try stream.print(\"\\n=== Slab Allocator Statistics ===\\n\", .{});\n            try stream.print(\"Overall Memory:\\n\", .{});\n            try stream.print(\"  Total allocated: {} bytes ({d:.2} MB)\\n\", .{\n                self.total_allocated_bytes,\n                @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0,\n            });\n            try stream.print(\"  In use:          {} bytes ({d:.2} MB)\\n\", .{\n                self.bytes_in_use,\n                @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0,\n            });\n            try stream.print(\"  Free:            {} bytes ({d:.2} MB)\\n\", .{\n                self.bytes_free,\n                @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0,\n            });\n\n            try stream.print(\"\\nOverall Structure:\\n\", .{});\n            try stream.print(\"  Slab Count:    {}\\n\", .{self.slab_count});\n            try stream.print(\"  Total chunks:    {}\\n\", .{self.total_chunks});\n            try stream.print(\"  Total slots:     {}\\n\", .{self.total_slots});\n            try stream.print(\"  Slots in use:    {}\\n\", .{self.slots_in_use});\n            try stream.print(\"  Slots free:      {}\\n\", .{self.slots_free});\n\n            try stream.print(\"\\nOverall Efficiency:\\n\", .{});\n            try stream.print(\"  Utilization:     {d:.1}%\\n\", .{self.utilization_ratio * 100.0});\n            try stream.print(\"  Fragmentation:   {d:.1}%\\n\", .{self.fragmentation_ratio * 100.0});\n\n            if (self.slabs.len > 0) {\n                try stream.print(\"\\nPer-Slab Breakdown:\\n\", .{});\n                try stream.print(\n                    \"  {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\\n\",\n                    .{ \"Size\", \"Algn\", \"Chunks\", \"Slots\", \"InUse\", \"Bytes\", \"Util%\" },\n                );\n                try stream.print(\n                    \"  {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\\n\",\n                    .{ \"\", \"\", \"\", \"\", \"\", \"\", \"\" },\n                );\n\n                for (self.slabs) |slab| {\n                    try stream.print(\"  {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\\n\", .{\n                        slab.key.size,\n                        @intFromEnum(slab.key.alignment),\n                        slab.chunk_count,\n                        slab.total_slots,\n                        slab.slots_in_use,\n                        slab.bytes_allocated,\n                        slab.utilization_ratio * 100.0,\n                    });\n                }\n            }\n        }\n    };\n\n    pub fn getStats(self: *Self, a: std.mem.Allocator) !Stats {\n        var slab_stats: std.ArrayList(Slab.Stats) = try .initCapacity(a, self.slabs.entries.len);\n        errdefer slab_stats.deinit(a);\n\n        var stats = Stats{\n            .total_allocated_bytes = 0,\n            .bytes_in_use = 0,\n            .bytes_free = 0,\n            .slab_count = self.slabs.count(),\n            .total_chunks = 0,\n            .total_slots = 0,\n            .slots_in_use = 0,\n            .slots_free = 0,\n            .fragmentation_ratio = 0.0,\n            .utilization_ratio = 0.0,\n            .slabs = &.{},\n        };\n\n        var it = self.slabs.iterator();\n        while (it.next()) |entry| {\n            const key = entry.key_ptr.*;\n            const slab = entry.value_ptr;\n            const slab_stat = slab.getStats(key);\n\n            slab_stats.appendAssumeCapacity(slab_stat);\n\n            stats.total_allocated_bytes += slab_stat.bytes_allocated;\n            stats.bytes_in_use += slab_stat.bytes_in_use;\n            stats.bytes_free += slab_stat.bytes_free;\n            stats.total_chunks += slab_stat.chunk_count;\n            stats.total_slots += slab_stat.total_slots;\n            stats.slots_in_use += slab_stat.slots_in_use;\n            stats.slots_free += slab_stat.slots_free;\n        }\n\n        if (stats.total_allocated_bytes > 0) {\n            stats.fragmentation_ratio = @as(f64, @floatFromInt(stats.bytes_free)) /\n                @as(f64, @floatFromInt(stats.total_allocated_bytes));\n            stats.utilization_ratio = @as(f64, @floatFromInt(stats.bytes_in_use)) /\n                @as(f64, @floatFromInt(stats.total_allocated_bytes));\n        }\n\n        stats.slabs = try slab_stats.toOwnedSlice(a);\n        return stats;\n    }\n\n    pub const vtable = Allocator.VTable{\n        .alloc = alloc,\n        .free = free,\n        .remap = Allocator.noRemap,\n        .resize = Allocator.noResize,\n    };\n\n    pub fn allocator(self: *Self) Allocator {\n        return .{\n            .ptr = self,\n            .vtable = &vtable,\n        };\n    }\n\n    fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 {\n        const self: *Self = @ptrCast(@alignCast(ctx));\n        _ = ret_addr;\n\n        const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits());\n\n        const list_gop = self.slabs.getOrPut(\n            self.child_allocator,\n            SlabKey{ .size = aligned_len, .alignment = alignment },\n        ) catch return null;\n\n        if (!list_gop.found_existing) {\n            list_gop.value_ptr.* = Slab.init(\n                self.child_allocator,\n                alignment,\n                aligned_len,\n                self.max_slot_count,\n            ) catch return null;\n        }\n\n        const list = list_gop.value_ptr;\n        const buf = list.alloc(self.child_allocator) catch return null;\n        return buf[0..len].ptr;\n    }\n\n    fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void {\n        const self: *Self = @ptrCast(@alignCast(ctx));\n        _ = ret_addr;\n\n        const ptr = memory.ptr;\n        const len = memory.len;\n        const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits());\n        const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?;\n        list.free(ptr);\n    }\n};\n\nconst testing = std.testing;\n\nconst TestSlabAllocator = SlabAllocator;\n\ntest \"slab allocator - basic allocation and free\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate some memory\n    const ptr1 = try allocator.alloc(u8, 100);\n    try testing.expect(ptr1.len == 100);\n\n    // Write to it to ensure it's valid\n    @memset(ptr1, 42);\n    try testing.expectEqual(@as(u8, 42), ptr1[50]);\n\n    // Free it\n    allocator.free(ptr1);\n}\n\ntest \"slab allocator - multiple allocations\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    const ptr1 = try allocator.alloc(u8, 64);\n    const ptr2 = try allocator.alloc(u8, 128);\n    const ptr3 = try allocator.alloc(u8, 256);\n\n    // Ensure they don't overlap\n    const addr1 = @intFromPtr(ptr1.ptr);\n    const addr2 = @intFromPtr(ptr2.ptr);\n    const addr3 = @intFromPtr(ptr3.ptr);\n\n    try testing.expect(addr1 + 64 <= addr2 or addr2 + 128 <= addr1);\n    try testing.expect(addr2 + 128 <= addr3 or addr3 + 256 <= addr2);\n\n    allocator.free(ptr1);\n    allocator.free(ptr2);\n    allocator.free(ptr3);\n}\n\ntest \"slab allocator - no coalescing (different size classes)\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate two blocks of same size\n    const ptr1 = try allocator.alloc(u8, 128);\n    const ptr2 = try allocator.alloc(u8, 128);\n\n    // Free them (no coalescing in slab allocator)\n    allocator.free(ptr1);\n    allocator.free(ptr2);\n\n    // Can't allocate larger block from these freed 128-byte blocks\n    const ptr3 = try allocator.alloc(u8, 256);\n\n    // ptr3 will be from a different size class, not coalesced from ptr1+ptr2\n    const addr1 = @intFromPtr(ptr1.ptr);\n    const addr3 = @intFromPtr(ptr3.ptr);\n\n    // They should NOT be adjacent (different size classes)\n    try testing.expect(addr3 < addr1 or addr3 >= addr1 + 256);\n\n    allocator.free(ptr3);\n}\n\ntest \"slab allocator - reuse freed memory\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    const ptr1 = try allocator.alloc(u8, 64);\n    const addr1 = @intFromPtr(ptr1.ptr);\n    allocator.free(ptr1);\n\n    // Allocate same size, should reuse from same slab\n    const ptr2 = try allocator.alloc(u8, 64);\n    const addr2 = @intFromPtr(ptr2.ptr);\n\n    try testing.expectEqual(addr1, addr2);\n    allocator.free(ptr2);\n}\n\ntest \"slab allocator - multiple size classes\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate various sizes - each creates a new slab\n    var ptrs: [10][]u8 = undefined;\n    const sizes = [_]usize{ 24, 40, 64, 88, 128, 144, 200, 256, 512, 1000 };\n\n    for (&ptrs, sizes) |*ptr, size| {\n        ptr.* = try allocator.alloc(u8, size);\n        @memset(ptr.*, 0xFF);\n    }\n\n    // Should have created multiple slabs\n    try testing.expect(slab_alloc.slabs.count() >= 10);\n\n    // Free all\n    for (ptrs) |ptr| {\n        allocator.free(ptr);\n    }\n}\n\ntest \"slab allocator - various sizes\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Test different sizes (not limited to powers of 2!)\n    const sizes = [_]usize{ 8, 16, 24, 32, 40, 64, 88, 128, 144, 256 };\n\n    for (sizes) |size| {\n        const ptr = try allocator.alloc(u8, size);\n        try testing.expect(ptr.len == size);\n        @memset(ptr, @intCast(size & 0xFF));\n        allocator.free(ptr);\n    }\n}\n\ntest \"slab allocator - exact sizes (no rounding)\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Odd sizes stay exact (unlike buddy which rounds to power of 2)\n    const ptr1 = try allocator.alloc(u8, 100);\n    const ptr2 = try allocator.alloc(u8, 200);\n    const ptr3 = try allocator.alloc(u8, 50);\n\n    // Exact sizes!\n    try testing.expect(ptr1.len == 100);\n    try testing.expect(ptr2.len == 200);\n    try testing.expect(ptr3.len == 50);\n\n    allocator.free(ptr1);\n    allocator.free(ptr2);\n    allocator.free(ptr3);\n}\n\ntest \"slab allocator - chunk allocation\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate many items of same size to force multiple chunks\n    var ptrs: [100][]u8 = undefined;\n    for (&ptrs) |*ptr| {\n        ptr.* = try allocator.alloc(u8, 64);\n    }\n\n    // Should have allocated multiple chunks (32 items per chunk)\n    const slab = slab_alloc.slabs.getPtr(.{ .size = 64, .alignment = Alignment.@\"1\" }).?;\n    try testing.expect(slab.chunks.items.len > 1);\n\n    // Free all\n    for (ptrs) |ptr| {\n        allocator.free(ptr);\n    }\n}\n\ntest \"slab allocator - reset with retain_capacity\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate some memory\n    const ptr1 = try allocator.alloc(u8, 128);\n    const ptr2 = try allocator.alloc(u8, 256);\n    _ = ptr1;\n    _ = ptr2;\n\n    const slabs_before = slab_alloc.slabs.count();\n    const slab_128 = slab_alloc.slabs.getPtr(.{ .size = 128, .alignment = Alignment.@\"1\" }).?;\n    const chunks_before = slab_128.chunks.items.len;\n\n    // Reset but keep chunks\n    slab_alloc.reset(.retain_capacity);\n\n    try testing.expectEqual(slabs_before, slab_alloc.slabs.count());\n    try testing.expectEqual(chunks_before, slab_128.chunks.items.len);\n\n    // Should be able to allocate again\n    const ptr3 = try allocator.alloc(u8, 512);\n    allocator.free(ptr3);\n}\n\ntest \"slab allocator - reset with clear\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate some memory\n    const ptr1 = try allocator.alloc(u8, 128);\n    _ = ptr1;\n\n    try testing.expect(slab_alloc.slabs.count() > 0);\n\n    // Reset and free everything\n    slab_alloc.reset(.clear);\n\n    try testing.expectEqual(@as(usize, 0), slab_alloc.slabs.count());\n\n    // Should still work after reset\n    const ptr2 = try allocator.alloc(u8, 256);\n    allocator.free(ptr2);\n}\n\ntest \"slab allocator - stress test\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    var prng = std.Random.DefaultPrng.init(0);\n    const random = prng.random();\n\n    var ptrs: std.ArrayList([]u8) = .empty;\n\n    defer {\n        for (ptrs.items) |ptr| {\n            allocator.free(ptr);\n        }\n        ptrs.deinit(allocator);\n    }\n\n    // Random allocations and frees\n    var i: usize = 0;\n    while (i < 100) : (i += 1) {\n        if (random.boolean() and ptrs.items.len > 0) {\n            // Free a random allocation\n            const index = random.uintLessThan(usize, ptrs.items.len);\n            allocator.free(ptrs.swapRemove(index));\n        } else {\n            // Allocate random size (8 to 512)\n            const size = random.uintAtMost(usize, 504) + 8;\n            const ptr = try allocator.alloc(u8, size);\n            try ptrs.append(allocator, ptr);\n\n            // Write to ensure it's valid\n            @memset(ptr, @intCast(i & 0xFF));\n        }\n    }\n}\n\ntest \"slab allocator - alignment\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    const ptr1 = try allocator.create(u64);\n    const ptr2 = try allocator.create(u32);\n    const ptr3 = try allocator.create([100]u8);\n\n    allocator.destroy(ptr1);\n    allocator.destroy(ptr2);\n    allocator.destroy(ptr3);\n}\n\ntest \"slab allocator - no resize support\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    const slice = try allocator.alloc(u8, 100);\n    @memset(slice, 42);\n\n    // Resize should fail (not supported)\n    try testing.expect(!allocator.resize(slice, 90));\n    try testing.expect(!allocator.resize(slice, 200));\n\n    allocator.free(slice);\n}\n\ntest \"slab allocator - fragmentation pattern\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate 10 items\n    var items: [10][]u8 = undefined;\n    for (&items) |*item| {\n        item.* = try allocator.alloc(u8, 64);\n        @memset(item.*, 0xFF);\n    }\n\n    // Free every other one\n    allocator.free(items[0]);\n    allocator.free(items[2]);\n    allocator.free(items[4]);\n    allocator.free(items[6]);\n    allocator.free(items[8]);\n\n    // Allocate new items - should reuse freed slots\n    const new1 = try allocator.alloc(u8, 64);\n    const new2 = try allocator.alloc(u8, 64);\n    const new3 = try allocator.alloc(u8, 64);\n\n    // Should get some of the freed slots back\n    const addrs = [_]usize{\n        @intFromPtr(items[0].ptr),\n        @intFromPtr(items[2].ptr),\n        @intFromPtr(items[4].ptr),\n        @intFromPtr(items[6].ptr),\n        @intFromPtr(items[8].ptr),\n    };\n\n    const new1_addr = @intFromPtr(new1.ptr);\n    var found = false;\n    for (addrs) |addr| {\n        if (new1_addr == addr) found = true;\n    }\n    try testing.expect(found);\n\n    // Cleanup\n    allocator.free(items[1]);\n    allocator.free(items[3]);\n    allocator.free(items[5]);\n    allocator.free(items[7]);\n    allocator.free(items[9]);\n    allocator.free(new1);\n    allocator.free(new2);\n    allocator.free(new3);\n}\n\ntest \"slab allocator - many small allocations\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate 1000 small items\n    var ptrs: std.ArrayList([]u8) = .empty;\n    defer {\n        for (ptrs.items) |ptr| {\n            allocator.free(ptr);\n        }\n        ptrs.deinit(allocator);\n    }\n\n    var i: usize = 0;\n    while (i < 1000) : (i += 1) {\n        const ptr = try allocator.alloc(u8, 24);\n        try ptrs.append(allocator, ptr);\n    }\n\n    // Should have created multiple chunks\n    const slab = slab_alloc.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@\"1\" }).?;\n    try testing.expect(slab.chunks.items.len > 1);\n}\n\ntest \"slab allocator - zero waste for exact sizes\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // These sizes have zero internal fragmentation (unlike buddy)\n    const sizes = [_]usize{ 24, 40, 56, 88, 144, 152, 184, 232, 648 };\n\n    for (sizes) |size| {\n        const ptr = try allocator.alloc(u8, size);\n\n        // Exact size returned!\n        try testing.expectEqual(size, ptr.len);\n\n        @memset(ptr, 0xFF);\n        allocator.free(ptr);\n    }\n}\n\ntest \"slab allocator - different size classes don't interfere\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Allocate size 64\n    const ptr_64 = try allocator.alloc(u8, 64);\n    const addr_64 = @intFromPtr(ptr_64.ptr);\n    allocator.free(ptr_64);\n\n    // Allocate size 128 - should NOT reuse size-64 slot\n    const ptr_128 = try allocator.alloc(u8, 128);\n    const addr_128 = @intFromPtr(ptr_128.ptr);\n\n    try testing.expect(addr_64 != addr_128);\n\n    // Allocate size 64 again - SHOULD reuse original slot\n    const ptr_64_again = try allocator.alloc(u8, 64);\n    const addr_64_again = @intFromPtr(ptr_64_again.ptr);\n\n    try testing.expectEqual(addr_64, addr_64_again);\n\n    allocator.free(ptr_128);\n    allocator.free(ptr_64_again);\n}\n\ntest \"slab allocator - 16-byte alignment\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    // Request 16-byte aligned memory\n    const ptr = try allocator.alignedAlloc(u8, .@\"16\", 152);\n    defer allocator.free(ptr);\n\n    // Verify alignment\n    const addr = @intFromPtr(ptr.ptr);\n    try testing.expect(addr % 16 == 0);\n\n    // Make sure we can use it\n    @memset(ptr, 0xFF);\n}\n\ntest \"slab allocator - various alignments\" {\n    var slab_alloc = TestSlabAllocator.init(testing.allocator, 16);\n    defer slab_alloc.deinit();\n\n    const allocator = slab_alloc.allocator();\n\n    const alignments = [_]std.mem.Alignment{ .@\"1\", .@\"2\", .@\"4\", .@\"8\", .@\"16\" };\n\n    inline for (alignments) |alignment| {\n        const ptr = try allocator.alignedAlloc(u8, alignment, 100);\n        defer allocator.free(ptr);\n\n        const addr = @intFromPtr(ptr.ptr);\n        const align_value = alignment.toByteUnits();\n        try testing.expect(addr % align_value == 0);\n    }\n}\n"
  },
  {
    "path": "src/string.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst M = @This();\n\n// German-string (small string optimization)\npub const String = packed struct {\n    len: i32,\n    payload: packed union {\n        // Zig won't let you put an array in a packed struct/union. But it will\n        // let you put a vector.\n        content: @Vector(12, u8),\n        heap: packed struct { prefix: @Vector(4, u8), ptr: [*]const u8 },\n    },\n\n    const tombstone = -1;\n    pub const empty = String{ .len = 0, .payload = .{ .content = @splat(0) } };\n    pub const deleted = String{ .len = tombstone, .payload = .{ .content = @splat(0) } };\n\n    // for packages that already have String imported, then can use String.Global\n    pub const Global = M.Global;\n\n    // Wraps an existing string. For strings with len <= 12, this can be done at\n    // comptime: comptime String.wrap(\"id\");\n    // For strings with len > 12, this must be done at runtime even for a string\n    // literal. This is because, at comptime, we do not have a ptr for data and\n    // thus can't store it.\n    pub fn wrap(input: anytype) String {\n        if (@inComptime()) {\n            const l = input.len;\n            if (l > 12) {\n                @compileError(\"Comptime string must be <= 12 bytes (SSO only): \" ++ input);\n            }\n\n            var content: [12]u8 = @splat(0);\n            @memcpy(content[0..l], input);\n            return .{ .len = @intCast(l), .payload = .{ .content = content } };\n        }\n\n        // Runtime path - handle both String and []const u8\n        if (@TypeOf(input) == String) {\n            return input;\n        }\n\n        const l = input.len;\n\n        if (l <= 12) {\n            var content: [12]u8 = @splat(0);\n            @memcpy(content[0..l], input);\n            return .{ .len = @intCast(l), .payload = .{ .content = content } };\n        }\n\n        return .{\n            .len = @intCast(l),\n            .payload = .{ .heap = .{\n                .prefix = input[0..4].*,\n                .ptr = input.ptr,\n            } },\n        };\n    }\n\n    pub const InitOpts = struct {\n        dupe: bool = true,\n    };\n    pub fn init(allocator: Allocator, input: []const u8, opts: InitOpts) !String {\n        if (input.len >= std.math.maxInt(i32)) {\n            return error.StringTooLarge;\n        }\n        const l: u32 = @intCast(input.len);\n        if (l <= 12) {\n            var content: [12]u8 = @splat(0);\n            @memcpy(content[0..l], input);\n            return .{ .len = @intCast(l), .payload = .{ .content = content } };\n        }\n\n        return .{\n            .len = @intCast(l),\n            .payload = .{ .heap = .{\n                .prefix = input[0..4].*,\n                .ptr = (intern(input) orelse (if (opts.dupe) (try allocator.dupe(u8, input)) else input)).ptr,\n            } },\n        };\n    }\n\n    pub fn deinit(self: *const String, allocator: Allocator) void {\n        const len = self.len;\n        if (len > 12) {\n            allocator.free(self.payload.heap.ptr[0..@intCast(len)]);\n        }\n    }\n\n    pub fn dupe(self: *const String, allocator: Allocator) !String {\n        return .init(allocator, self.str(), .{ .dupe = true });\n    }\n\n    pub fn concat(allocator: Allocator, parts: []const []const u8) !String {\n        var total_len: usize = 0;\n        for (parts) |part| {\n            total_len += part.len;\n        }\n\n        if (total_len <= 12) {\n            var content: [12]u8 = @splat(0);\n            var pos: usize = 0;\n            for (parts) |part| {\n                @memcpy(content[pos..][0..part.len], part);\n                pos += part.len;\n            }\n            return .{ .len = @intCast(total_len), .payload = .{ .content = content } };\n        }\n\n        const result = try allocator.alloc(u8, total_len);\n        var pos: usize = 0;\n        for (parts) |part| {\n            @memcpy(result[pos..][0..part.len], part);\n            pos += part.len;\n        }\n\n        return .{\n            .len = @intCast(total_len),\n            .payload = .{ .heap = .{\n                .prefix = result[0..4].*,\n                .ptr = (intern(result) orelse result).ptr,\n            } },\n        };\n    }\n\n    pub fn str(self: *const String) []const u8 {\n        const l = self.len;\n        if (l < 0) {\n            return \"\";\n        }\n\n        const ul: usize = @intCast(l);\n\n        if (ul <= 12) {\n            const slice: []const u8 = @ptrCast(self);\n            return slice[4 .. ul + 4];\n        }\n\n        return self.payload.heap.ptr[0..ul];\n    }\n\n    pub fn isDeleted(self: *const String) bool {\n        return self.len == tombstone;\n    }\n\n    pub fn format(self: String, writer: *std.Io.Writer) !void {\n        return writer.writeAll(self.str());\n    }\n\n    pub fn eql(a: String, b: String) bool {\n        if (@as(*const u64, @ptrCast(&a)).* != @as(*const u64, @ptrCast(&b)).*) {\n            return false;\n        }\n\n        const len = a.len;\n        if (len < 0 or b.len < 0) {\n            return false;\n        }\n\n        if (len <= 12) {\n            return @reduce(.And, a.payload.content == b.payload.content);\n        }\n\n        // a.len == b.len at this point\n        const al: usize = @intCast(len);\n        const bl: usize = @intCast(len);\n        return std.mem.eql(u8, a.payload.heap.ptr[0..al], b.payload.heap.ptr[0..bl]);\n    }\n\n    pub fn eqlSlice(a: String, b: []const u8) bool {\n        return switch (a.eqlSliceOrDeleted(b)) {\n            .equal => |r| r,\n            .deleted => false,\n        };\n    }\n\n    const EqualOrDeleted = union(enum) {\n        deleted,\n        equal: bool,\n    };\n    pub fn eqlSliceOrDeleted(a: String, b: []const u8) EqualOrDeleted {\n        if (a.len == tombstone) {\n            return .deleted;\n        }\n        return .{ .equal = std.mem.eql(u8, a.str(), b) };\n    }\n\n    // This can be used outside of the small string optimization\n    pub fn intern(input: []const u8) ?[]const u8 {\n        switch (input.len) {\n            1 => switch (input[0]) {\n                '\\n' => return \"\\n\",\n                '\\r' => return \"\\r\",\n                '\\t' => return \"\\t\",\n                ' ' => return \" \",\n                '0' => return \"0\",\n                '1' => return \"1\",\n                '2' => return \"2\",\n                '3' => return \"3\",\n                '4' => return \"4\",\n                '5' => return \"5\",\n                '6' => return \"6\",\n                '7' => return \"7\",\n                '8' => return \"8\",\n                '9' => return \"9\",\n                '.' => return \".\",\n                ',' => return \",\",\n                '-' => return \"-\",\n                '(' => return \"(\",\n                ')' => return \")\",\n                '?' => return \"?\",\n                ';' => return \";\",\n                '=' => return \"=\",\n                else => {},\n            },\n            2 => switch (@as(u16, @bitCast(input[0..2].*))) {\n                asUint(\"id\") => return \"id\",\n                asUint(\"  \") => return \"  \",\n                asUint(\"\\r\\n\") => return \"\\r\\n\",\n                asUint(\", \") => return \", \",\n                asUint(\"·\") => return \"·\",\n                else => {},\n            },\n            3 => switch (@as(u24, @bitCast(input[0..3].*))) {\n                asUint(\"   \") => return \"   \",\n                asUint(\"•\") => return \"•\",\n                else => {},\n            },\n            4 => switch (@as(u32, @bitCast(input[0..4].*))) {\n                asUint(\"    \") => return \"    \",\n                asUint(\" to \") => return \" to \",\n                else => {},\n            },\n            5 => switch (@as(u40, @bitCast(input[0..5].*))) {\n                asUint(\"     \") => return \"     \",\n                asUint(\" › \") => return \" › \",\n                else => {},\n            },\n            6 => switch (@as(u48, @bitCast(input[0..6].*))) {\n                asUint(\"      \") => return \"      \",\n                else => {},\n            },\n            7 => switch (@as(u56, @bitCast(input[0..7].*))) {\n                asUint(\"       \") => return \"       \",\n                else => {},\n            },\n            8 => switch (@as(u64, @bitCast(input[0..8].*))) {\n                asUint(\"        \") => return \"        \",\n                else => {},\n            },\n            9 => switch (@as(u72, @bitCast(input[0..9].*))) {\n                asUint(\"         \") => return \"         \",\n                else => {},\n            },\n            10 => switch (@as(u80, @bitCast(input[0..10].*))) {\n                asUint(\"          \") => return \"          \",\n                else => {},\n            },\n            13 => switch (@as(u104, @bitCast(input[0..13].*))) {\n                asUint(\"border-radius\") => return \"border-radius\",\n                asUint(\"padding-right\") => return \"padding-right\",\n                asUint(\"margin-bottom\") => return \"margin-bottom\",\n                asUint(\"space-between\") => return \"space-between\",\n                else => {},\n            },\n            14 => switch (@as(u112, @bitCast(input[0..14].*))) {\n                asUint(\"padding-bottom\") => return \"padding-bottom\",\n                asUint(\"text-transform\") => return \"text-transform\",\n                asUint(\"letter-spacing\") => return \"letter-spacing\",\n                asUint(\"vertical-align\") => return \"vertical-align\",\n                else => {},\n            },\n            15 => switch (@as(u120, @bitCast(input[0..15].*))) {\n                asUint(\"text-decoration\") => return \"text-decoration\",\n                asUint(\"justify-content\") => return \"justify-content\",\n                else => {},\n            },\n            16 => switch (@as(u128, @bitCast(input[0..16].*))) {\n                asUint(\"background-color\") => return \"background-color\",\n                else => {},\n            },\n            else => {},\n        }\n        return null;\n    }\n};\n\npub fn isAllWhitespace(text: []const u8) bool {\n    return for (text) |c| {\n        if (!std.ascii.isWhitespace(c)) break false;\n    } else true;\n}\n\n// Discriminatory type that signals the bridge to use arena instead of call_arena\n// Use this for strings that need to persist beyond the current call\n// The caller can unwrap and store just the underlying .str field\npub const Global = struct {\n    str: String,\n};\n\nfn asUint(comptime string: anytype) std.meta.Int(\n    .unsigned,\n    @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0\n) {\n    const byteLength = @sizeOf(@TypeOf(string.*)) - 1;\n    const expectedType = *const [byteLength:0]u8;\n    if (@TypeOf(string) != expectedType) {\n        @compileError(\"expected : \" ++ @typeName(expectedType) ++ \", got: \" ++ @typeName(@TypeOf(string)));\n    }\n\n    return @bitCast(@as(*const [byteLength]u8, string).*);\n}\n\nconst testing = @import(\"testing.zig\");\ntest \"String\" {\n    const other_short = try String.init(undefined, \"other_short\", .{});\n    const other_long = try String.init(testing.allocator, \"other_long\" ** 100, .{});\n    defer other_long.deinit(testing.allocator);\n\n    inline for (0..100) |i| {\n        const input = \"a\" ** i;\n        const str = try String.init(testing.allocator, input, .{});\n        defer str.deinit(testing.allocator);\n\n        try testing.expectEqual(input, str.str());\n\n        try testing.expectEqual(true, str.eql(str));\n        try testing.expectEqual(true, str.eqlSlice(input));\n        try testing.expectEqual(false, str.eql(other_short));\n        try testing.expectEqual(false, str.eqlSlice(\"other_short\"));\n\n        try testing.expectEqual(false, str.eql(other_long));\n        try testing.expectEqual(false, str.eqlSlice(\"other_long\" ** 100));\n    }\n}\n\ntest \"String.concat\" {\n    {\n        const result = try String.concat(testing.allocator, &.{});\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(@as(usize, 0), result.str().len);\n        try testing.expectEqual(\"\", result.str());\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{\"hello\"});\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"hello\", result.str());\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"foo\", \"bar\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"foobar\", result.str());\n        try testing.expectEqual(@as(i32, 6), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"test\", \"ing\", \"1234\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"testing1234\", result.str());\n        try testing.expectEqual(@as(i32, 11), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"foo\", \"bar\", \"baz\", \"qux\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"foobarbazqux\", result.str());\n        try testing.expectEqual(@as(i32, 12), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"hello\", \" world!\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"hello world!\", result.str());\n        try testing.expectEqual(@as(i32, 12), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"a\", \"b\", \"c\", \"d\", \"e\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"abcde\", result.str());\n        try testing.expectEqual(@as(i32, 5), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"one\", \" \", \"two\", \" \", \"three\", \" \", \"four\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"one two three four\", result.str());\n        try testing.expectEqual(@as(i32, 18), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"hello\", \"\", \"world\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"helloworld\", result.str());\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"\", \"\", \"\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"\", result.str());\n        try testing.expectEqual(@as(i32, 0), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"café\", \" ☕\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"café ☕\", result.str());\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"Hello \", \"世界\", \" and \", \"مرحبا\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"Hello 世界 and مرحبا\", result.str());\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \" \", \"test\", \" \" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\" test \", result.str());\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"  \", \"  \" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"    \", result.str());\n        try testing.expectEqual(@as(i32, 4), result.len);\n    }\n\n    {\n        const result = try String.concat(testing.allocator, &.{ \"Item \", \"1\", \"2\", \"3\" });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"Item 123\", result.str());\n    }\n\n    {\n        const original = \"Hello, world!\";\n        const result = try String.concat(testing.allocator, &.{ original[0..5], original[7..] });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"Helloworld!\", result.str());\n    }\n\n    {\n        const original = \"Hello!\";\n        const result = try String.concat(testing.allocator, &.{ original[0..5], \" world\", original[5..] });\n        defer result.deinit(testing.allocator);\n        try testing.expectEqual(\"Hello world!\", result.str());\n    }\n}\n"
  },
  {
    "path": "src/sys/libcurl.zig",
    "content": "// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst c = @cImport({\n    @cInclude(\"curl/curl.h\");\n});\n\nconst IS_DEBUG = builtin.mode == .Debug;\n\npub const Curl = c.CURL;\npub const CurlM = c.CURLM;\npub const CurlCode = c.CURLcode;\npub const CurlMCode = c.CURLMcode;\npub const CurlSList = c.curl_slist;\npub const CurlHeader = c.curl_header;\npub const CurlHttpPost = c.curl_httppost;\npub const CurlSocket = c.curl_socket_t;\npub const CurlBlob = c.curl_blob;\npub const CurlOffT = c.curl_off_t;\n\npub const CurlDebugFunction = fn (*Curl, CurlInfoType, [*c]u8, usize, *anyopaque) c_int;\npub const CurlHeaderFunction = fn ([*]const u8, usize, usize, *anyopaque) usize;\npub const CurlWriteFunction = fn ([*]const u8, usize, usize, *anyopaque) usize;\npub const curl_writefunc_error: usize = c.CURL_WRITEFUNC_ERROR;\n\npub const FreeCallback = fn (ptr: ?*anyopaque) void;\npub const StrdupCallback = fn (str: [*:0]const u8) ?[*:0]u8;\npub const MallocCallback = fn (size: usize) ?*anyopaque;\npub const CallocCallback = fn (nmemb: usize, size: usize) ?*anyopaque;\npub const ReallocCallback = fn (ptr: ?*anyopaque, size: usize) ?*anyopaque;\n\npub const CurlAllocator = struct {\n    free: FreeCallback,\n    strdup: StrdupCallback,\n    malloc: MallocCallback,\n    calloc: CallocCallback,\n    realloc: ReallocCallback,\n};\n\npub const CurlGlobalFlags = packed struct(u8) {\n    ssl: bool = false,\n    _reserved: u7 = 0,\n\n    pub fn to_c(self: @This()) c_long {\n        var flags: c_long = 0;\n        if (self.ssl) flags |= c.CURL_GLOBAL_SSL;\n        return flags;\n    }\n};\n\npub const CurlHeaderOrigin = enum(c_uint) {\n    header = c.CURLH_HEADER,\n    trailer = c.CURLH_TRAILER,\n    connect = c.CURLH_CONNECT,\n    @\"1xx\" = c.CURLH_1XX,\n    pseudo = c.CURLH_PSEUDO,\n};\n\npub const CurlWaitEvents = packed struct(c_short) {\n    pollin: bool = false,\n    pollpri: bool = false,\n    pollout: bool = false,\n    _reserved: u13 = 0,\n};\n\npub const CurlInfoType = enum(c.curl_infotype) {\n    text = c.CURLINFO_TEXT,\n    header_in = c.CURLINFO_HEADER_IN,\n    header_out = c.CURLINFO_HEADER_OUT,\n    data_in = c.CURLINFO_DATA_IN,\n    data_out = c.CURLINFO_DATA_OUT,\n    ssl_data_in = c.CURLINFO_SSL_DATA_IN,\n    ssl_data_out = c.CURLINFO_SSL_DATA_OUT,\n    end = c.CURLINFO_END,\n};\n\npub const CurlWaitFd = extern struct {\n    fd: CurlSocket,\n    events: CurlWaitEvents,\n    revents: CurlWaitEvents,\n};\n\ncomptime {\n    const debug_cb_check: c.curl_debug_callback = struct {\n        fn cb(handle: ?*Curl, msg_type: c.curl_infotype, raw: [*c]u8, len: usize, user: ?*anyopaque) callconv(.c) c_int {\n            _ = handle;\n            _ = msg_type;\n            _ = raw;\n            _ = len;\n            _ = user;\n            return 0;\n        }\n    }.cb;\n    const write_cb_check: c.curl_write_callback = struct {\n        fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {\n            _ = buffer;\n            _ = count;\n            _ = len;\n            _ = user;\n            return 0;\n        }\n    }.cb;\n    _ = debug_cb_check;\n    _ = write_cb_check;\n\n    if (@sizeOf(CurlWaitFd) != @sizeOf(c.curl_waitfd)) {\n        @compileError(\"CurlWaitFd size mismatch\");\n    }\n    if (@offsetOf(CurlWaitFd, \"fd\") != @offsetOf(c.curl_waitfd, \"fd\") or\n        @offsetOf(CurlWaitFd, \"events\") != @offsetOf(c.curl_waitfd, \"events\") or\n        @offsetOf(CurlWaitFd, \"revents\") != @offsetOf(c.curl_waitfd, \"revents\"))\n    {\n        @compileError(\"CurlWaitFd layout mismatch\");\n    }\n    if (c.CURL_WAIT_POLLIN != 1 or c.CURL_WAIT_POLLPRI != 2 or c.CURL_WAIT_POLLOUT != 4) {\n        @compileError(\"CURL_WAIT_* flag values don't match CurlWaitEvents packed struct bit layout\");\n    }\n}\n\npub const CurlOption = enum(c.CURLoption) {\n    url = c.CURLOPT_URL,\n    timeout_ms = c.CURLOPT_TIMEOUT_MS,\n    connect_timeout_ms = c.CURLOPT_CONNECTTIMEOUT_MS,\n    max_redirs = c.CURLOPT_MAXREDIRS,\n    follow_location = c.CURLOPT_FOLLOWLOCATION,\n    redir_protocols_str = c.CURLOPT_REDIR_PROTOCOLS_STR,\n    proxy = c.CURLOPT_PROXY,\n    ca_info_blob = c.CURLOPT_CAINFO_BLOB,\n    proxy_ca_info_blob = c.CURLOPT_PROXY_CAINFO_BLOB,\n    ssl_verify_host = c.CURLOPT_SSL_VERIFYHOST,\n    ssl_verify_peer = c.CURLOPT_SSL_VERIFYPEER,\n    proxy_ssl_verify_host = c.CURLOPT_PROXY_SSL_VERIFYHOST,\n    proxy_ssl_verify_peer = c.CURLOPT_PROXY_SSL_VERIFYPEER,\n    accept_encoding = c.CURLOPT_ACCEPT_ENCODING,\n    verbose = c.CURLOPT_VERBOSE,\n    debug_function = c.CURLOPT_DEBUGFUNCTION,\n    custom_request = c.CURLOPT_CUSTOMREQUEST,\n    post = c.CURLOPT_POST,\n    http_post = c.CURLOPT_HTTPPOST,\n    post_field_size = c.CURLOPT_POSTFIELDSIZE,\n    copy_post_fields = c.CURLOPT_COPYPOSTFIELDS,\n    http_get = c.CURLOPT_HTTPGET,\n    http_header = c.CURLOPT_HTTPHEADER,\n    cookie = c.CURLOPT_COOKIE,\n    private = c.CURLOPT_PRIVATE,\n    proxy_user_pwd = c.CURLOPT_PROXYUSERPWD,\n    user_pwd = c.CURLOPT_USERPWD,\n    header_data = c.CURLOPT_HEADERDATA,\n    header_function = c.CURLOPT_HEADERFUNCTION,\n    write_data = c.CURLOPT_WRITEDATA,\n    write_function = c.CURLOPT_WRITEFUNCTION,\n};\n\npub const CurlMOption = enum(c.CURLMoption) {\n    max_host_connections = c.CURLMOPT_MAX_HOST_CONNECTIONS,\n};\n\npub const CurlInfo = enum(c.CURLINFO) {\n    effective_url = c.CURLINFO_EFFECTIVE_URL,\n    private = c.CURLINFO_PRIVATE,\n    redirect_count = c.CURLINFO_REDIRECT_COUNT,\n    response_code = c.CURLINFO_RESPONSE_CODE,\n};\n\npub const Error = error{\n    UnsupportedProtocol,\n    FailedInit,\n    UrlMalformat,\n    NotBuiltIn,\n    CouldntResolveProxy,\n    CouldntResolveHost,\n    CouldntConnect,\n    WeirdServerReply,\n    RemoteAccessDenied,\n    FtpAcceptFailed,\n    FtpWeirdPassReply,\n    FtpAcceptTimeout,\n    FtpWeirdPasvReply,\n    FtpWeird227Format,\n    FtpCantGetHost,\n    Http2,\n    FtpCouldntSetType,\n    PartialFile,\n    FtpCouldntRetrFile,\n    QuoteError,\n    HttpReturnedError,\n    WriteError,\n    UploadFailed,\n    ReadError,\n    OutOfMemory,\n    OperationTimedout,\n    FtpPortFailed,\n    FtpCouldntUseRest,\n    RangeError,\n    SslConnectError,\n    BadDownloadResume,\n    FileCouldntReadFile,\n    LdapCannotBind,\n    LdapSearchFailed,\n    AbortedByCallback,\n    BadFunctionArgument,\n    InterfaceFailed,\n    TooManyRedirects,\n    UnknownOption,\n    SetoptOptionSyntax,\n    GotNothing,\n    SslEngineNotfound,\n    SslEngineSetfailed,\n    SendError,\n    RecvError,\n    SslCertproblem,\n    SslCipher,\n    PeerFailedVerification,\n    BadContentEncoding,\n    FilesizeExceeded,\n    UseSslFailed,\n    SendFailRewind,\n    SslEngineInitfailed,\n    LoginDenied,\n    TftpNotfound,\n    TftpPerm,\n    RemoteDiskFull,\n    TftpIllegal,\n    TftpUnknownid,\n    RemoteFileExists,\n    TftpNosuchuser,\n    SslCacertBadfile,\n    RemoteFileNotFound,\n    Ssh,\n    SslShutdownFailed,\n    Again,\n    SslCrlBadfile,\n    SslIssuerError,\n    FtpPretFailed,\n    RtspCseqError,\n    RtspSessionError,\n    FtpBadFileList,\n    ChunkFailed,\n    NoConnectionAvailable,\n    SslPinnedpubkeynotmatch,\n    SslInvalidcertstatus,\n    Http2Stream,\n    RecursiveApiCall,\n    AuthError,\n    Http3,\n    QuicConnectError,\n    Proxy,\n    SslClientcert,\n    UnrecoverablePoll,\n    TooLarge,\n    Unknown,\n};\n\npub fn errorFromCode(code: c.CURLcode) Error {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(code != c.CURLE_OK);\n    }\n\n    return switch (code) {\n        c.CURLE_UNSUPPORTED_PROTOCOL => Error.UnsupportedProtocol,\n        c.CURLE_FAILED_INIT => Error.FailedInit,\n        c.CURLE_URL_MALFORMAT => Error.UrlMalformat,\n        c.CURLE_NOT_BUILT_IN => Error.NotBuiltIn,\n        c.CURLE_COULDNT_RESOLVE_PROXY => Error.CouldntResolveProxy,\n        c.CURLE_COULDNT_RESOLVE_HOST => Error.CouldntResolveHost,\n        c.CURLE_COULDNT_CONNECT => Error.CouldntConnect,\n        c.CURLE_WEIRD_SERVER_REPLY => Error.WeirdServerReply,\n        c.CURLE_REMOTE_ACCESS_DENIED => Error.RemoteAccessDenied,\n        c.CURLE_FTP_ACCEPT_FAILED => Error.FtpAcceptFailed,\n        c.CURLE_FTP_WEIRD_PASS_REPLY => Error.FtpWeirdPassReply,\n        c.CURLE_FTP_ACCEPT_TIMEOUT => Error.FtpAcceptTimeout,\n        c.CURLE_FTP_WEIRD_PASV_REPLY => Error.FtpWeirdPasvReply,\n        c.CURLE_FTP_WEIRD_227_FORMAT => Error.FtpWeird227Format,\n        c.CURLE_FTP_CANT_GET_HOST => Error.FtpCantGetHost,\n        c.CURLE_HTTP2 => Error.Http2,\n        c.CURLE_FTP_COULDNT_SET_TYPE => Error.FtpCouldntSetType,\n        c.CURLE_PARTIAL_FILE => Error.PartialFile,\n        c.CURLE_FTP_COULDNT_RETR_FILE => Error.FtpCouldntRetrFile,\n        c.CURLE_QUOTE_ERROR => Error.QuoteError,\n        c.CURLE_HTTP_RETURNED_ERROR => Error.HttpReturnedError,\n        c.CURLE_WRITE_ERROR => Error.WriteError,\n        c.CURLE_UPLOAD_FAILED => Error.UploadFailed,\n        c.CURLE_READ_ERROR => Error.ReadError,\n        c.CURLE_OUT_OF_MEMORY => Error.OutOfMemory,\n        c.CURLE_OPERATION_TIMEDOUT => Error.OperationTimedout,\n        c.CURLE_FTP_PORT_FAILED => Error.FtpPortFailed,\n        c.CURLE_FTP_COULDNT_USE_REST => Error.FtpCouldntUseRest,\n        c.CURLE_RANGE_ERROR => Error.RangeError,\n        c.CURLE_SSL_CONNECT_ERROR => Error.SslConnectError,\n        c.CURLE_BAD_DOWNLOAD_RESUME => Error.BadDownloadResume,\n        c.CURLE_FILE_COULDNT_READ_FILE => Error.FileCouldntReadFile,\n        c.CURLE_LDAP_CANNOT_BIND => Error.LdapCannotBind,\n        c.CURLE_LDAP_SEARCH_FAILED => Error.LdapSearchFailed,\n        c.CURLE_ABORTED_BY_CALLBACK => Error.AbortedByCallback,\n        c.CURLE_BAD_FUNCTION_ARGUMENT => Error.BadFunctionArgument,\n        c.CURLE_INTERFACE_FAILED => Error.InterfaceFailed,\n        c.CURLE_TOO_MANY_REDIRECTS => Error.TooManyRedirects,\n        c.CURLE_UNKNOWN_OPTION => Error.UnknownOption,\n        c.CURLE_SETOPT_OPTION_SYNTAX => Error.SetoptOptionSyntax,\n        c.CURLE_GOT_NOTHING => Error.GotNothing,\n        c.CURLE_SSL_ENGINE_NOTFOUND => Error.SslEngineNotfound,\n        c.CURLE_SSL_ENGINE_SETFAILED => Error.SslEngineSetfailed,\n        c.CURLE_SEND_ERROR => Error.SendError,\n        c.CURLE_RECV_ERROR => Error.RecvError,\n        c.CURLE_SSL_CERTPROBLEM => Error.SslCertproblem,\n        c.CURLE_SSL_CIPHER => Error.SslCipher,\n        c.CURLE_PEER_FAILED_VERIFICATION => Error.PeerFailedVerification,\n        c.CURLE_BAD_CONTENT_ENCODING => Error.BadContentEncoding,\n        c.CURLE_FILESIZE_EXCEEDED => Error.FilesizeExceeded,\n        c.CURLE_USE_SSL_FAILED => Error.UseSslFailed,\n        c.CURLE_SEND_FAIL_REWIND => Error.SendFailRewind,\n        c.CURLE_SSL_ENGINE_INITFAILED => Error.SslEngineInitfailed,\n        c.CURLE_LOGIN_DENIED => Error.LoginDenied,\n        c.CURLE_TFTP_NOTFOUND => Error.TftpNotfound,\n        c.CURLE_TFTP_PERM => Error.TftpPerm,\n        c.CURLE_REMOTE_DISK_FULL => Error.RemoteDiskFull,\n        c.CURLE_TFTP_ILLEGAL => Error.TftpIllegal,\n        c.CURLE_TFTP_UNKNOWNID => Error.TftpUnknownid,\n        c.CURLE_REMOTE_FILE_EXISTS => Error.RemoteFileExists,\n        c.CURLE_TFTP_NOSUCHUSER => Error.TftpNosuchuser,\n        c.CURLE_SSL_CACERT_BADFILE => Error.SslCacertBadfile,\n        c.CURLE_REMOTE_FILE_NOT_FOUND => Error.RemoteFileNotFound,\n        c.CURLE_SSH => Error.Ssh,\n        c.CURLE_SSL_SHUTDOWN_FAILED => Error.SslShutdownFailed,\n        c.CURLE_AGAIN => Error.Again,\n        c.CURLE_SSL_CRL_BADFILE => Error.SslCrlBadfile,\n        c.CURLE_SSL_ISSUER_ERROR => Error.SslIssuerError,\n        c.CURLE_FTP_PRET_FAILED => Error.FtpPretFailed,\n        c.CURLE_RTSP_CSEQ_ERROR => Error.RtspCseqError,\n        c.CURLE_RTSP_SESSION_ERROR => Error.RtspSessionError,\n        c.CURLE_FTP_BAD_FILE_LIST => Error.FtpBadFileList,\n        c.CURLE_CHUNK_FAILED => Error.ChunkFailed,\n        c.CURLE_NO_CONNECTION_AVAILABLE => Error.NoConnectionAvailable,\n        c.CURLE_SSL_PINNEDPUBKEYNOTMATCH => Error.SslPinnedpubkeynotmatch,\n        c.CURLE_SSL_INVALIDCERTSTATUS => Error.SslInvalidcertstatus,\n        c.CURLE_HTTP2_STREAM => Error.Http2Stream,\n        c.CURLE_RECURSIVE_API_CALL => Error.RecursiveApiCall,\n        c.CURLE_AUTH_ERROR => Error.AuthError,\n        c.CURLE_HTTP3 => Error.Http3,\n        c.CURLE_QUIC_CONNECT_ERROR => Error.QuicConnectError,\n        c.CURLE_PROXY => Error.Proxy,\n        c.CURLE_SSL_CLIENTCERT => Error.SslClientcert,\n        c.CURLE_UNRECOVERABLE_POLL => Error.UnrecoverablePoll,\n        c.CURLE_TOO_LARGE => Error.TooLarge,\n        else => Error.Unknown,\n    };\n}\n\npub const ErrorMulti = error{\n    BadHandle,\n    BadEasyHandle,\n    OutOfMemory,\n    InternalError,\n    BadSocket,\n    UnknownOption,\n    AddedAlready,\n    RecursiveApiCall,\n    WakeupFailure,\n    BadFunctionArgument,\n    AbortedByCallback,\n    UnrecoverablePoll,\n    Unknown,\n};\n\npub const ErrorHeader = error{\n    OutOfMemory,\n    BadArgument,\n    NotBuiltIn,\n    Unknown,\n};\n\npub fn errorMFromCode(code: c.CURLMcode) ErrorMulti {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(code != c.CURLM_OK);\n    }\n\n    return switch (code) {\n        c.CURLM_BAD_HANDLE => ErrorMulti.BadHandle,\n        c.CURLM_BAD_EASY_HANDLE => ErrorMulti.BadEasyHandle,\n        c.CURLM_OUT_OF_MEMORY => ErrorMulti.OutOfMemory,\n        c.CURLM_INTERNAL_ERROR => ErrorMulti.InternalError,\n        c.CURLM_BAD_SOCKET => ErrorMulti.BadSocket,\n        c.CURLM_UNKNOWN_OPTION => ErrorMulti.UnknownOption,\n        c.CURLM_ADDED_ALREADY => ErrorMulti.AddedAlready,\n        c.CURLM_RECURSIVE_API_CALL => ErrorMulti.RecursiveApiCall,\n        c.CURLM_WAKEUP_FAILURE => ErrorMulti.WakeupFailure,\n        c.CURLM_BAD_FUNCTION_ARGUMENT => ErrorMulti.BadFunctionArgument,\n        c.CURLM_ABORTED_BY_CALLBACK => ErrorMulti.AbortedByCallback,\n        c.CURLM_UNRECOVERABLE_POLL => ErrorMulti.UnrecoverablePoll,\n        else => ErrorMulti.Unknown,\n    };\n}\n\npub fn errorHFromCode(code: c.CURLHcode) ErrorHeader {\n    if (comptime IS_DEBUG) {\n        std.debug.assert(code != c.CURLHE_OK);\n    }\n\n    return switch (code) {\n        c.CURLHE_OUT_OF_MEMORY => ErrorHeader.OutOfMemory,\n        c.CURLHE_BAD_ARGUMENT => ErrorHeader.BadArgument,\n        c.CURLHE_NOT_BUILT_IN => ErrorHeader.NotBuiltIn,\n        else => ErrorHeader.Unknown,\n    };\n}\n\npub fn errorCheck(code: c.CURLcode) Error!void {\n    if (code == c.CURLE_OK) {\n        return;\n    }\n    return errorFromCode(code);\n}\n\npub fn errorMCheck(code: c.CURLMcode) ErrorMulti!void {\n    if (code == c.CURLM_OK) {\n        return;\n    }\n    if (code == c.CURLM_CALL_MULTI_PERFORM) {\n        return;\n    }\n    return errorMFromCode(code);\n}\n\npub fn errorHCheck(code: c.CURLHcode) ErrorHeader!void {\n    if (code == c.CURLHE_OK) {\n        return;\n    }\n    return errorHFromCode(code);\n}\n\npub const CurlMsgType = enum(c.CURLMSG) {\n    none = c.CURLMSG_NONE,\n    done = c.CURLMSG_DONE,\n    last = c.CURLMSG_LAST,\n};\n\npub const CurlMsgData = union(CurlMsgType) {\n    none: ?*anyopaque,\n    done: ?Error,\n    last: ?*anyopaque,\n};\n\npub const CurlMsg = struct {\n    easy_handle: *Curl,\n    data: CurlMsgData,\n};\n\npub fn curl_global_init(flags: CurlGlobalFlags, comptime curl_allocator: ?CurlAllocator) Error!void {\n    const alloc = curl_allocator orelse {\n        return errorCheck(c.curl_global_init(flags.to_c()));\n    };\n\n    // The purpose of these wrappers is to hide callconv\n    // and provide an easy place to add logging when debugging.\n    const free = struct {\n        fn cb(ptr: ?*anyopaque) callconv(.c) void {\n            alloc.free(ptr);\n        }\n    }.cb;\n    const strdup = struct {\n        fn cb(str: [*c]const u8) callconv(.c) [*c]u8 {\n            const s: [*:0]const u8 = @ptrCast(str orelse return null);\n            return @ptrCast(alloc.strdup(s));\n        }\n    }.cb;\n    const malloc = struct {\n        fn cb(size: usize) callconv(.c) ?*anyopaque {\n            return alloc.malloc(size);\n        }\n    }.cb;\n    const calloc = struct {\n        fn cb(nmemb: usize, size: usize) callconv(.c) ?*anyopaque {\n            return alloc.calloc(nmemb, size);\n        }\n    }.cb;\n    const realloc = struct {\n        fn cb(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque {\n            return alloc.realloc(ptr, size);\n        }\n    }.cb;\n\n    try errorCheck(c.curl_global_init_mem(flags.to_c(), malloc, free, realloc, strdup, calloc));\n}\n\npub fn curl_global_cleanup() void {\n    c.curl_global_cleanup();\n}\n\npub fn curl_version() [*c]const u8 {\n    return c.curl_version();\n}\n\npub fn curl_easy_init() ?*Curl {\n    return c.curl_easy_init();\n}\n\npub fn curl_easy_cleanup(easy: *Curl) void {\n    c.curl_easy_cleanup(easy);\n}\n\npub fn curl_easy_perform(easy: *Curl) Error!void {\n    try errorCheck(c.curl_easy_perform(easy));\n}\n\npub fn curl_easy_setopt(easy: *Curl, comptime option: CurlOption, value: anytype) Error!void {\n    const opt: c.CURLoption = @intFromEnum(option);\n    const code = switch (option) {\n        .verbose,\n        .post,\n        .http_get,\n        .ssl_verify_host,\n        .ssl_verify_peer,\n        .proxy_ssl_verify_host,\n        .proxy_ssl_verify_peer,\n        => blk: {\n            const n: c_long = switch (@typeInfo(@TypeOf(value))) {\n                .bool => switch (option) {\n                    .ssl_verify_host, .proxy_ssl_verify_host => if (value) 2 else 0,\n                    else => if (value) 1 else 0,\n                },\n                else => @compileError(\"expected bool|integer for \" ++ @tagName(option) ++ \", got \" ++ @typeName(@TypeOf(value))),\n            };\n            break :blk c.curl_easy_setopt(easy, opt, n);\n        },\n\n        .timeout_ms,\n        .connect_timeout_ms,\n        .max_redirs,\n        .follow_location,\n        .post_field_size,\n        => blk: {\n            const n: c_long = switch (@typeInfo(@TypeOf(value))) {\n                .comptime_int, .int => @intCast(value),\n                else => @compileError(\"expected integer for \" ++ @tagName(option) ++ \", got \" ++ @typeName(@TypeOf(value))),\n            };\n            break :blk c.curl_easy_setopt(easy, opt, n);\n        },\n\n        .url,\n        .redir_protocols_str,\n        .proxy,\n        .accept_encoding,\n        .custom_request,\n        .cookie,\n        .user_pwd,\n        .proxy_user_pwd,\n        .copy_post_fields,\n        => blk: {\n            const s: ?[*]const u8 = value;\n            break :blk c.curl_easy_setopt(easy, opt, s);\n        },\n\n        .ca_info_blob,\n        .proxy_ca_info_blob,\n        => blk: {\n            const blob: CurlBlob = value;\n            break :blk c.curl_easy_setopt(easy, opt, blob);\n        },\n\n        .http_post => blk: {\n            // CURLOPT_HTTPPOST expects ?*curl_httppost (multipart formdata)\n            const ptr: ?*CurlHttpPost = value;\n            break :blk c.curl_easy_setopt(easy, opt, ptr);\n        },\n\n        .http_header => blk: {\n            const list: ?*CurlSList = value;\n            break :blk c.curl_easy_setopt(easy, opt, list);\n        },\n\n        .private,\n        .header_data,\n        .write_data,\n        => blk: {\n            const ptr: ?*anyopaque = switch (@typeInfo(@TypeOf(value))) {\n                .null => null,\n                else => @ptrCast(value),\n            };\n            break :blk c.curl_easy_setopt(easy, opt, ptr);\n        },\n\n        .debug_function => blk: {\n            const cb: c.curl_debug_callback = switch (@typeInfo(@TypeOf(value))) {\n                .null => null,\n                .@\"fn\" => struct {\n                    fn cb(handle: ?*Curl, msg_type: c.curl_infotype, raw: [*c]u8, len: usize, user: ?*anyopaque) callconv(.c) c_int {\n                        const h = handle orelse unreachable;\n                        const u = user orelse unreachable;\n                        return value(h, @enumFromInt(@intFromEnum(msg_type)), raw, len, u);\n                    }\n                }.cb,\n                else => @compileError(\"expected Zig function or null for \" ++ @tagName(option) ++ \", got \" ++ @typeName(@TypeOf(value))),\n            };\n            break :blk c.curl_easy_setopt(easy, opt, cb);\n        },\n\n        .header_function => blk: {\n            const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {\n                .null => null,\n                .@\"fn\" => struct {\n                    fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {\n                        const u = user orelse unreachable;\n                        return value(@ptrCast(buffer), count, len, u);\n                    }\n                }.cb,\n                else => @compileError(\"expected Zig function or null for \" ++ @tagName(option) ++ \", got \" ++ @typeName(@TypeOf(value))),\n            };\n            break :blk c.curl_easy_setopt(easy, opt, cb);\n        },\n\n        .write_function => blk: {\n            const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {\n                .null => null,\n                .@\"fn\" => |info| struct {\n                    fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {\n                        const user_arg = if (@typeInfo(info.params[3].type.?) == .optional)\n                            user\n                        else\n                            user orelse unreachable;\n                        return value(@ptrCast(buffer), count, len, user_arg);\n                    }\n                }.cb,\n                else => @compileError(\"expected Zig function or null for \" ++ @tagName(option) ++ \", got \" ++ @typeName(@TypeOf(value))),\n            };\n            break :blk c.curl_easy_setopt(easy, opt, cb);\n        },\n    };\n    try errorCheck(code);\n}\n\npub fn curl_easy_getinfo(easy: *Curl, comptime info: CurlInfo, out: anytype) Error!void {\n    if (@typeInfo(@TypeOf(out)) != .pointer) {\n        @compileError(\"curl_easy_getinfo out must be a pointer, got \" ++ @typeName(@TypeOf(out)));\n    }\n\n    const inf: c.CURLINFO = @intFromEnum(info);\n    const code = switch (info) {\n        .effective_url => blk: {\n            const p: *[*c]u8 = out;\n            break :blk c.curl_easy_getinfo(easy, inf, p);\n        },\n        .response_code,\n        .redirect_count,\n        => blk: {\n            const p: *c_long = out;\n            break :blk c.curl_easy_getinfo(easy, inf, p);\n        },\n        .private => blk: {\n            const p: **anyopaque = out;\n            break :blk c.curl_easy_getinfo(easy, inf, p);\n        },\n    };\n    try errorCheck(code);\n}\n\npub fn curl_easy_header(\n    easy: *Curl,\n    name: [*:0]const u8,\n    index: usize,\n    comptime origin: CurlHeaderOrigin,\n    request: c_int,\n    hout: *?*CurlHeader,\n) ErrorHeader!void {\n    var c_hout: [*c]CurlHeader = null;\n    const code = c.curl_easy_header(easy, name, index, @intFromEnum(origin), request, &c_hout);\n    switch (code) {\n        c.CURLHE_OK => {\n            hout.* = @ptrCast(c_hout);\n            return;\n        },\n        c.CURLHE_BADINDEX,\n        c.CURLHE_MISSING,\n        c.CURLHE_NOHEADERS,\n        c.CURLHE_NOREQUEST,\n        => {\n            hout.* = null;\n            return;\n        },\n        else => {\n            hout.* = null;\n            return errorHFromCode(code);\n        },\n    }\n}\n\npub fn curl_easy_nextheader(\n    easy: *Curl,\n    comptime origin: CurlHeaderOrigin,\n    request: c_int,\n    prev: ?*CurlHeader,\n) ?*CurlHeader {\n    const ptr = c.curl_easy_nextheader(easy, @intFromEnum(origin), request, prev);\n    if (ptr == null) return null;\n    return @ptrCast(ptr);\n}\n\npub fn curl_multi_init() ?*CurlM {\n    return c.curl_multi_init();\n}\n\npub fn curl_multi_cleanup(multi: *CurlM) ErrorMulti!void {\n    try errorMCheck(c.curl_multi_cleanup(multi));\n}\n\npub fn curl_multi_setopt(multi: *CurlM, comptime option: CurlMOption, value: anytype) ErrorMulti!void {\n    const opt: c.CURLMoption = @intFromEnum(option);\n    const code = switch (option) {\n        .max_host_connections => blk: {\n            const n: c_long = switch (@typeInfo(@TypeOf(value))) {\n                .comptime_int, .int => @intCast(value),\n                else => @compileError(\"expected integer for \" ++ @tagName(option) ++ \", got \" ++ @typeName(@TypeOf(value))),\n            };\n            break :blk c.curl_multi_setopt(multi, opt, n);\n        },\n    };\n    try errorMCheck(code);\n}\n\npub fn curl_multi_add_handle(multi: *CurlM, easy: *Curl) ErrorMulti!void {\n    try errorMCheck(c.curl_multi_add_handle(multi, easy));\n}\n\npub fn curl_multi_remove_handle(multi: *CurlM, easy: *Curl) ErrorMulti!void {\n    try errorMCheck(c.curl_multi_remove_handle(multi, easy));\n}\n\npub fn curl_multi_perform(multi: *CurlM, running_handles: *c_int) ErrorMulti!void {\n    try errorMCheck(c.curl_multi_perform(multi, running_handles));\n}\n\npub fn curl_multi_poll(\n    multi: *CurlM,\n    extra_fds: []CurlWaitFd,\n    timeout_ms: c_int,\n    numfds: ?*c_int,\n) ErrorMulti!void {\n    const raw_fds: [*c]c.curl_waitfd = if (extra_fds.len == 0) null else @ptrCast(extra_fds.ptr);\n    try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds));\n}\n\npub fn curl_multi_waitfds(multi: *CurlM, ufds: []CurlWaitFd, fd_count: *c_uint) ErrorMulti!void {\n    const raw_fds: [*c]c.curl_waitfd = if (ufds.len == 0) null else @ptrCast(ufds.ptr);\n    try errorMCheck(c.curl_multi_waitfds(multi, raw_fds, @intCast(ufds.len), fd_count));\n}\n\npub fn curl_multi_timeout(multi: *CurlM, timeout_ms: *c_long) ErrorMulti!void {\n    try errorMCheck(c.curl_multi_timeout(multi, timeout_ms));\n}\n\npub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg {\n    const ptr = c.curl_multi_info_read(multi, msgs_in_queue);\n    if (ptr == null) return null;\n\n    const msg: *const c.CURLMsg = @ptrCast(ptr);\n    const easy_handle = msg.easy_handle orelse unreachable;\n\n    return switch (msg.msg) {\n        c.CURLMSG_NONE => .{\n            .easy_handle = easy_handle,\n            .data = .{ .none = msg.data.whatever },\n        },\n        c.CURLMSG_DONE => .{\n            .easy_handle = easy_handle,\n            .data = .{ .done = if (errorCheck(msg.data.result)) |_| null else |err| err },\n        },\n        c.CURLMSG_LAST => .{\n            .easy_handle = easy_handle,\n            .data = .{ .last = msg.data.whatever },\n        },\n        else => unreachable,\n    };\n}\n\npub fn curl_slist_append(list: ?*CurlSList, header: [*:0]const u8) ?*CurlSList {\n    return c.curl_slist_append(list, header);\n}\n\npub fn curl_slist_free_all(list: ?*CurlSList) void {\n    if (list) |ptr| {\n        c.curl_slist_free_all(ptr);\n    }\n}\n"
  },
  {
    "path": "src/telemetry/lightpanda.zig",
    "content": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst build_config = @import(\"build_config\");\n\nconst Allocator = std.mem.Allocator;\n\nconst log = @import(\"../log.zig\");\nconst App = @import(\"../App.zig\");\nconst Config = @import(\"../Config.zig\");\nconst telemetry = @import(\"telemetry.zig\");\nconst Runtime = @import(\"../network/Runtime.zig\");\nconst Connection = @import(\"../network/http.zig\").Connection;\n\nconst URL = \"https://telemetry.lightpanda.io\";\nconst BUFFER_SIZE = 1024;\nconst MAX_BODY_SIZE = 500 * 1024; // 500KB server limit\n\nconst LightPanda = @This();\n\nallocator: Allocator,\nruntime: *Runtime,\nwriter: std.Io.Writer.Allocating,\n\n/// Protects concurrent producers in send().\nmutex: std.Thread.Mutex = .{},\n\niid: ?[36]u8 = null,\nrun_mode: Config.RunMode = .serve,\n\nhead: std.atomic.Value(usize) = .init(0),\ntail: std.atomic.Value(usize) = .init(0),\ndropped: std.atomic.Value(usize) = .init(0),\nbuffer: [BUFFER_SIZE]telemetry.Event = undefined,\n\npub fn init(self: *LightPanda, app: *App, iid: ?[36]u8, run_mode: Config.RunMode) !void {\n    self.* = .{\n        .iid = iid,\n        .run_mode = run_mode,\n        .allocator = app.allocator,\n        .runtime = &app.network,\n        .writer = std.Io.Writer.Allocating.init(app.allocator),\n    };\n\n    self.runtime.onTick(@ptrCast(self), flushCallback);\n}\n\npub fn deinit(self: *LightPanda) void {\n    self.writer.deinit();\n}\n\npub fn send(self: *LightPanda, raw_event: telemetry.Event) !void {\n    self.mutex.lock();\n    defer self.mutex.unlock();\n\n    const t = self.tail.load(.monotonic);\n    const h = self.head.load(.acquire);\n    if (t - h >= BUFFER_SIZE) {\n        _ = self.dropped.fetchAdd(1, .monotonic);\n        return;\n    }\n\n    self.buffer[t % BUFFER_SIZE] = raw_event;\n    self.tail.store(t + 1, .release);\n}\n\nfn flushCallback(ctx: *anyopaque) void {\n    const self: *LightPanda = @ptrCast(@alignCast(ctx));\n    self.postEvent() catch |err| {\n        log.warn(.telemetry, \"flush error\", .{ .err = err });\n    };\n}\n\nfn postEvent(self: *LightPanda) !void {\n    const conn = self.runtime.getConnection() orelse {\n        return;\n    };\n    errdefer self.runtime.releaseConnection(conn);\n\n    const h = self.head.load(.monotonic);\n    const t = self.tail.load(.acquire);\n    const dropped = self.dropped.swap(0, .monotonic);\n\n    if (h == t and dropped == 0) {\n        self.runtime.releaseConnection(conn);\n        return;\n    }\n    errdefer _ = self.dropped.fetchAdd(dropped, .monotonic);\n\n    self.writer.clearRetainingCapacity();\n\n    if (dropped > 0) {\n        _ = try self.writeEvent(.{ .buffer_overflow = .{ .dropped = dropped } });\n    }\n\n    var sent: usize = 0;\n    for (h..t) |i| {\n        const fit = try self.writeEvent(self.buffer[i % BUFFER_SIZE]);\n        if (!fit) break;\n\n        sent += 1;\n    }\n\n    try conn.setURL(URL);\n    try conn.setMethod(.POST);\n    try conn.setBody(self.writer.written());\n\n    self.head.store(h + sent, .release);\n    self.runtime.submitRequest(conn);\n}\n\nfn writeEvent(self: *LightPanda, event: telemetry.Event) !bool {\n    const iid: ?[]const u8 = if (self.iid) |*id| id else null;\n    const wrapped = LightPandaEvent{ .iid = iid, .mode = self.run_mode, .event = event };\n\n    const checkpoint = self.writer.written().len;\n\n    try std.json.Stringify.value(&wrapped, .{ .emit_null_optional_fields = false }, &self.writer.writer);\n    try self.writer.writer.writeByte('\\n');\n\n    if (self.writer.written().len > MAX_BODY_SIZE) {\n        self.writer.shrinkRetainingCapacity(checkpoint);\n        return false;\n    }\n    return true;\n}\n\nconst LightPandaEvent = struct {\n    iid: ?[]const u8,\n    mode: Config.RunMode,\n    event: telemetry.Event,\n\n    pub fn jsonStringify(self: *const LightPandaEvent, writer: anytype) !void {\n        try writer.beginObject();\n\n        try writer.objectField(\"iid\");\n        try writer.write(self.iid);\n\n        try writer.objectField(\"mode\");\n        try writer.write(self.mode);\n\n        try writer.objectField(\"os\");\n        try writer.write(builtin.os.tag);\n\n        try writer.objectField(\"arch\");\n        try writer.write(builtin.cpu.arch);\n\n        try writer.objectField(\"version\");\n        try writer.write(build_config.git_version orelse build_config.git_commit);\n\n        try writer.objectField(\"event\");\n        try writer.write(@tagName(std.meta.activeTag(self.event)));\n\n        inline for (@typeInfo(telemetry.Event).@\"union\".fields) |union_field| {\n            if (self.event == @field(telemetry.Event, union_field.name)) {\n                const inner = @field(self.event, union_field.name);\n                const TI = @typeInfo(@TypeOf(inner));\n                if (TI == .@\"struct\") {\n                    inline for (TI.@\"struct\".fields) |field| {\n                        try writer.objectField(field.name);\n                        try writer.write(@field(inner, field.name));\n                    }\n                }\n            }\n        }\n\n        try writer.endObject();\n    }\n};\n"
  },
  {
    "path": "src/telemetry/telemetry.zig",
    "content": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst Allocator = std.mem.Allocator;\n\nconst log = @import(\"../log.zig\");\nconst App = @import(\"../App.zig\");\nconst Config = @import(\"../Config.zig\");\n\nconst uuidv4 = @import(\"../id.zig\").uuidv4;\nconst IID_FILE = \"iid\";\n\npub fn isDisabled() bool {\n    if (builtin.mode == .Debug or builtin.is_test) {\n        return true;\n    }\n\n    return std.process.hasEnvVarConstant(\"LIGHTPANDA_DISABLE_TELEMETRY\");\n}\n\npub const Telemetry = TelemetryT(@import(\"lightpanda.zig\"));\n\nfn TelemetryT(comptime P: type) type {\n    return struct {\n        provider: *P,\n\n        disabled: bool,\n\n        const Self = @This();\n\n        pub fn init(app: *App, run_mode: Config.RunMode) !Self {\n            const disabled = isDisabled();\n            if (builtin.mode != .Debug and builtin.is_test == false) {\n                log.info(.telemetry, \"telemetry status\", .{ .disabled = disabled });\n            }\n\n            const iid: ?[36]u8 = if (disabled) null else getOrCreateId(app.app_dir_path);\n\n            const provider = try app.allocator.create(P);\n            errdefer app.allocator.destroy(provider);\n\n            try P.init(provider, app, iid, run_mode);\n\n            return .{\n                .disabled = disabled,\n                .provider = provider,\n            };\n        }\n\n        pub fn deinit(self: *Self, allocator: Allocator) void {\n            self.provider.deinit();\n            allocator.destroy(self.provider);\n        }\n\n        pub fn record(self: *Self, event: Event) void {\n            if (self.disabled) {\n                return;\n            }\n            self.provider.send(event) catch |err| {\n                log.warn(.telemetry, \"record error\", .{ .err = err, .type = @tagName(std.meta.activeTag(event)) });\n            };\n        }\n    };\n}\n\nfn getOrCreateId(app_dir_path_: ?[]const u8) ?[36]u8 {\n    const app_dir_path = app_dir_path_ orelse {\n        var id: [36]u8 = undefined;\n        uuidv4(&id);\n        return id;\n    };\n\n    var buf: [37]u8 = undefined;\n    var dir = std.fs.openDirAbsolute(app_dir_path, .{}) catch |err| {\n        log.warn(.telemetry, \"data directory open error\", .{ .path = app_dir_path, .err = err });\n        return null;\n    };\n    defer dir.close();\n\n    const data = dir.readFile(IID_FILE, &buf) catch |err| switch (err) {\n        error.FileNotFound => &.{},\n        else => {\n            log.warn(.telemetry, \"ID read error\", .{ .path = app_dir_path, .err = err });\n            return null;\n        },\n    };\n\n    var id: [36]u8 = undefined;\n    if (data.len == 36) {\n        @memcpy(id[0..36], data);\n        return id;\n    }\n\n    uuidv4(&id);\n    dir.writeFile(.{ .sub_path = IID_FILE, .data = &id }) catch |err| {\n        log.warn(.telemetry, \"ID write error\", .{ .path = app_dir_path, .err = err });\n        return null;\n    };\n    return id;\n}\n\npub const Event = union(enum) {\n    run: void,\n    navigate: Navigate,\n    buffer_overflow: BufferOverflow,\n    flag: []const u8, // used for testing\n\n    const Navigate = struct {\n        tls: bool,\n        proxy: bool,\n        driver: []const u8 = \"cdp\",\n    };\n\n    const BufferOverflow = struct {\n        dropped: usize,\n    };\n};\n\nextern fn setenv(name: [*:0]u8, value: [*:0]u8, override: c_int) c_int;\nextern fn unsetenv(name: [*:0]u8) c_int;\n\nconst testing = @import(\"../testing.zig\");\ntest \"telemetry: always disabled in debug builds\" {\n    // Must be disabled regardless of environment variable.\n    _ = unsetenv(@constCast(\"LIGHTPANDA_DISABLE_TELEMETRY\"));\n    try testing.expectEqual(true, isDisabled());\n\n    _ = setenv(@constCast(\"LIGHTPANDA_DISABLE_TELEMETRY\"), @constCast(\"\"), 0);\n    defer _ = unsetenv(@constCast(\"LIGHTPANDA_DISABLE_TELEMETRY\"));\n    try testing.expectEqual(true, isDisabled());\n\n    const FailingProvider = struct {\n        fn init(_: *@This(), _: *App, _: ?[36]u8, _: Config.RunMode) !void {}\n        fn deinit(_: *@This()) void {}\n        pub fn send(_: *@This(), _: Event) !void {\n            unreachable;\n        }\n    };\n\n    var telemetry = try TelemetryT(FailingProvider).init(testing.test_app, .serve);\n    defer telemetry.deinit(testing.test_app.allocator);\n    telemetry.record(.{ .run = {} });\n}\n\ntest \"telemetry: getOrCreateId\" {\n    defer std.fs.cwd().deleteFile(\"/tmp/\" ++ IID_FILE) catch {};\n\n    std.fs.cwd().deleteFile(\"/tmp/\" ++ IID_FILE) catch {};\n\n    const id1 = getOrCreateId(\"/tmp/\").?;\n    const id2 = getOrCreateId(\"/tmp/\").?;\n    try testing.expectEqual(&id1, &id2);\n\n    std.fs.cwd().deleteFile(\"/tmp/\" ++ IID_FILE) catch {};\n    const id3 = getOrCreateId(\"/tmp/\").?;\n    try testing.expectEqual(false, std.mem.eql(u8, &id1, &id3));\n\n    const id4 = getOrCreateId(null).?;\n    try testing.expectEqual(false, std.mem.eql(u8, &id1, &id4));\n    try testing.expectEqual(false, std.mem.eql(u8, &id3, &id4));\n}\n\ntest \"telemetry: sends event to provider\" {\n    var telemetry = try TelemetryT(MockProvider).init(testing.test_app, .serve);\n    defer telemetry.deinit(testing.test_app.allocator);\n    telemetry.disabled = false;\n    const mock = telemetry.provider;\n\n    telemetry.record(.{ .flag = \"1\" });\n    telemetry.record(.{ .flag = \"2\" });\n    telemetry.record(.{ .flag = \"3\" });\n    try testing.expectEqual(3, mock.events.items.len);\n\n    for (mock.events.items, 0..) |event, i| {\n        try testing.expectEqual(i + 1, std.fmt.parseInt(usize, event.flag, 10));\n    }\n}\n\nconst MockProvider = struct {\n    allocator: Allocator,\n    events: std.ArrayList(Event),\n\n    fn init(self: *MockProvider, app: *App, _: ?[36]u8, _: Config.RunMode) !void {\n        self.* = .{\n            .events = .{},\n            .allocator = app.allocator,\n        };\n    }\n    fn deinit(self: *MockProvider) void {\n        self.events.deinit(self.allocator);\n    }\n    pub fn send(self: *MockProvider, event: Event) !void {\n        try self.events.append(self.allocator, event);\n    }\n};\n"
  },
  {
    "path": "src/test_runner.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst Allocator = std.mem.Allocator;\n\nconst BORDER = \"=\" ** 80;\n\n// use in custom panic handler\nvar current_test: ?[]const u8 = null;\npub var v8_peak_memory: usize = 0;\npub var tracking_allocator: Allocator = undefined;\n\nvar RUNNER: *Runner = undefined;\n\npub fn main() !void {\n    var mem: [8192]u8 = undefined;\n    var fba = std.heap.FixedBufferAllocator.init(&mem);\n\n    var gpa: std.heap.DebugAllocator(.{}) = .init;\n    var arena = std.heap.ArenaAllocator.init(gpa.allocator());\n    defer arena.deinit();\n\n    var ta = TrackingAllocator.init(gpa.allocator());\n    tracking_allocator = ta.allocator();\n\n    const allocator = fba.allocator();\n\n    const env = Env.init(allocator);\n\n    var runner = Runner.init(allocator, arena.allocator(), &ta, env);\n    RUNNER = &runner;\n    try runner.run();\n}\n\nconst Runner = struct {\n    env: Env,\n    allocator: Allocator,\n    ta: *TrackingAllocator,\n\n    // per-test arena, used for collecting substests\n    arena: Allocator,\n    subtests: std.ArrayList([]const u8),\n\n    fn init(allocator: Allocator, arena: Allocator, ta: *TrackingAllocator, env: Env) Runner {\n        return .{\n            .ta = ta,\n            .env = env,\n            .arena = arena,\n            .subtests = .empty,\n            .allocator = allocator,\n        };\n    }\n\n    pub fn run(self: *Runner) !void {\n        var slowest = SlowTracker.init(self.allocator, 5);\n        defer slowest.deinit();\n\n        var pass: usize = 0;\n        var fail: usize = 0;\n        var skip: usize = 0;\n        var leak: usize = 0;\n        // track all tests duration, excluding setup and teardown.\n        var ns_duration: u64 = 0;\n\n        Printer.fmt(\"\\r\\x1b[0K\", .{}); // beginning of line and clear to end of line\n\n        for (builtin.test_functions) |t| {\n            if (isSetup(t)) {\n                t.func() catch |err| {\n                    Printer.status(.fail, \"\\nsetup \\\"{s}\\\" failed: {}\\n\", .{ t.name, err });\n                    return err;\n                };\n            }\n        }\n\n        // If we have a subfilter, Document#query_selector_all\n        // Then we have a special check to make sure _some_ test was run. This\n        const webapi_html_test_mode = self.env.filter == null and self.env.subfilter != null;\n\n        for (builtin.test_functions) |t| {\n            if (isSetup(t) or isTeardown(t)) {\n                continue;\n            }\n\n            var status = Status.pass;\n            slowest.startTiming();\n\n            const is_unnamed_test = isUnnamed(t);\n            if (!is_unnamed_test) {\n                if (self.env.filter) |f| {\n                    if (std.mem.indexOf(u8, t.name, f) == null) {\n                        continue;\n                    }\n                } else if (webapi_html_test_mode) {\n                    // allow filtering by subfilter only, assumes subfilters\n                    // only exists for \"WebApi: \" tests (which is true for now).\n                    if (std.mem.indexOf(u8, t.name, \"WebApi: \") == null) {\n                        continue;\n                    }\n                }\n            }\n\n            const friendly_name = blk: {\n                const name = t.name;\n                var it = std.mem.splitScalar(u8, name, '.');\n                while (it.next()) |value| {\n                    if (std.mem.eql(u8, value, \"test\")) {\n                        const rest = it.rest();\n                        break :blk if (rest.len > 0) rest else name;\n                    }\n                }\n                break :blk name;\n            };\n            defer {\n                self.subtests = .{};\n                const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr));\n                _ = arena.reset(.{ .retain_with_limit = 2048 });\n            }\n\n            current_test = friendly_name;\n            std.testing.allocator_instance = .{};\n            const result = t.func();\n            current_test = null;\n\n            if (webapi_html_test_mode and self.subtests.items.len == 0) {\n                continue;\n            }\n\n            const ns_taken = slowest.endTiming(friendly_name, is_unnamed_test);\n            ns_duration += ns_taken;\n\n            if (std.testing.allocator_instance.deinit() == .leak) {\n                leak += 1;\n                Printer.status(.fail, \"\\n{s}\\n\\\"{s}\\\" - Memory Leak\\n{s}\\n\", .{ BORDER, friendly_name, BORDER });\n            }\n\n            if (result) |_| {\n                if (!is_unnamed_test) {\n                    pass += 1;\n                }\n            } else |err| switch (err) {\n                error.SkipZigTest => {\n                    skip += 1;\n                    status = .skip;\n                },\n                else => {\n                    status = .fail;\n                    fail += 1;\n                    Printer.status(.fail, \"\\n{s}\\n\\\"{s}\\\" - {s}\\n\", .{ BORDER, friendly_name, @errorName(err) });\n                    if (self.subtests.getLastOrNull()) |st| {\n                        Printer.status(.fail, \" {s}\\n\", .{st});\n                    }\n                    Printer.status(.fail, BORDER ++ \"\\n\", .{});\n                    if (@errorReturnTrace()) |trace| {\n                        std.debug.dumpStackTrace(trace.*);\n                    }\n                    if (self.env.fail_first) {\n                        break;\n                    }\n                },\n            }\n\n            if (!is_unnamed_test) {\n                if (self.env.verbose) {\n                    const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;\n                    Printer.status(status, \"{s} ({d:.2}ms)\\n\", .{ friendly_name, ms });\n                    for (self.subtests.items) |st| {\n                        Printer.status(status, \"  - {s} \\n\", .{st});\n                    }\n                } else {\n                    Printer.status(status, \".\", .{});\n                }\n            }\n        }\n\n        for (builtin.test_functions) |t| {\n            if (isTeardown(t)) {\n                t.func() catch |err| {\n                    Printer.status(.fail, \"\\nteardown \\\"{s}\\\" failed: {}\\n\", .{ t.name, err });\n                    return err;\n                };\n            }\n        }\n\n        const total_tests = pass + fail;\n        const status = if (total_tests > 0 and fail == 0) Status.pass else Status.fail;\n        Printer.status(status, \"\\n{d} of {d} test{s} passed\\n\", .{ pass, total_tests, if (total_tests != 1) \"s\" else \"\" });\n        if (skip > 0) {\n            Printer.status(.skip, \"{d} test{s} skipped\\n\", .{ skip, if (skip != 1) \"s\" else \"\" });\n        }\n        if (leak > 0) {\n            Printer.status(.fail, \"{d} test{s} leaked\\n\", .{ leak, if (leak != 1) \"s\" else \"\" });\n        }\n        Printer.fmt(\"\\n\", .{});\n        try slowest.display();\n        Printer.fmt(\"\\n\", .{});\n\n        // stats\n        if (self.env.metrics) {\n            var stdout = std.fs.File.stdout();\n            var writer = stdout.writer(&.{});\n            const stats = self.ta.stats();\n            try std.json.Stringify.value(&.{\n                .{ .name = \"browser\", .bench = .{\n                    .duration = ns_duration,\n                    .alloc_nb = stats.allocation_count,\n                    .realloc_nb = stats.reallocation_count,\n                    .alloc_size = stats.allocated_bytes,\n                } },\n                .{ .name = \"v8\", .bench = .{\n                    .duration = ns_duration,\n                    .alloc_nb = 0,\n                    .realloc_nb = 0,\n                    .alloc_size = v8_peak_memory,\n                } },\n            }, .{ .whitespace = .indent_2 }, &writer.interface);\n        }\n\n        std.posix.exit(if (fail == 0) 0 else 1);\n    }\n};\n\npub fn shouldRun(name: []const u8) bool {\n    const sf = RUNNER.env.subfilter orelse return true;\n    return std.mem.indexOf(u8, name, sf) != null;\n}\n\npub fn subtest(name: []const u8) !void {\n    try RUNNER.subtests.append(RUNNER.arena, try RUNNER.arena.dupe(u8, name));\n}\n\nconst Printer = struct {\n    fn fmt(comptime format: []const u8, args: anytype) void {\n        std.debug.print(format, args);\n    }\n\n    fn status(s: Status, comptime format: []const u8, args: anytype) void {\n        switch (s) {\n            .pass => std.debug.print(\"\\x1b[32m\", .{}),\n            .fail => std.debug.print(\"\\x1b[31m\", .{}),\n            .skip => std.debug.print(\"\\x1b[33m\", .{}),\n            else => {},\n        }\n        std.debug.print(format ++ \"\\x1b[0m\", args);\n    }\n};\n\nconst Status = enum {\n    pass,\n    fail,\n    skip,\n    text,\n};\n\nconst SlowTracker = struct {\n    const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);\n    max: usize,\n    slowest: SlowestQueue,\n    timer: std.time.Timer,\n\n    fn init(allocator: Allocator, count: u32) SlowTracker {\n        const timer = std.time.Timer.start() catch @panic(\"failed to start timer\");\n        var slowest = SlowestQueue.init(allocator, {});\n        slowest.ensureTotalCapacity(count) catch @panic(\"OOM\");\n        return .{\n            .max = count,\n            .timer = timer,\n            .slowest = slowest,\n        };\n    }\n\n    const TestInfo = struct {\n        ns: u64,\n        name: []const u8,\n    };\n\n    fn deinit(self: SlowTracker) void {\n        self.slowest.deinit();\n    }\n\n    fn startTiming(self: *SlowTracker) void {\n        self.timer.reset();\n    }\n\n    fn endTiming(self: *SlowTracker, test_name: []const u8, is_unnamed_test: bool) u64 {\n        var timer = self.timer;\n        const ns = timer.lap();\n        if (is_unnamed_test) {\n            return ns;\n        }\n\n        var slowest = &self.slowest;\n\n        if (slowest.count() < self.max) {\n            // Capacity is fixed to the # of slow tests we want to track\n            // If we've tracked fewer tests than this capacity, than always add\n            slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic(\"failed to track test timing\");\n            return ns;\n        }\n\n        {\n            // Optimization to avoid shifting the dequeue for the common case\n            // where the test isn't one of our slowest.\n            const fastest_of_the_slow = slowest.peekMin() orelse unreachable;\n            if (fastest_of_the_slow.ns > ns) {\n                // the test was faster than our fastest slow test, don't add\n                return ns;\n            }\n        }\n\n        // the previous fastest of our slow tests, has been pushed off.\n        _ = slowest.removeMin();\n        slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic(\"failed to track test timing\");\n        return ns;\n    }\n\n    fn display(self: *SlowTracker) !void {\n        var slowest = self.slowest;\n        const count = slowest.count();\n        Printer.fmt(\"Slowest {d} test{s}: \\n\", .{ count, if (count != 1) \"s\" else \"\" });\n        while (slowest.removeMinOrNull()) |info| {\n            const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;\n            Printer.fmt(\"  {d:.2}ms\\t{s}\\n\", .{ ms, info.name });\n        }\n    }\n\n    fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {\n        _ = context;\n        return std.math.order(a.ns, b.ns);\n    }\n};\n\nconst Env = struct {\n    verbose: bool,\n    fail_first: bool,\n    filter: ?[]const u8,\n    subfilter: ?[]const u8,\n    metrics: bool,\n\n    fn init(allocator: Allocator) Env {\n        const full_filter = readEnv(allocator, \"TEST_FILTER\");\n        const filter, const subfilter = parseFilter(full_filter);\n        return .{\n            .verbose = readEnvBool(allocator, \"TEST_VERBOSE\", true),\n            .fail_first = readEnvBool(allocator, \"TEST_FAIL_FIRST\", false),\n            .filter = filter,\n            .subfilter = subfilter,\n            .metrics = readEnvBool(allocator, \"METRICS\", false),\n        };\n    }\n\n    fn deinit(self: Env, allocator: Allocator) void {\n        if (self.filter) |f| {\n            allocator.free(f);\n        }\n    }\n\n    fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 {\n        const v = std.process.getEnvVarOwned(allocator, key) catch |err| {\n            if (err == error.EnvironmentVariableNotFound) {\n                return null;\n            }\n            std.log.warn(\"failed to get env var {s} due to err {}\", .{ key, err });\n            return null;\n        };\n        return v;\n    }\n\n    fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool {\n        const value = readEnv(allocator, key) orelse return deflt;\n        defer allocator.free(value);\n        return std.ascii.eqlIgnoreCase(value, \"true\");\n    }\n\n    fn parseFilter(full_filter: ?[]const u8) struct { ?[]const u8, ?[]const u8 } {\n        const ff = full_filter orelse return .{ null, null };\n        if (ff.len == 0) return .{ null, null };\n\n        const split = std.mem.indexOfScalarPos(u8, ff, 0, '#') orelse {\n            return .{ ff, null };\n        };\n\n        const filter = std.mem.trim(u8, ff[0..split], \" \");\n\n        return .{\n            if (filter.len == 0) null else filter,\n            std.mem.trim(u8, ff[split + 1 ..], \" \"),\n        };\n    }\n};\n\npub const panic = std.debug.FullPanic(struct {\n    pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {\n        if (current_test) |ct| {\n            std.debug.print(\"\\x1b[31m{s}\\npanic running \\\"{s}\\\"\\n\", .{ BORDER, ct });\n            if (RUNNER.subtests.getLastOrNull()) |st| {\n                std.debug.print(\" {s}\\n\", .{st});\n            }\n            std.debug.print(\"\\x1b[0m{s}\\n\", .{BORDER});\n        }\n        std.debug.defaultPanic(msg, first_trace_addr);\n    }\n}.panicFn);\n\nfn isUnnamed(t: std.builtin.TestFn) bool {\n    const marker = \".test_\";\n    const test_name = t.name;\n    const index = std.mem.indexOf(u8, test_name, marker) orelse return false;\n    _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;\n    return true;\n}\n\nfn isSetup(t: std.builtin.TestFn) bool {\n    return std.mem.endsWith(u8, t.name, \"tests:beforeAll\");\n}\n\nfn isTeardown(t: std.builtin.TestFn) bool {\n    return std.mem.endsWith(u8, t.name, \"tests:afterAll\");\n}\n\npub const TrackingAllocator = struct {\n    parent_allocator: Allocator,\n    free_count: usize = 0,\n    allocated_bytes: usize = 0,\n    allocation_count: usize = 0,\n    reallocation_count: usize = 0,\n    mutex: std.Thread.Mutex = .{},\n\n    const Stats = struct {\n        allocated_bytes: usize,\n        allocation_count: usize,\n        reallocation_count: usize,\n    };\n\n    fn init(parent_allocator: Allocator) TrackingAllocator {\n        return .{\n            .parent_allocator = parent_allocator,\n        };\n    }\n\n    pub fn stats(self: *const TrackingAllocator) Stats {\n        return .{\n            .allocated_bytes = self.allocated_bytes,\n            .allocation_count = self.allocation_count,\n            .reallocation_count = self.reallocation_count,\n        };\n    }\n\n    pub fn allocator(self: *TrackingAllocator) Allocator {\n        return .{ .ptr = self, .vtable = &.{\n            .alloc = alloc,\n            .resize = resize,\n            .free = free,\n            .remap = remap,\n        } };\n    }\n\n    fn alloc(\n        ctx: *anyopaque,\n        len: usize,\n        alignment: std.mem.Alignment,\n        return_address: usize,\n    ) ?[*]u8 {\n        const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        const result = self.parent_allocator.rawAlloc(len, alignment, return_address);\n        self.allocation_count += 1;\n        self.allocated_bytes += len;\n        return result;\n    }\n\n    fn resize(\n        ctx: *anyopaque,\n        old_mem: []u8,\n        alignment: std.mem.Alignment,\n        new_len: usize,\n        ra: usize,\n    ) bool {\n        const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        const result = self.parent_allocator.rawResize(old_mem, alignment, new_len, ra);\n        if (result) self.reallocation_count += 1;\n        return result;\n    }\n\n    fn free(\n        ctx: *anyopaque,\n        old_mem: []u8,\n        alignment: std.mem.Alignment,\n        ra: usize,\n    ) void {\n        const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        self.parent_allocator.rawFree(old_mem, alignment, ra);\n        self.free_count += 1;\n    }\n\n    fn remap(\n        ctx: *anyopaque,\n        memory: []u8,\n        alignment: std.mem.Alignment,\n        new_len: usize,\n        ret_addr: usize,\n    ) ?[*]u8 {\n        const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));\n        self.mutex.lock();\n        defer self.mutex.unlock();\n\n        const result = self.parent_allocator.rawRemap(memory, alignment, new_len, ret_addr);\n        if (result != null) self.reallocation_count += 1;\n        return result;\n    }\n};\n"
  },
  {
    "path": "src/testing.zig",
    "content": "// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)\n//\n// Francis Bouvier <francis@lightpanda.io>\n// Pierre Tachoire <pierre@lightpanda.io>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\npub const allocator = std.testing.allocator;\npub const expectError = std.testing.expectError;\npub const expect = std.testing.expect;\npub const expectString = std.testing.expectEqualStrings;\npub const expectEqualSlices = std.testing.expectEqualSlices;\n\n// sometimes it's super useful to have an arena you don't really care about\n// in a test. Like, you need a mutable string, so you just want to dupe a\n// string literal. It has nothing to do with the code under test, it's just\n// infrastructure for the test itself.\npub var arena_instance = std.heap.ArenaAllocator.init(std.heap.c_allocator);\npub const arena_allocator = arena_instance.allocator();\n\npub fn reset() void {\n    _ = arena_instance.reset(.retain_capacity);\n}\n\nconst App = @import(\"App.zig\");\nconst js = @import(\"browser/js/js.zig\");\nconst Config = @import(\"Config.zig\");\nconst HttpClient = @import(\"browser/HttpClient.zig\");\nconst Page = @import(\"browser/Page.zig\");\nconst Browser = @import(\"browser/Browser.zig\");\nconst Session = @import(\"browser/Session.zig\");\nconst Notification = @import(\"Notification.zig\");\n\n// Merged std.testing.expectEqual and std.testing.expectString\n// can be useful when testing fields of an anytype an you don't know\n// exactly how to assert equality\npub fn expectEqual(expected: anytype, actual: anytype) !void {\n    switch (@typeInfo(@TypeOf(actual))) {\n        .array => |arr| if (arr.child == u8) {\n            return std.testing.expectEqualStrings(expected, &actual);\n        },\n        .pointer => |ptr| {\n            if (ptr.child == u8) {\n                return std.testing.expectEqualStrings(expected, actual);\n            } else if (comptime isStringArray(ptr.child)) {\n                return std.testing.expectEqualStrings(expected, actual);\n            } else if (ptr.child == []u8 or ptr.child == []const u8) {\n                return expectString(expected, actual);\n            }\n        },\n        .@\"struct\" => |structType| {\n            inline for (structType.fields) |field| {\n                try expectEqual(@field(expected, field.name), @field(actual, field.name));\n            }\n            return;\n        },\n        .optional => {\n            if (@typeInfo(@TypeOf(expected)) == .null) {\n                return std.testing.expectEqual(null, actual);\n            }\n            if (actual) |_actual| {\n                return expectEqual(expected, _actual);\n            }\n            return std.testing.expectEqual(expected, null);\n        },\n        .@\"union\" => |union_info| {\n            if (union_info.tag_type == null) {\n                @compileError(\"Unable to compare untagged union values\");\n            }\n            const Tag = std.meta.Tag(@TypeOf(expected));\n\n            const expectedTag = @as(Tag, expected);\n            const actualTag = @as(Tag, actual);\n            try expectEqual(expectedTag, actualTag);\n\n            inline for (std.meta.fields(@TypeOf(actual))) |fld| {\n                if (std.mem.eql(u8, fld.name, @tagName(actualTag))) {\n                    try expectEqual(@field(expected, fld.name), @field(actual, fld.name));\n                    return;\n                }\n            }\n            unreachable;\n        },\n        else => {},\n    }\n    return std.testing.expectEqual(expected, actual);\n}\n\npub fn expectDelta(expected: anytype, actual: anytype, delta: anytype) !void {\n    if (@typeInfo(@TypeOf(expected)) == .null) {\n        return std.testing.expectEqual(null, actual);\n    }\n\n    switch (@typeInfo(@TypeOf(actual))) {\n        .optional => {\n            if (actual) |value| {\n                return expectDelta(expected, value, delta);\n            }\n            return std.testing.expectEqual(null, expected);\n        },\n        else => {},\n    }\n\n    switch (@typeInfo(@TypeOf(expected))) {\n        .optional => {\n            if (expected) |value| {\n                return expectDelta(value, actual, delta);\n            }\n            return std.testing.expectEqual(null, actual);\n        },\n        else => {},\n    }\n\n    var diff = expected - actual;\n    if (diff < 0) {\n        diff = -diff;\n    }\n    if (diff <= delta) {\n        return;\n    }\n\n    print(\"Expected {} to be within {} of {}. Actual diff: {}\", .{ expected, delta, actual, diff });\n    return error.NotWithinDelta;\n}\n\nfn isStringArray(comptime T: type) bool {\n    if (!is(.array)(T) and !isPtrTo(.array)(T)) {\n        return false;\n    }\n    return std.meta.Elem(T) == u8;\n}\n\npub const TraitFn = fn (type) bool;\npub fn is(comptime id: std.builtin.TypeId) TraitFn {\n    const Closure = struct {\n        pub fn trait(comptime T: type) bool {\n            return id == @typeInfo(T);\n        }\n    };\n    return Closure.trait;\n}\n\npub fn isPtrTo(comptime id: std.builtin.TypeId) TraitFn {\n    const Closure = struct {\n        pub fn trait(comptime T: type) bool {\n            if (!comptime isSingleItemPtr(T)) return false;\n            return id == @typeInfo(std.meta.Child(T));\n        }\n    };\n    return Closure.trait;\n}\n\npub fn isSingleItemPtr(comptime T: type) bool {\n    if (comptime is(.pointer)(T)) {\n        return @typeInfo(T).pointer.size == .one;\n    }\n    return false;\n}\n\npub fn print(comptime fmt: []const u8, args: anytype) void {\n    if (@inComptime()) {\n        @compileError(std.fmt.comptimePrint(fmt, args));\n    } else {\n        std.debug.print(fmt, args);\n    }\n}\n\nconst String = @import(\"string.zig\").String;\npub fn newString(str: []const u8) String {\n    return String.init(arena_allocator, str, .{}) catch unreachable;\n}\n\npub const Random = struct {\n    var instance: ?std.Random.DefaultPrng = null;\n\n    pub fn fill(buf: []u8) void {\n        var r = random();\n        r.bytes(buf);\n    }\n\n    pub fn fillAtLeast(buf: []u8, min: usize) []u8 {\n        var r = random();\n        const l = r.intRangeAtMost(usize, min, buf.len);\n        r.bytes(buf[0..l]);\n        return buf;\n    }\n\n    pub fn intRange(comptime T: type, min: T, max: T) T {\n        var r = random();\n        return r.intRangeAtMost(T, min, max);\n    }\n\n    pub fn random() std.Random {\n        if (instance == null) {\n            var seed: u64 = undefined;\n            std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;\n            instance = std.Random.DefaultPrng.init(seed);\n            // instance = std.Random.DefaultPrng.init(0);\n        }\n        return instance.?.random();\n    }\n};\n\npub fn expectJson(a: anytype, b: anytype) !void {\n    var arena = std.heap.ArenaAllocator.init(allocator);\n    defer arena.deinit();\n\n    const aa = arena.allocator();\n\n    const a_value = try convertToJson(aa, a);\n    const b_value = try convertToJson(aa, b);\n\n    errdefer {\n        const a_json = std.json.Stringify.valueAlloc(aa, a_value, .{ .whitespace = .indent_2 }) catch unreachable;\n        const b_json = std.json.Stringify.valueAlloc(aa, b_value, .{ .whitespace = .indent_2 }) catch unreachable;\n        std.debug.print(\"== Expected ==\\n{s}\\n\\n== Actual ==\\n{s}\", .{ a_json, b_json });\n    }\n\n    try expectJsonValue(a_value, b_value);\n}\n\npub fn isEqualJson(a: anytype, b: anytype) !bool {\n    var arena = std.heap.ArenaAllocator.init(allocator);\n    defer arena.deinit();\n\n    const aa = arena.allocator();\n    const a_value = try convertToJson(aa, a);\n    const b_value = try convertToJson(aa, b);\n    return isJsonValue(a_value, b_value);\n}\n\nfn convertToJson(arena: Allocator, value: anytype) !std.json.Value {\n    const T = @TypeOf(value);\n    if (T == std.json.Value) {\n        return value;\n    }\n\n    var str: []const u8 = undefined;\n    if (T == []u8 or T == []const u8 or comptime isStringArray(T)) {\n        str = value;\n    } else {\n        str = try std.json.Stringify.valueAlloc(arena, value, .{});\n    }\n    return std.json.parseFromSliceLeaky(std.json.Value, arena, str, .{});\n}\n\nfn expectJsonValue(a: std.json.Value, b: std.json.Value) !void {\n    try expectEqual(@tagName(a), @tagName(b));\n\n    // at this point, we know that if a is an int, b must also be an int\n    switch (a) {\n        .null => return,\n        .bool => try expectEqual(a.bool, b.bool),\n        .integer => try expectEqual(a.integer, b.integer),\n        .float => try expectEqual(a.float, b.float),\n        .number_string => try expectEqual(a.number_string, b.number_string),\n        .string => try expectEqual(a.string, b.string),\n        .array => {\n            const a_len = a.array.items.len;\n            const b_len = b.array.items.len;\n            try expectEqual(a_len, b_len);\n            for (a.array.items, b.array.items) |a_item, b_item| {\n                try expectJsonValue(a_item, b_item);\n            }\n        },\n        .object => {\n            var it = a.object.iterator();\n            while (it.next()) |entry| {\n                const key = entry.key_ptr.*;\n                if (b.object.get(key)) |b_item| {\n                    try expectJsonValue(entry.value_ptr.*, b_item);\n                } else {\n                    return error.MissingKey;\n                }\n            }\n        },\n    }\n}\n\nfn isJsonValue(a: std.json.Value, b: std.json.Value) bool {\n    if (std.mem.eql(u8, @tagName(a), @tagName(b)) == false) {\n        return false;\n    }\n\n    // at this point, we know that if a is an int, b must also be an int\n    switch (a) {\n        .null => return true,\n        .bool => return a.bool == b.bool,\n        .integer => return a.integer == b.integer,\n        .float => return a.float == b.float,\n        .number_string => return std.mem.eql(u8, a.number_string, b.number_string),\n        .string => return std.mem.eql(u8, a.string, b.string),\n        .array => {\n            const a_len = a.array.items.len;\n            const b_len = b.array.items.len;\n            if (a_len != b_len) {\n                return false;\n            }\n            for (a.array.items, b.array.items) |a_item, b_item| {\n                if (isJsonValue(a_item, b_item) == false) {\n                    return false;\n                }\n            }\n            return true;\n        },\n        .object => {\n            var it = a.object.iterator();\n            while (it.next()) |entry| {\n                const key = entry.key_ptr.*;\n                if (b.object.get(key)) |b_item| {\n                    if (isJsonValue(entry.value_ptr.*, b_item) == false) {\n                        return false;\n                    }\n                } else {\n                    return false;\n                }\n            }\n            return true;\n        },\n    }\n}\n\npub var test_app: *App = undefined;\npub var test_http: *HttpClient = undefined;\npub var test_browser: Browser = undefined;\npub var test_notification: *Notification = undefined;\npub var test_session: *Session = undefined;\n\nconst WEB_API_TEST_ROOT = \"src/browser/tests/\";\nconst HtmlRunnerOpts = struct {};\n\npub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void {\n    _ = opts;\n    defer reset();\n\n    const root = try std.fs.path.joinZ(arena_allocator, &.{ WEB_API_TEST_ROOT, path });\n    const stat = std.fs.cwd().statFile(root) catch |err| {\n        std.debug.print(\"Failed to stat file: '{s}'\", .{root});\n        return err;\n    };\n\n    switch (stat.kind) {\n        .file => {\n            if (@import(\"root\").shouldRun(std.fs.path.basename(root)) == false) {\n                return;\n            }\n            try @import(\"root\").subtest(root);\n            try runWebApiTest(root);\n        },\n        .directory => {\n            var dir = try std.fs.cwd().openDir(root, .{\n                .iterate = true,\n                .no_follow = true,\n                .access_sub_paths = false,\n            });\n            defer dir.close();\n\n            var it = dir.iterateAssumeFirstIteration();\n            while (try it.next()) |entry| {\n                if (entry.kind != .file) {\n                    continue;\n                }\n\n                if (!std.mem.endsWith(u8, entry.name, \".html\")) {\n                    continue;\n                }\n\n                if (@import(\"root\").shouldRun(entry.name) == false) {\n                    continue;\n                }\n\n                const full_path = try std.fs.path.joinZ(arena_allocator, &.{ root, entry.name });\n                try @import(\"root\").subtest(entry.name);\n                try runWebApiTest(full_path);\n            }\n        },\n        else => |kind| {\n            std.debug.print(\"Unknown file type: {s} for {s}\\n\", .{ @tagName(kind), root });\n            return error.InvalidTestPath;\n        },\n    }\n}\n\nfn runWebApiTest(test_file: [:0]const u8) !void {\n    const page = try test_session.createPage();\n    defer test_session.removePage();\n\n    const url = try std.fmt.allocPrintSentinel(\n        arena_allocator,\n        \"http://127.0.0.1:9582/{s}\",\n        .{test_file},\n        0,\n    );\n\n    var ls: js.Local.Scope = undefined;\n    page.js.localScope(&ls);\n    defer ls.deinit();\n\n    var try_catch: js.TryCatch = undefined;\n    try_catch.init(&ls.local);\n    defer try_catch.deinit();\n\n    try page.navigate(url, .{});\n    _ = test_session.wait(2000);\n\n    test_browser.runMicrotasks();\n\n    ls.local.eval(\"testing.assertOk()\", \"testing.assertOk()\") catch |err| {\n        const caught = try_catch.caughtOrError(arena_allocator, err);\n        std.debug.print(\"{s}: test failure\\nError: {f}\\n\", .{ test_file, caught });\n        return err;\n    };\n}\n\n// Used by a few CDP tests - wouldn't be sad to see this go.\npub fn pageTest(comptime test_file: []const u8) !*Page {\n    const page = try test_session.createPage();\n    errdefer test_session.removePage();\n\n    const url = try std.fmt.allocPrintSentinel(\n        arena_allocator,\n        \"http://127.0.0.1:9582/{s}{s}\",\n        .{ WEB_API_TEST_ROOT, test_file },\n        0,\n    );\n\n    try page.navigate(url, .{});\n    _ = test_session.wait(2000);\n    return page;\n}\n\ntest {\n    std.testing.refAllDecls(@This());\n}\n\nconst log = @import(\"log.zig\");\nconst TestHTTPServer = @import(\"TestHTTPServer.zig\");\n\nconst Server = @import(\"Server.zig\");\nvar test_cdp_server: ?*Server = null;\nvar test_cdp_server_thread: ?std.Thread = null;\nvar test_http_server: ?TestHTTPServer = null;\nvar test_http_server_thread: ?std.Thread = null;\n\nvar test_config: Config = undefined;\n\ntest \"tests:beforeAll\" {\n    log.opts.level = .warn;\n    log.opts.format = .pretty;\n\n    const test_allocator = @import(\"root\").tracking_allocator;\n\n    test_config = try Config.init(test_allocator, \"test\", .{ .serve = .{\n        .common = .{\n            .tls_verify_host = false,\n            .user_agent_suffix = \"internal-tester\",\n        },\n    } });\n\n    test_app = try App.init(test_allocator, &test_config);\n    errdefer test_app.deinit();\n\n    test_http = try HttpClient.init(test_allocator, &test_app.network);\n    errdefer test_http.deinit();\n\n    test_browser = try Browser.init(test_app, .{ .http_client = test_http });\n    errdefer test_browser.deinit();\n\n    // Create notification for testing\n    test_notification = try Notification.init(test_app.allocator);\n    errdefer test_notification.deinit();\n\n    test_session = try test_browser.newSession(test_notification);\n\n    var wg: std.Thread.WaitGroup = .{};\n    wg.startMany(2);\n\n    test_cdp_server_thread = try std.Thread.spawn(.{}, serveCDP, .{&wg});\n\n    test_http_server = TestHTTPServer.init(testHTTPHandler);\n    test_http_server_thread = try std.Thread.spawn(.{}, TestHTTPServer.run, .{ &test_http_server.?, &wg });\n\n    // need to wait for the servers to be listening, else tests will fail because\n    // they aren't able to connect.\n    wg.wait();\n}\n\ntest \"tests:afterAll\" {\n    test_app.network.stop();\n    if (test_cdp_server_thread) |thread| {\n        thread.join();\n    }\n    if (test_cdp_server) |server| {\n        server.deinit();\n    }\n\n    if (test_http_server) |*server| {\n        server.stop();\n    }\n    if (test_http_server_thread) |thread| {\n        thread.join();\n    }\n    if (test_http_server) |*server| {\n        server.deinit();\n    }\n\n    @import(\"root\").v8_peak_memory = test_browser.env.isolate.getHeapStatistics().total_physical_size;\n\n    test_notification.deinit();\n    test_browser.deinit();\n    test_http.deinit();\n    test_app.deinit();\n    test_config.deinit(@import(\"root\").tracking_allocator);\n}\n\nfn serveCDP(wg: *std.Thread.WaitGroup) !void {\n    const address = try std.net.Address.parseIp(\"127.0.0.1\", 9583);\n\n    test_cdp_server = Server.init(test_app, address) catch |err| {\n        std.debug.print(\"CDP server error: {}\", .{err});\n        return err;\n    };\n    wg.finish();\n\n    test_app.network.run();\n}\n\nfn testHTTPHandler(req: *std.http.Server.Request) !void {\n    const path = req.head.target;\n\n    if (std.mem.eql(u8, path, \"/xhr\")) {\n        return req.respond(\"1234567890\" ** 10, .{\n            .extra_headers = &.{\n                .{ .name = \"Content-Type\", .value = \"text/html; charset=utf-8\" },\n            },\n        });\n    }\n\n    if (std.mem.eql(u8, path, \"/xhr_empty\")) {\n        return req.respond(\"\", .{\n            .extra_headers = &.{\n                .{ .name = \"Content-Type\", .value = \"text/html; charset=utf-8\" },\n            },\n        });\n    }\n\n    if (std.mem.eql(u8, path, \"/xhr/json\")) {\n        return req.respond(\"{\\\"over\\\":\\\"9000!!!\\\",\\\"updated_at\\\":1765867200000}\", .{\n            .extra_headers = &.{\n                .{ .name = \"Content-Type\", .value = \"application/json\" },\n            },\n        });\n    }\n\n    if (std.mem.eql(u8, path, \"/xhr/redirect\")) {\n        return req.respond(\"\", .{\n            .status = .found,\n            .extra_headers = &.{\n                .{ .name = \"Location\", .value = \"http://127.0.0.1:9582/xhr\" },\n            },\n        });\n    }\n\n    if (std.mem.eql(u8, path, \"/xhr/404\")) {\n        return req.respond(\"Not Found\", .{\n            .status = .not_found,\n            .extra_headers = &.{\n                .{ .name = \"Content-Type\", .value = \"text/plain\" },\n            },\n        });\n    }\n\n    if (std.mem.eql(u8, path, \"/xhr/500\")) {\n        return req.respond(\"Internal Server Error\", .{\n            .status = .internal_server_error,\n            .extra_headers = &.{\n                .{ .name = \"Content-Type\", .value = \"text/plain\" },\n            },\n        });\n    }\n\n    if (std.mem.eql(u8, path, \"/xhr/binary\")) {\n        return req.respond(&.{ 0, 0, 1, 2, 0, 0, 9 }, .{\n            .extra_headers = &.{\n                .{ .name = \"Content-Type\", .value = \"application/octet-stream\" },\n            },\n        });\n    }\n\n    if (std.mem.startsWith(u8, path, \"/src/browser/tests/\")) {\n        // strip off leading / so that it's relative to CWD\n        return TestHTTPServer.sendFile(req, path[1..]);\n    }\n\n    std.debug.print(\"TestHTTPServer was asked to serve an unknown file: {s}\\n\", .{path});\n\n    unreachable;\n}\n\n/// LogFilter provides a scoped way to suppress specific log categories during tests.\n/// This is useful for tests that trigger expected errors or warnings.\npub const LogFilter = struct {\n    old_filter: []const log.Scope,\n\n    /// Sets the log filter to suppress the specified scope(s).\n    /// Returns a LogFilter that should be deinitialized to restore previous filters.\n    pub fn init(comptime scopes: []const log.Scope) LogFilter {\n        comptime std.debug.assert(@TypeOf(scopes) == []const log.Scope);\n        const old_filter = log.opts.filter_scopes;\n        log.opts.filter_scopes = scopes;\n        return .{ .old_filter = old_filter };\n    }\n\n    /// Restores the log filters to their previous state.\n    pub fn deinit(self: LogFilter) void {\n        log.opts.filter_scopes = self.old_filter;\n    }\n};\n"
  }
]