Full Code of hotwired/turbo for AI

main 4481af6b6d74 cached
223 files
547.0 KB
145.0k tokens
862 symbols
1 requests
Download .txt
Showing preview only (599K chars total). Download the full file or copy to clipboard to get everything.
Repository: hotwired/turbo
Branch: main
Commit: 4481af6b6d74
Files: 223
Total size: 547.0 KB

Directory structure:
gitextract_vgl1u45s/

├── .devcontainer/
│   ├── Dockerfile
│   └── devcontainer.json
├── .eslintignore
├── .eslintrc.js
├── .github/
│   ├── scripts/
│   │   └── publish-dev-build
│   └── workflows/
│       ├── ci.yml
│       └── dev-builds.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│   ├── launch.json
│   └── tasks.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── MIT-LICENSE
├── README.md
├── package.json
├── playwright.config.js
├── rollup.config.js
├── src/
│   ├── core/
│   │   ├── bardo.js
│   │   ├── cache.js
│   │   ├── config/
│   │   │   ├── drive.js
│   │   │   ├── forms.js
│   │   │   └── index.js
│   │   ├── drive/
│   │   │   ├── error_renderer.js
│   │   │   ├── form_submission.js
│   │   │   ├── head_snapshot.js
│   │   │   ├── history.js
│   │   │   ├── limited_set.js
│   │   │   ├── morphing_page_renderer.js
│   │   │   ├── navigator.js
│   │   │   ├── page_renderer.js
│   │   │   ├── page_snapshot.js
│   │   │   ├── page_view.js
│   │   │   ├── prefetch_cache.js
│   │   │   ├── preloader.js
│   │   │   ├── progress_bar.js
│   │   │   ├── snapshot_cache.js
│   │   │   ├── view_transitioner.js
│   │   │   └── visit.js
│   │   ├── errors.js
│   │   ├── frames/
│   │   │   ├── frame_controller.js
│   │   │   ├── frame_redirector.js
│   │   │   ├── frame_renderer.js
│   │   │   ├── frame_view.js
│   │   │   ├── link_interceptor.js
│   │   │   └── morphing_frame_renderer.js
│   │   ├── index.js
│   │   ├── lru_cache.js
│   │   ├── morphing.js
│   │   ├── native/
│   │   │   └── browser_adapter.js
│   │   ├── renderer.js
│   │   ├── session.js
│   │   ├── snapshot.js
│   │   ├── streams/
│   │   │   ├── stream_actions.js
│   │   │   ├── stream_message.js
│   │   │   └── stream_message_renderer.js
│   │   ├── url.js
│   │   └── view.js
│   ├── elements/
│   │   ├── frame_element.js
│   │   ├── index.js
│   │   ├── stream_element.js
│   │   └── stream_source_element.js
│   ├── http/
│   │   ├── fetch.js
│   │   ├── fetch_request.js
│   │   ├── fetch_response.js
│   │   └── index.js
│   ├── index.js
│   ├── observers/
│   │   ├── appearance_observer.js
│   │   ├── cache_observer.js
│   │   ├── form_link_click_observer.js
│   │   ├── form_submit_observer.js
│   │   ├── link_click_observer.js
│   │   ├── link_prefetch_observer.js
│   │   ├── page_observer.js
│   │   ├── scroll_observer.js
│   │   └── stream_observer.js
│   ├── polyfills/
│   │   └── index.js
│   ├── script_warning.js
│   ├── tests/
│   │   ├── fixtures/
│   │   │   ├── 422.html
│   │   │   ├── 422_morph.html
│   │   │   ├── 422_tall.html
│   │   │   ├── 500.html
│   │   │   ├── additional_assets.html
│   │   │   ├── additional_script.html
│   │   │   ├── async_script.html
│   │   │   ├── async_script_2.html
│   │   │   ├── autofocus-inert.html
│   │   │   ├── autofocus.html
│   │   │   ├── bare.html
│   │   │   ├── body_noscript.html
│   │   │   ├── body_noscript_with_content.html
│   │   │   ├── body_script.html
│   │   │   ├── cache_observer.html
│   │   │   ├── dir_rtl.html
│   │   │   ├── drive.html
│   │   │   ├── drive_disabled.html
│   │   │   ├── es_locale.html
│   │   │   ├── esm.html
│   │   │   ├── eval_false_script.html
│   │   │   ├── form.html
│   │   │   ├── form_mode.html
│   │   │   ├── frame_navigation.html
│   │   │   ├── frame_preloading.html
│   │   │   ├── frame_refresh_after_navigation.html
│   │   │   ├── frame_refresh_morph.html
│   │   │   ├── frame_refresh_reload.html
│   │   │   ├── frames/
│   │   │   │   ├── body_script.html
│   │   │   │   ├── body_script_2.html
│   │   │   │   ├── empty_head.html
│   │   │   │   ├── eval_false_script.html
│   │   │   │   ├── form-redirect.html
│   │   │   │   ├── form-redirected.html
│   │   │   │   ├── form.html
│   │   │   │   ├── frame.html
│   │   │   │   ├── frame_for_eager.html
│   │   │   │   ├── hello.html
│   │   │   │   ├── parent.html
│   │   │   │   ├── part.html
│   │   │   │   ├── preloading.html
│   │   │   │   ├── recursive.html
│   │   │   │   ├── self.html
│   │   │   │   └── unvisitable.html
│   │   │   ├── frames.html
│   │   │   ├── greetings.ejs
│   │   │   ├── head_script.html
│   │   │   ├── headers.html
│   │   │   ├── hot_preloading.html
│   │   │   ├── hover_to_prefetch.html
│   │   │   ├── hover_to_prefetch_custom_cache_time.html
│   │   │   ├── hover_to_prefetch_disabled.html
│   │   │   ├── hover_to_prefetch_iframe.html
│   │   │   ├── hover_to_prefetch_without_meta_tag_with_link_to_with_meta_tag.html
│   │   │   ├── link_redirect.html
│   │   │   ├── link_redirect_target.html
│   │   │   ├── loading.html
│   │   │   ├── navigation.html
│   │   │   ├── noscript.css
│   │   │   ├── one.html
│   │   │   ├── page_refresh.html
│   │   │   ├── page_refresh_replace.html
│   │   │   ├── page_refresh_scroll_reset.html
│   │   │   ├── page_refresh_stream_action.html
│   │   │   ├── page_refreshed.html
│   │   │   ├── page_with_eager_frame.html
│   │   │   ├── pausable_rendering.html
│   │   │   ├── pausable_requests.html
│   │   │   ├── permanent_children.html
│   │   │   ├── permanent_element.html
│   │   │   ├── prefetched.html
│   │   │   ├── preloaded.html
│   │   │   ├── preloading.html
│   │   │   ├── remote_permanent_frame.html
│   │   │   ├── rendering.html
│   │   │   ├── response.js
│   │   │   ├── root/
│   │   │   │   ├── index.html
│   │   │   │   └── page.html
│   │   │   ├── scroll/
│   │   │   │   ├── one.html
│   │   │   │   └── two.html
│   │   │   ├── scroll_restoration.html
│   │   │   ├── stream.html
│   │   │   ├── stylesheets/
│   │   │   │   ├── common.css
│   │   │   │   ├── left.css
│   │   │   │   ├── left.html
│   │   │   │   ├── right.css
│   │   │   │   └── right.html
│   │   │   ├── tabs/
│   │   │   │   ├── three.html
│   │   │   │   └── two.html
│   │   │   ├── tabs.html
│   │   │   ├── target.html
│   │   │   ├── test.css
│   │   │   ├── test.js
│   │   │   ├── tracked_asset_change.html
│   │   │   ├── tracked_nonce_change.html
│   │   │   ├── transitions/
│   │   │   │   ├── left.html
│   │   │   │   ├── left_legacy.html
│   │   │   │   ├── other.html
│   │   │   │   ├── right.html
│   │   │   │   └── right_legacy.html
│   │   │   ├── two.html
│   │   │   ├── ujs.html
│   │   │   ├── umd.html
│   │   │   ├── video.webm
│   │   │   ├── visit.html
│   │   │   └── visit_control_reload.html
│   │   ├── functional/
│   │   │   ├── async_script_tests.js
│   │   │   ├── autofocus_tests.js
│   │   │   ├── cache_observer_tests.js
│   │   │   ├── drive_disabled_tests.js
│   │   │   ├── drive_stylesheet_merging_tests.js
│   │   │   ├── drive_tests.js
│   │   │   ├── drive_view_transition_legacy_tests.js
│   │   │   ├── drive_view_transition_tests.js
│   │   │   ├── form_mode_tests.js
│   │   │   ├── form_submission_tests.js
│   │   │   ├── frame_navigation_tests.js
│   │   │   ├── frame_tests.js
│   │   │   ├── import_tests.js
│   │   │   ├── link_prefetch_observer_tests.js
│   │   │   ├── loading_tests.js
│   │   │   ├── navigation_tests.js
│   │   │   ├── page_refresh_stream_action_tests.js
│   │   │   ├── page_refresh_tests.js
│   │   │   ├── pausable_rendering_tests.js
│   │   │   ├── pausable_requests_tests.js
│   │   │   ├── preloader_tests.js
│   │   │   ├── rendering_tests.js
│   │   │   ├── root_tests.js
│   │   │   ├── scroll_restoration_tests.js
│   │   │   ├── stream_tests.js
│   │   │   └── visit_tests.js
│   │   ├── helpers/
│   │   │   ├── dom_test_case.js
│   │   │   └── page.js
│   │   ├── integration/
│   │   │   └── ujs_tests.js
│   │   ├── server.mjs
│   │   └── unit/
│   │       ├── deprecated_adapter_support_tests.js
│   │       ├── export_tests.js
│   │       ├── limited_set_tests.js
│   │       ├── native_adapter_support_tests.js
│   │       └── stream_element_tests.js
│   └── util.js
└── web-test-runner.config.mjs

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/Dockerfile
================================================
# See comments in devcontainer.json for details of setting the playwright tag.
ARG PLAYWRIGHT_TAG="v1.51.1-jammy"
FROM mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG}

WORKDIR /app


================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/typescript-node
{
  "name": "Node.js & Playwright",
  "build": {
    "dockerfile": "Dockerfile",
    // Change to pick a different tag, see https://mcr.microsoft.com/en-us/product/playwright/tags
    //
    // IMPORTANT: The playwright image version must match @playwright/test version from package.json.
    // Otherwise it will not work, the test version will look for the browser versions which it won't
    // find on the image.
    //
    // pick '...-arm64' version if running on Apple Silicon.
    "args": {
      "PLAYWRIGHT_TAG": "v1.51.1-jammy"
    }
  },
  // Add the IDs of extensions you want installed when the container is created.
  "customizations": {
    "vscode": {
      "extensions": ["dbaeumer.vscode-eslint"]
    }
  },
  // Use 'forwardPorts' to make a list of ports inside the container available locally.
  "forwardPorts": [9000],
  // Use 'postCreateCommand' to run commands after the container is created.
  "postCreateCommand": "yarn install && yarn build"
}


================================================
FILE: .eslintignore
================================================
dist/
node_modules/


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: ["eslint:recommended"],
  overrides: [
    {
      env: {
        node: true
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script"
      }
    }
  ],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module"
  },
  rules: {
    "comma-dangle": "error",
    "curly": ["error", "multi-line"],
    "getter-return": "off",
    "no-console": "off",
    "no-duplicate-imports": ["error"],
    "no-multi-spaces": ["error", { "exceptions": { "VariableDeclarator": true }}],
    "no-multiple-empty-lines": ["error", { "max": 2 }],
    "no-self-assign": ["error", { "props": false }],
    "no-trailing-spaces": ["error"],
    "no-unused-vars": ["error", { argsIgnorePattern: "_*" }],
    "no-useless-escape": "off",
    "no-var": ["error"],
    "prefer-const": ["error"],
    "semi": ["error", "never"]
  },
  globals: {
    test: true,
    setup: true
  }
}


================================================
FILE: .github/scripts/publish-dev-build
================================================
#!/usr/bin/env bash
set -eux

DEV_BUILD_REPO_NAME="hotwired/dev-builds"
DEV_BUILD_ORIGIN_URL="https://${1}@github.com/${DEV_BUILD_REPO_NAME}.git"
BUILD_PATH="$HOME/publish-dev-build"

mkdir "$BUILD_PATH"

cd "$GITHUB_WORKSPACE"
package_name="$(jq -r .name package.json)"
package_files=( dist package.json )
tag="${package_name}/${GITHUB_SHA:0:7}"

name="$(git log -n 1 --format=format:%cn)"
email="$(git log -n 1 --format=format:%ce)"
subject="$(git log -n 1 --format=format:%s)"
date="$(git log -n 1 --format=format:%ai)"
url="https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}"
message="$tag $subject"$'\n\n'"$url"

cp -R "${package_files[@]}" "$BUILD_PATH"

cd "$BUILD_PATH"
git init .
git remote add origin "$DEV_BUILD_ORIGIN_URL"
git symbolic-ref HEAD refs/heads/publish-dev-build
git add "${package_files[@]}"

GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
GIT_AUTHOR_NAME="$name" GIT_COMMITTER_NAME="$name" \
GIT_AUTHOR_EMAIL="$email" GIT_COMMITTER_EMAIL="$email" \
  git commit -m "$message"

git tag "$tag"
[ "$GITHUB_REF" != "refs/heads/main" ] || git tag -f "${package_name}/latest"
git push -f --tags

echo done


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [push, pull_request]

jobs:
  build:

    runs-on: ubuntu-22.04

    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: '22'
        cache: 'yarn'
    - run: yarn install --frozen-lockfile
    - run: yarn run playwright install --with-deps
    - run: yarn build

    - name: Set Chrome Version
      run: |
        CHROMEVER="$(chromedriver --version | cut -d' ' -f2)"
        echo "Actions ChromeDriver is $CHROMEVER"
        echo "CHROMEVER=${CHROMEVER}" >> $GITHUB_ENV

    - name: Lint
      run: yarn lint

    - name: Unit Test
      run: yarn test:unit

    - name: Chrome Test
      run: yarn test:browser --project=chrome

    - name: Firefox Test
      run: yarn test:browser --project=firefox

    - uses: actions/upload-artifact@v4
      with:
        name: turbo-dist
        path: dist/*


================================================
FILE: .github/workflows/dev-builds.yml
================================================
name: dev-builds

on:
  workflow_dispatch:
  push:
    branches:
      - main
      - 'builds/**'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '22'
          cache: 'yarn'

      - run: yarn install --frozen-lockfile
      - run: yarn build

      - name: Publish dev build
        run: .github/scripts/publish-dev-build '${{ secrets.DEV_BUILD_GITHUB_TOKEN }}'


================================================
FILE: .gitignore
================================================
/dist
/node_modules
/test-results
*.log
package-lock.json


================================================
FILE: .prettierignore
================================================
dist/
node_modules/


================================================
FILE: .prettierrc.json
================================================
{
  "singleQuote": false,
  "printWidth": 120,
  "semi": false,
  "trailingComma" : "none"
}


================================================
FILE: .vscode/launch.json
================================================
{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Turbo: Debug browser tests",
      "cwd": "${workspaceFolder}",
      "port": 9229,
      "outputCapture": "std",
      "internalConsoleOptions": "openOnSessionStart",
      "runtimeExecutable": "yarn",
      "runtimeArgs": ["test"]
    }
  ]
}


================================================
FILE: .vscode/tasks.json
================================================
{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Turbo: Build dist directory",
      "type": "shell",
      "command": "yarn build",
      "group": {
        "kind": "build",
        "isDefault": true
      }
    },
    {
      "label": "Turbo: Run tests",
      "type": "shell",
      "dependsOn": "Turbo: Build dist directory",
      "command": "yarn test",
      "group": {
        "kind": "test",
        "isDefault": true
      }
    },
    {
      "label": "Turbo: Start dev server",
      "type": "shell",
      "dependsOn": "Turbo: Build dist directory",
      "command": "yarn start",
      "problemMatcher": []
    }
  ]
}


================================================
FILE: CHANGELOG.md
================================================
# Changelog

Please see [our GitHub "Releases" page](https://github.com/hotwired/turbo/releases).


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting one of the project maintainers listed below. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Project Maintainers

* Sam Stephenson <<sam@hey.com>>
* Javan Makhmali <<javan@javan.us>>

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: CONTRIBUTING.md
================================================
[![Version](https://img.shields.io/npm/v/@hotwired/turbo)](https://www.npmjs.com/package/@hotwired/turbo)
[![License](https://img.shields.io/github/license/hotwired/turbo)](https://github.com/hotwired/turbo)

# Contributing

Note that we have a [code of conduct](https://github.com/hotwired/turbo/blob/main/CODE_OF_CONDUCT.md). Please follow it in your interactions with this project.

## Sending a Pull Request

The core team is monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation.

Before submitting a pull request, please:

1. Fork the repository and create your branch.
2. Follow the setup instructions in this file.
3. If you’re fixing a bug or adding code that should be tested, add tests!
4. Ensure the test suite passes.

## Developing locally

First, clone the `hotwired/turbo` repository and install dependencies:

```bash
git clone https://github.com/hotwired/turbo.git
```

```bash
cd turbo
yarn install
```

Then create a branch for your changes:

```bash
git checkout -b <your_branch_name>
```

### Testing

Tests are run through `yarn` using [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/) with [Playwright](https://github.com/microsoft/playwright) for browser testing. Browser and runtime configuration can be found in [`web-test-runner.config.mjs`](./web-test-runner.config.mjs) and [`playwright.config.js`](./playwright.config.js).

To begin testing, install the browser drivers:

```bash
yarn playwright install --with-deps
```

Then build the source. Because tests are run against the compiled source (and are themselves compiled) be sure to run `yarn build` prior to testing. Alternatively, you can run `yarn watch` to build and watch for changes.

```bash
yarn build
```

### Running the test suite

The test suite can be run with `yarn`, using the test commands defined in [`package.json`](./package.json). To run all tests in all configured browsers:

```bash
yarn test
```

To run just the unit or browser tests:

```bash
yarn test:unit
yarn test:browser
```

By default, tests are run in "headless" mode against all configured browsers (currently `chrome` and `firefox`). Use the `--headed` flag to run in normal mode. Use the `--project` flag to run against a particular browser.

```bash
yarn test:browser --project=firefox
yarn test:browser --project=chrome
yarn test:browser --project=chrome --headed
```

### Running a single test

To run a single test file, pass its path as an argument. To run a particular test case, append its starting line number after a colon.

```bash
yarn test:browser src/tests/functional/drive_tests.js
yarn test:browser src/tests/functional/drive_tests.js:11
yarn test:browser src/tests/functional/drive_tests.js:11 --project=chrome
```

### Running the local web server

Because tests are running headless in browsers, debugging can be difficult. Sometimes the simplest thing to do is load the test fixtures into the browser and navigate manually. To make this easier, a local web server is included.

To run the web server, ensure the source is built and start the server with `yarn`:

```bash
yarn build
yarn start
```

The web server is available on port 9000, serving from the project root. Fixture files are accessible by path. For example, the file at `src/tests/fixtures/rendering.html` will be accessible at <http://localhost:9000/src/tests/fixtures/rendering.html>.


================================================
FILE: MIT-LICENSE
================================================
Copyright (c) 37signals

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# Turbo

Turbo uses complementary techniques to dramatically reduce the amount of custom JavaScript that most web applications will need to write:

* Turbo Drive accelerates links and form submissions by negating the need for full page reloads.
* Turbo Frames decompose pages into independent contexts, which scope navigation and can be lazily loaded.
* Turbo Streams deliver page changes over WebSocket or in response to form submissions using just HTML and a set of CRUD-like actions.
* Turbo Native lets your majestic monolith form the center of your native iOS and Android apps, with seamless transitions between web and native sections.

It's all done by sending HTML over the wire. And for those instances when that's not enough, you can reach for the other side of Hotwire, and finish the job with [Stimulus](https://github.com/hotwired/stimulus).

Read more on [turbo.hotwired.dev](https://turbo.hotwired.dev).

## Contributing

Please read [CONTRIBUTING.md](./CONTRIBUTING.md).

© 2026 37signals LLC.


================================================
FILE: package.json
================================================
{
  "name": "@hotwired/turbo",
  "version": "8.0.23",
  "description": "The speed of a single-page web application without having to write any JavaScript",
  "module": "dist/turbo.es2017-esm.js",
  "main": "dist/turbo.es2017-umd.js",
  "files": [
    "dist/*.js",
    "dist/*.js.map"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/hotwired/turbo.git"
  },
  "keywords": [
    "hotwire",
    "turbo",
    "browser",
    "pushstate"
  ],
  "author": "37signals LLC",
  "contributors": [
    "Jeffrey Hardy <jeff@basecamp.com>",
    "Javan Makhmali <javan@javan.us>",
    "Sam Stephenson <sstephenson@gmail.com>"
  ],
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/hotwired/turbo/issues"
  },
  "homepage": "https://turbo.hotwired.dev",
  "publishConfig": {
    "access": "public"
  },
  "devDependencies": {
    "@open-wc/testing": "^3.1.7",
    "@playwright/test": "~1.51.1",
    "@rollup/plugin-node-resolve": "13.1.3",
    "@web/dev-server-esbuild": "^0.3.3",
    "@web/test-runner": "^0.15.0",
    "@web/test-runner-playwright": "^0.9.0",
    "arg": "^5.0.1",
    "body-parser": "^1.20.1",
    "eslint": "^8.13.0",
    "express": "^4.18.2",
    "idiomorph": "~0.7.4",
    "multer": "^2.0.2",
    "rollup": "^2.35.1"
  },
  "scripts": {
    "clean": "rm -fr dist",
    "clean:win": "rmdir /s /q dist",
    "build": "yarn install && rollup -c",
    "build:win": "yarn install && rollup -c",
    "watch": "rollup -wc",
    "start": "node src/tests/server.mjs",
    "test": "yarn test:unit && yarn test:browser",
    "test:browser": "playwright test",
    "test:unit": "NODE_OPTIONS=--inspect web-test-runner",
    "test:unit:win": "SET NODE_OPTIONS=--inspect & web-test-runner",
    "release": "yarn build && yarn publish",
    "lint": "eslint . --ext .js"
  },
  "engines": {
    "node": ">= 18"
  }
}


================================================
FILE: playwright.config.js
================================================
import { devices } from "@playwright/test"

const config = {
  projects: [
    {
      name: "chrome",
      use: {
        ...devices["Desktop Chrome"],
        contextOptions: {
          timeout: 10000
        },
        hasTouch: true
      }
    },
    {
      name: "firefox",
      use: {
        ...devices["Desktop Firefox"],
        contextOptions: {
          timeout: 10000
        },
        hasTouch: true
      }
    }
  ],
  timeout: 10000,
  browserStartTimeout: 10000,
  retries: 2,
  testDir: "./src/tests/",
  testMatch: /(functional|integration)\/.*_tests\.js/,
  webServer: {
    command: "yarn start",
    url: "http://localhost:9000/src/tests/fixtures/test.js",
    timeout: 10000,
    // eslint-disable-next-line no-undef
    reuseExistingServer: !process.env.CI
  },
  use: {
    baseURL: "http://localhost:9000/"
  }
}

export default config


================================================
FILE: rollup.config.js
================================================
import resolve from "@rollup/plugin-node-resolve"

import { version } from "./package.json"
const year = new Date().getFullYear()
const banner = `/*!\nTurbo ${version}\nCopyright © ${year} 37signals LLC\n */`

export default [
  {
    input: "src/index.js",
    output: [
      {
        name: "Turbo",
        file: "dist/turbo.es2017-umd.js",
        format: "umd",
        banner
      },
      {
        file: "dist/turbo.es2017-esm.js",
        format: "esm",
        banner
      }
    ],
    plugins: [resolve()],
    watch: {
      include: "src/**"
    }
  }
]


================================================
FILE: src/core/bardo.js
================================================
export class Bardo {
  static async preservingPermanentElements(delegate, permanentElementMap, callback) {
    const bardo = new this(delegate, permanentElementMap)
    bardo.enter()
    await callback()
    bardo.leave()
  }

  constructor(delegate, permanentElementMap) {
    this.delegate = delegate
    this.permanentElementMap = permanentElementMap
  }

  enter() {
    for (const id in this.permanentElementMap) {
      const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id]
      this.delegate.enteringBardo(currentPermanentElement, newPermanentElement)
      this.replaceNewPermanentElementWithPlaceholder(newPermanentElement)
    }
  }

  leave() {
    for (const id in this.permanentElementMap) {
      const [currentPermanentElement] = this.permanentElementMap[id]
      this.replaceCurrentPermanentElementWithClone(currentPermanentElement)
      this.replacePlaceholderWithPermanentElement(currentPermanentElement)
      this.delegate.leavingBardo(currentPermanentElement)
    }
  }

  replaceNewPermanentElementWithPlaceholder(permanentElement) {
    const placeholder = createPlaceholderForPermanentElement(permanentElement)
    permanentElement.replaceWith(placeholder)
  }

  replaceCurrentPermanentElementWithClone(permanentElement) {
    const clone = permanentElement.cloneNode(true)
    permanentElement.replaceWith(clone)
  }

  replacePlaceholderWithPermanentElement(permanentElement) {
    const placeholder = this.getPlaceholderById(permanentElement.id)
    placeholder?.replaceWith(permanentElement)
  }

  getPlaceholderById(id) {
    return this.placeholders.find((element) => element.content == id)
  }

  get placeholders() {
    return [...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")]
  }
}

function createPlaceholderForPermanentElement(permanentElement) {
  const element = document.createElement("meta")
  element.setAttribute("name", "turbo-permanent-placeholder")
  element.setAttribute("content", permanentElement.id)
  return element
}


================================================
FILE: src/core/cache.js
================================================
import { setMetaContent } from "../util"

export class Cache {
  constructor(session) {
    this.session = session
  }

  clear() {
    this.session.clearCache()
  }

  resetCacheControl() {
    this.#setCacheControl("")
  }

  exemptPageFromCache() {
    this.#setCacheControl("no-cache")
  }

  exemptPageFromPreview() {
    this.#setCacheControl("no-preview")
  }

  #setCacheControl(value) {
    setMetaContent("turbo-cache-control", value)
  }
}


================================================
FILE: src/core/config/drive.js
================================================
export const drive = {
  enabled: true,
  progressBarDelay: 500,
  unvisitableExtensions: new Set(
    [
      ".7z", ".aac", ".apk", ".avi", ".bmp", ".bz2", ".css", ".csv", ".deb", ".dmg", ".doc",
      ".docx", ".exe", ".gif", ".gz", ".heic", ".heif", ".ico", ".iso", ".jpeg", ".jpg",
      ".js", ".json", ".m4a", ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".mpg", ".msi",
      ".ogg", ".ogv", ".pdf", ".pkg", ".png", ".ppt", ".pptx", ".rar", ".rtf",
      ".svg", ".tar", ".tif", ".tiff", ".txt", ".wav", ".webm", ".webp", ".wma", ".wmv",
      ".xls", ".xlsx", ".xml", ".zip"
    ]
  )
}


================================================
FILE: src/core/config/forms.js
================================================
import { cancelEvent } from "../../util"

const submitter = {
  "aria-disabled": {
    beforeSubmit: submitter => {
      submitter.setAttribute("aria-disabled", "true")
      submitter.addEventListener("click", cancelEvent)
    },

    afterSubmit: submitter => {
      submitter.removeAttribute("aria-disabled")
      submitter.removeEventListener("click", cancelEvent)
    }
  },

  "disabled": {
    beforeSubmit: submitter => submitter.disabled = true,
    afterSubmit: submitter => submitter.disabled = false
  }
}

class Config {
  #submitter = null

  constructor(config) {
    Object.assign(this, config)
  }

  get submitter() {
    return this.#submitter
  }

  set submitter(value) {
    this.#submitter = submitter[value] || value
  }
}

export const forms = new Config({
  mode: "on",
  submitter: "disabled"
})


================================================
FILE: src/core/config/index.js
================================================
import { drive } from "./drive"
import { forms } from "./forms"

export const config = {
  drive,
  forms
}


================================================
FILE: src/core/drive/error_renderer.js
================================================
import { activateScriptElement } from "../../util"
import { Renderer } from "../renderer"

export class ErrorRenderer extends Renderer {
  static renderElement(currentElement, newElement) {
    const { documentElement, body } = document

    documentElement.replaceChild(newElement, body)
  }

  async render() {
    this.replaceHeadAndBody()
    this.activateScriptElements()
  }

  replaceHeadAndBody() {
    const { documentElement, head } = document
    documentElement.replaceChild(this.newHead, head)
    this.renderElement(this.currentElement, this.newElement)
  }

  activateScriptElements() {
    for (const replaceableElement of this.scriptElements) {
      const parentNode = replaceableElement.parentNode
      if (parentNode) {
        const element = activateScriptElement(replaceableElement)
        parentNode.replaceChild(element, replaceableElement)
      }
    }
  }

  get newHead() {
    return this.newSnapshot.headSnapshot.element
  }

  get scriptElements() {
    return document.documentElement.querySelectorAll("script")
  }
}


================================================
FILE: src/core/drive/form_submission.js
================================================
import { FetchRequest, FetchMethod, fetchMethodFromString, fetchEnctypeFromString, isSafe } from "../../http/fetch_request"
import { expandURL } from "../url"
import { clearBusyState, dispatch, getAttribute, getMetaContent, hasAttribute, markAsBusy } from "../../util"
import { StreamMessage } from "../streams/stream_message"
import { prefetchCache } from "./prefetch_cache"
import { config } from "../config"

export const FormSubmissionState = {
  initialized: "initialized",
  requesting: "requesting",
  waiting: "waiting",
  receiving: "receiving",
  stopping: "stopping",
  stopped: "stopped"
}

export const FormEnctype = {
  urlEncoded: "application/x-www-form-urlencoded",
  multipart: "multipart/form-data",
  plain: "text/plain"
}

export class FormSubmission {
  state = FormSubmissionState.initialized

  static confirmMethod(message) {
    return Promise.resolve(confirm(message))
  }

  constructor(delegate, formElement, submitter, mustRedirect = false) {
    const method = getMethod(formElement, submitter)
    const action = getAction(getFormAction(formElement, submitter), method)
    const body = buildFormData(formElement, submitter)
    const enctype = getEnctype(formElement, submitter)

    this.delegate = delegate
    this.formElement = formElement
    this.submitter = submitter
    this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype)
    this.mustRedirect = mustRedirect
  }

  get method() {
    return this.fetchRequest.method
  }

  set method(value) {
    this.fetchRequest.method = value
  }

  get action() {
    return this.fetchRequest.url.toString()
  }

  set action(value) {
    this.fetchRequest.url = expandURL(value)
  }

  get body() {
    return this.fetchRequest.body
  }

  get enctype() {
    return this.fetchRequest.enctype
  }

  get isSafe() {
    return this.fetchRequest.isSafe
  }

  get location() {
    return this.fetchRequest.url
  }

  // The submission process

  async start() {
    const { initialized, requesting } = FormSubmissionState
    const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement)

    if (typeof confirmationMessage === "string") {
      const confirmMethod = typeof config.forms.confirm === "function" ?
        config.forms.confirm :
        FormSubmission.confirmMethod

      const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter)
      if (!answer) {
        return
      }
    }

    if (this.state == initialized) {
      this.state = requesting
      return this.fetchRequest.perform()
    }
  }

  stop() {
    const { stopping, stopped } = FormSubmissionState
    if (this.state != stopping && this.state != stopped) {
      this.state = stopping
      this.fetchRequest.cancel()
      return true
    }
  }

  // Fetch request delegate

  prepareRequest(request) {
    if (!request.isSafe) {
      const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token")
      if (token) {
        request.headers["X-CSRF-Token"] = token
      }
    }

    if (this.requestAcceptsTurboStreamResponse(request)) {
      request.acceptResponseType(StreamMessage.contentType)
    }
  }

  requestStarted(_request) {
    this.state = FormSubmissionState.waiting
    if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter)
    this.setSubmitsWith()
    markAsBusy(this.formElement)
    dispatch("turbo:submit-start", {
      target: this.formElement,
      detail: { formSubmission: this }
    })
    this.delegate.formSubmissionStarted(this)
  }

  requestPreventedHandlingResponse(request, response) {
    prefetchCache.clear()

    this.result = { success: response.succeeded, fetchResponse: response }
  }

  requestSucceededWithResponse(request, response) {
    if (response.clientError || response.serverError) {
      this.delegate.formSubmissionFailedWithResponse(this, response)
      return
    }

    prefetchCache.clear()

    if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
      const error = new Error("Form responses must redirect to another location")
      this.delegate.formSubmissionErrored(this, error)
    } else {
      this.state = FormSubmissionState.receiving
      this.result = { success: true, fetchResponse: response }
      this.delegate.formSubmissionSucceededWithResponse(this, response)
    }
  }

  requestFailedWithResponse(request, response) {
    this.result = { success: false, fetchResponse: response }
    this.delegate.formSubmissionFailedWithResponse(this, response)
  }

  requestErrored(request, error) {
    this.result = { success: false, error }
    this.delegate.formSubmissionErrored(this, error)
  }

  requestFinished(_request) {
    this.state = FormSubmissionState.stopped
    if (this.submitter) config.forms.submitter.afterSubmit(this.submitter)
    this.resetSubmitterText()
    clearBusyState(this.formElement)
    dispatch("turbo:submit-end", {
      target: this.formElement,
      detail: { formSubmission: this, ...this.result }
    })
    this.delegate.formSubmissionFinished(this)
  }

  // Private

  setSubmitsWith() {
    if (!this.submitter || !this.submitsWith) return

    if (this.submitter.matches("button")) {
      this.originalSubmitText = this.submitter.innerHTML
      this.submitter.innerHTML = this.submitsWith
    } else if (this.submitter.matches("input")) {
      const input = this.submitter
      this.originalSubmitText = input.value
      input.value = this.submitsWith
    }
  }

  resetSubmitterText() {
    if (!this.submitter || !this.originalSubmitText) return

    if (this.submitter.matches("button")) {
      this.submitter.innerHTML = this.originalSubmitText
    } else if (this.submitter.matches("input")) {
      const input = this.submitter
      input.value = this.originalSubmitText
    }
  }

  requestMustRedirect(request) {
    return !request.isSafe && this.mustRedirect
  }

  requestAcceptsTurboStreamResponse(request) {
    return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement)
  }

  get submitsWith() {
    return this.submitter?.getAttribute("data-turbo-submits-with")
  }
}

function buildFormData(formElement, submitter) {
  const formData = new FormData(formElement)
  const name = submitter?.getAttribute("name")
  const value = submitter?.getAttribute("value")

  if (name) {
    formData.append(name, value || "")
  }

  return formData
}

function getCookieValue(cookieName) {
  if (cookieName != null) {
    const cookies = document.cookie ? document.cookie.split("; ") : []
    const cookie = cookies.find((cookie) => cookie.startsWith(cookieName))
    if (cookie) {
      const value = cookie.split("=").slice(1).join("=")
      return value ? decodeURIComponent(value) : undefined
    }
  }
}

function responseSucceededWithoutRedirect(response) {
  return response.statusCode == 200 && !response.redirected
}

function getFormAction(formElement, submitter) {
  const formElementAction = typeof formElement.action === "string" ? formElement.action : null

  if (submitter?.hasAttribute("formaction")) {
    return submitter.getAttribute("formaction") || ""
  } else {
    return formElement.getAttribute("action") || formElementAction || ""
  }
}

function getAction(formAction, fetchMethod) {
  const action = expandURL(formAction)

  if (isSafe(fetchMethod)) {
    action.search = ""
  }

  return action
}

function getMethod(formElement, submitter) {
  const method = submitter?.getAttribute("formmethod") || formElement.getAttribute("method") || ""
  return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get
}

function getEnctype(formElement, submitter) {
  return fetchEnctypeFromString(submitter?.getAttribute("formenctype") || formElement.enctype)
}


================================================
FILE: src/core/drive/head_snapshot.js
================================================
import { elementIsStylesheet } from "../../util"
import { Snapshot } from "../snapshot"

export class HeadSnapshot extends Snapshot {
  detailsByOuterHTML = this.children
    .filter((element) => !elementIsNoscript(element))
    .map((element) => elementWithoutNonce(element))
    .reduce((result, element) => {
      const { outerHTML } = element
      const details =
        outerHTML in result
          ? result[outerHTML]
          : {
              type: elementType(element),
              tracked: elementIsTracked(element),
              elements: []
            }
      return {
        ...result,
        [outerHTML]: {
          ...details,
          elements: [...details.elements, element]
        }
      }
    }, {})

  get trackedElementSignature() {
    return Object.keys(this.detailsByOuterHTML)
      .filter((outerHTML) => this.detailsByOuterHTML[outerHTML].tracked)
      .join("")
  }

  getScriptElementsNotInSnapshot(snapshot) {
    return this.getElementsMatchingTypeNotInSnapshot("script", snapshot)
  }

  getStylesheetElementsNotInSnapshot(snapshot) {
    return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot)
  }

  getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
    return Object.keys(this.detailsByOuterHTML)
      .filter((outerHTML) => !(outerHTML in snapshot.detailsByOuterHTML))
      .map((outerHTML) => this.detailsByOuterHTML[outerHTML])
      .filter(({ type }) => type == matchedType)
      .map(({ elements: [element] }) => element)
  }

  get provisionalElements() {
    return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
      const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML]
      if (type == null && !tracked) {
        return [...result, ...elements]
      } else if (elements.length > 1) {
        return [...result, ...elements.slice(1)]
      } else {
        return result
      }
    }, [])
  }

  getMetaValue(name) {
    const element = this.findMetaElementByName(name)
    return element ? element.getAttribute("content") : null
  }

  findMetaElementByName(name) {
    return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
      const {
        elements: [element]
      } = this.detailsByOuterHTML[outerHTML]
      return elementIsMetaElementWithName(element, name) ? element : result
    }, undefined | undefined)
  }
}

function elementType(element) {
  if (elementIsScript(element)) {
    return "script"
  } else if (elementIsStylesheet(element)) {
    return "stylesheet"
  }
}

function elementIsTracked(element) {
  return element.getAttribute("data-turbo-track") == "reload"
}

function elementIsScript(element) {
  const tagName = element.localName
  return tagName == "script"
}

function elementIsNoscript(element) {
  const tagName = element.localName
  return tagName == "noscript"
}

function elementIsMetaElementWithName(element, name) {
  const tagName = element.localName
  return tagName == "meta" && element.getAttribute("name") == name
}

function elementWithoutNonce(element) {
  if (element.hasAttribute("nonce")) {
    element.setAttribute("nonce", "")
  }

  return element
}


================================================
FILE: src/core/drive/history.js
================================================
import { uuid } from "../../util"

export class History {
  location
  restorationIdentifier = uuid()
  restorationData = {}
  started = false
  currentIndex = 0

  constructor(delegate) {
    this.delegate = delegate
  }

  start() {
    if (!this.started) {
      addEventListener("popstate", this.onPopState, false)
      this.currentIndex = history.state?.turbo?.restorationIndex || 0
      this.started = true
      this.replace(new URL(window.location.href))
    }
  }

  stop() {
    if (this.started) {
      removeEventListener("popstate", this.onPopState, false)
      this.started = false
    }
  }

  push(location, restorationIdentifier) {
    this.update(history.pushState, location, restorationIdentifier)
  }

  replace(location, restorationIdentifier) {
    this.update(history.replaceState, location, restorationIdentifier)
  }

  update(method, location, restorationIdentifier = uuid()) {
    if (method === history.pushState) ++this.currentIndex

    const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } }
    method.call(history, state, "", location.href)
    this.location = location
    this.restorationIdentifier = restorationIdentifier
  }

  // Restoration data

  getRestorationDataForIdentifier(restorationIdentifier) {
    return this.restorationData[restorationIdentifier] || {}
  }

  updateRestorationData(additionalData) {
    const { restorationIdentifier } = this
    const restorationData = this.restorationData[restorationIdentifier]
    this.restorationData[restorationIdentifier] = {
      ...restorationData,
      ...additionalData
    }
  }

  // Scroll restoration

  assumeControlOfScrollRestoration() {
    if (!this.previousScrollRestoration) {
      this.previousScrollRestoration = history.scrollRestoration ?? "auto"
      history.scrollRestoration = "manual"
    }
  }

  relinquishControlOfScrollRestoration() {
    if (this.previousScrollRestoration) {
      history.scrollRestoration = this.previousScrollRestoration
      delete this.previousScrollRestoration
    }
  }

  // Event handlers

  onPopState = (event) => {
    const { turbo } = event.state || {}
    this.location = new URL(window.location.href)

    if (turbo) {
      const { restorationIdentifier, restorationIndex } = turbo
      this.restorationIdentifier = restorationIdentifier
      const direction = restorationIndex > this.currentIndex ? "forward" : "back"
      this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction)
      this.currentIndex = restorationIndex
    } else {
      this.currentIndex++
      this.delegate.historyPoppedWithEmptyState(this.location)
    }
  }
}


================================================
FILE: src/core/drive/limited_set.js
================================================
export class LimitedSet extends Set {
  constructor(maxSize) {
    super()
    this.maxSize = maxSize
  }

  add(value) {
    if (this.size >= this.maxSize) {
      const iterator = this.values()
      const oldestValue = iterator.next().value
      this.delete(oldestValue)
    }
    super.add(value)
  }
}


================================================
FILE: src/core/drive/morphing_page_renderer.js
================================================
import { PageRenderer } from "./page_renderer"
import { dispatch } from "../../util"
import { morphElements, shouldRefreshFrameWithMorphing, closestFrameReloadableWithMorphing } from "../morphing"

export class MorphingPageRenderer extends PageRenderer {
  static renderElement(currentElement, newElement) {
    morphElements(currentElement, newElement, {
      callbacks: {
        beforeNodeMorphed: (node, newNode) => {
          if (
            shouldRefreshFrameWithMorphing(node, newNode) &&
              !closestFrameReloadableWithMorphing(node)
          ) {
            node.reload()
            return false
          }
          return true
        }
      }
    })

    dispatch("turbo:morph", { detail: { currentElement, newElement } })
  }

  async preservingPermanentElements(callback) {
    return await callback()
  }

  get renderMethod() {
    return "morph"
  }

  get shouldAutofocus() {
    return false
  }
}



================================================
FILE: src/core/drive/navigator.js
================================================
import { getVisitAction } from "../../util"
import { FormSubmission } from "./form_submission"
import { expandURL } from "../url"
import { Visit } from "./visit"
import { PageSnapshot } from "./page_snapshot"

export class Navigator {
  constructor(delegate) {
    this.delegate = delegate
  }

  proposeVisit(location, options = {}) {
    if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
      this.delegate.visitProposedToLocation(location, options)
    }
  }

  startVisit(locatable, restorationIdentifier, options = {}) {
    this.stop()
    this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
      referrer: this.location,
      ...options
    })
    this.currentVisit.start()
  }

  submitForm(form, submitter) {
    this.stop()
    this.formSubmission = new FormSubmission(this, form, submitter, true)

    this.formSubmission.start()
  }

  stop() {
    if (this.formSubmission) {
      this.formSubmission.stop()
      delete this.formSubmission
    }

    if (this.currentVisit) {
      this.currentVisit.cancel()
      delete this.currentVisit
    }
  }

  get adapter() {
    return this.delegate.adapter
  }

  get view() {
    return this.delegate.view
  }

  get rootLocation() {
    return this.view.snapshot.rootLocation
  }

  get history() {
    return this.delegate.history
  }

  // Form submission delegate

  formSubmissionStarted(formSubmission) {
    // Not all adapters implement formSubmissionStarted
    if (typeof this.adapter.formSubmissionStarted === "function") {
      this.adapter.formSubmissionStarted(formSubmission)
    }
  }

  async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
    if (formSubmission == this.formSubmission) {
      const responseHTML = await fetchResponse.responseHTML
      if (responseHTML) {
        const shouldCacheSnapshot = formSubmission.isSafe
        if (!shouldCacheSnapshot) {
          this.view.clearSnapshotCache()
        }

        const { statusCode, redirected } = fetchResponse
        const action = this.#getActionForFormSubmission(formSubmission, fetchResponse)
        const visitOptions = {
          action,
          shouldCacheSnapshot,
          response: { statusCode, responseHTML, redirected }
        }
        this.proposeVisit(fetchResponse.location, visitOptions)
      }
    }
  }

  async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
    const responseHTML = await fetchResponse.responseHTML

    if (responseHTML) {
      const snapshot = PageSnapshot.fromHTMLString(responseHTML)
      if (fetchResponse.serverError) {
        await this.view.renderError(snapshot, this.currentVisit)
      } else {
        await this.view.renderPage(snapshot, false, true, this.currentVisit)
      }
      if (snapshot.refreshScroll !== "preserve") {
        this.view.scrollToTop()
      }
      this.view.clearSnapshotCache()
    }
  }

  formSubmissionErrored(formSubmission, error) {
    console.error(error)
  }

  formSubmissionFinished(formSubmission) {
    // Not all adapters implement formSubmissionFinished
    if (typeof this.adapter.formSubmissionFinished === "function") {
      this.adapter.formSubmissionFinished(formSubmission)
    }
  }

  // Link prefetching

  linkPrefetchingIsEnabledForLocation(location) {
    // Not all adapters implement linkPrefetchingIsEnabledForLocation
    if (typeof this.adapter.linkPrefetchingIsEnabledForLocation === "function") {
      return this.adapter.linkPrefetchingIsEnabledForLocation(location)
    }

    return true
  }

  // Visit delegate

  visitStarted(visit) {
    this.delegate.visitStarted(visit)
  }

  visitCompleted(visit) {
    this.delegate.visitCompleted(visit)
    delete this.currentVisit
  }

  // Same-page links are no longer handled with a Visit.
  // This method is still needed for Turbo Native adapters.
  locationWithActionIsSamePage(location, action) {
    return false
  }

  // Visits

  get location() {
    return this.history.location
  }

  get restorationIdentifier() {
    return this.history.restorationIdentifier
  }

  #getActionForFormSubmission(formSubmission, fetchResponse) {
    const { submitter, formElement } = formSubmission
    return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse)
  }

  #getDefaultAction(fetchResponse) {
    const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href
    return sameLocationRedirect ? "replace" : "advance"
  }
}


================================================
FILE: src/core/drive/page_renderer.js
================================================
import { activateScriptElement, elementIsStylesheet, waitForLoad } from "../../util"
import { Renderer } from "../renderer"

export class PageRenderer extends Renderer {
  static renderElement(currentElement, newElement) {
    if (document.body && newElement instanceof HTMLBodyElement) {
      document.body.replaceWith(newElement)
    } else {
      document.documentElement.appendChild(newElement)
    }
  }

  get shouldRender() {
    return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical
  }

  get reloadReason() {
    if (!this.newSnapshot.isVisitable) {
      return {
        reason: "turbo_visit_control_is_reload"
      }
    }

    if (!this.trackedElementsAreIdentical) {
      return {
        reason: "tracked_element_mismatch"
      }
    }
  }

  async prepareToRender() {
    this.#setLanguage()
    await this.mergeHead()
  }

  async render() {
    if (this.willRender) {
      await this.replaceBody()
    }
  }

  finishRendering() {
    super.finishRendering()
    if (!this.isPreview) {
      this.focusFirstAutofocusableElement()
    }
  }

  get currentHeadSnapshot() {
    return this.currentSnapshot.headSnapshot
  }

  get newHeadSnapshot() {
    return this.newSnapshot.headSnapshot
  }

  get newElement() {
    return this.newSnapshot.element
  }

  #setLanguage() {
    const { documentElement } = this.currentSnapshot
    const { dir, lang } = this.newSnapshot

    if (lang) {
      documentElement.setAttribute("lang", lang)
    } else {
      documentElement.removeAttribute("lang")
    }
    if (dir) {
      documentElement.setAttribute("dir", dir)
    } else {
      documentElement.removeAttribute("dir")
    }
  }

  async mergeHead() {
    const mergedHeadElements = this.mergeProvisionalElements()
    const newStylesheetElements = this.copyNewHeadStylesheetElements()
    this.copyNewHeadScriptElements()

    await mergedHeadElements
    await newStylesheetElements

    if (this.willRender) {
      this.removeUnusedDynamicStylesheetElements()
    }
  }

  async replaceBody() {
    await this.preservingPermanentElements(async () => {
      this.activateNewBody()
      await this.assignNewBody()
    })
  }

  get trackedElementsAreIdentical() {
    return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature
  }

  async copyNewHeadStylesheetElements() {
    const loadingElements = []

    for (const element of this.newHeadStylesheetElements) {
      loadingElements.push(waitForLoad(element))

      document.head.appendChild(element)
    }

    await Promise.all(loadingElements)
  }

  copyNewHeadScriptElements() {
    for (const element of this.newHeadScriptElements) {
      document.head.appendChild(activateScriptElement(element))
    }
  }

  removeUnusedDynamicStylesheetElements() {
    for (const element of this.unusedDynamicStylesheetElements) {
      document.head.removeChild(element)
    }
  }

  async mergeProvisionalElements() {
    const newHeadElements = [...this.newHeadProvisionalElements]

    for (const element of this.currentHeadProvisionalElements) {
      if (!this.isCurrentElementInElementList(element, newHeadElements)) {
        document.head.removeChild(element)
      }
    }

    for (const element of newHeadElements) {
      document.head.appendChild(element)
    }
  }

  isCurrentElementInElementList(element, elementList) {
    for (const [index, newElement] of elementList.entries()) {
      // if title element...
      if (element.tagName == "TITLE") {
        if (newElement.tagName != "TITLE") {
          continue
        }
        if (element.innerHTML == newElement.innerHTML) {
          elementList.splice(index, 1)
          return true
        }
      }

      // if any other element...
      if (newElement.isEqualNode(element)) {
        elementList.splice(index, 1)
        return true
      }
    }

    return false
  }

  removeCurrentHeadProvisionalElements() {
    for (const element of this.currentHeadProvisionalElements) {
      document.head.removeChild(element)
    }
  }

  copyNewHeadProvisionalElements() {
    for (const element of this.newHeadProvisionalElements) {
      document.head.appendChild(element)
    }
  }

  activateNewBody() {
    document.adoptNode(this.newElement)
    this.deactivateNoscriptStylesheetElements()
    this.activateNewBodyScriptElements()
  }

  deactivateNoscriptStylesheetElements() {
    for (const noscriptElement of this.newElement.querySelectorAll("noscript")) {
      for (const child of [...noscriptElement.children]) {
        if (elementIsStylesheet(child)) {
          child.remove()
        }
      }
    }
  }

  activateNewBodyScriptElements() {
    for (const inertScriptElement of this.newBodyScriptElements) {
      const activatedScriptElement = activateScriptElement(inertScriptElement)
      inertScriptElement.replaceWith(activatedScriptElement)
    }
  }

  async assignNewBody() {
    await this.renderElement(this.currentElement, this.newElement)
  }

  get unusedDynamicStylesheetElements() {
    return this.oldHeadStylesheetElements.filter((element) => {
      return element.getAttribute("data-turbo-track") === "dynamic"
    })
  }

  get oldHeadStylesheetElements() {
    return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)
  }

  get newHeadStylesheetElements() {
    return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
  }

  get newHeadScriptElements() {
    return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot)
  }

  get currentHeadProvisionalElements() {
    return this.currentHeadSnapshot.provisionalElements
  }

  get newHeadProvisionalElements() {
    return this.newHeadSnapshot.provisionalElements
  }

  get newBodyScriptElements() {
    return this.newElement.querySelectorAll("script")
  }
}


================================================
FILE: src/core/drive/page_snapshot.js
================================================
import { elementIsStylesheet, parseHTMLDocument } from "../../util"
import { Snapshot } from "../snapshot"
import { expandURL } from "../url"
import { HeadSnapshot } from "./head_snapshot"

export class PageSnapshot extends Snapshot {
  static fromHTMLString(html = "") {
    return this.fromDocument(parseHTMLDocument(html))
  }

  static fromElement(element) {
    return this.fromDocument(element.ownerDocument)
  }

  static fromDocument({ documentElement, body, head }) {
    return new this(documentElement, body, new HeadSnapshot(head))
  }

  constructor(documentElement, body, headSnapshot) {
    super(body)
    this.documentElement = documentElement
    this.headSnapshot = headSnapshot
  }

  clone() {
    const clonedElement = this.element.cloneNode(true)

    const selectElements = this.element.querySelectorAll("select")
    const clonedSelectElements = clonedElement.querySelectorAll("select")

    for (const [index, source] of selectElements.entries()) {
      const clone = clonedSelectElements[index]
      for (const option of clone.selectedOptions) option.selected = false
      for (const option of source.selectedOptions) clone.options[option.index].selected = true
    }

    for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
      clonedPasswordInput.value = ""
    }

    for (const clonedNoscriptElement of clonedElement.querySelectorAll("noscript")) {
      for (const child of [...clonedNoscriptElement.children]) {
        if (elementIsStylesheet(child)) {
          child.remove()
        }
      }
    }

    return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)
  }

  get lang() {
    return this.documentElement.getAttribute("lang")
  }

  get dir() {
    return this.documentElement.getAttribute("dir")
  }

  get headElement() {
    return this.headSnapshot.element
  }

  get rootLocation() {
    const root = this.getSetting("root") ?? "/"
    return expandURL(root)
  }

  get cacheControlValue() {
    return this.getSetting("cache-control")
  }

  get isPreviewable() {
    return this.cacheControlValue != "no-preview"
  }

  get isCacheable() {
    return this.cacheControlValue != "no-cache"
  }

  get isVisitable() {
    return this.getSetting("visit-control") != "reload"
  }

  get prefersViewTransitions() {
    const viewTransitionEnabled = this.getSetting("view-transition") === "true" || this.headSnapshot.getMetaValue("view-transition") === "same-origin"
    return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches
  }

  get refreshMethod() {
    return this.getSetting("refresh-method")
  }

  get refreshScroll() {
    return this.getSetting("refresh-scroll")
  }

  // Private

  getSetting(name) {
    return this.headSnapshot.getMetaValue(`turbo-${name}`)
  }
}


================================================
FILE: src/core/drive/page_view.js
================================================
import { nextEventLoopTick } from "../../util"
import { View } from "../view"
import { ErrorRenderer } from "./error_renderer"
import { MorphingPageRenderer } from "./morphing_page_renderer"
import { PageRenderer } from "./page_renderer"
import { PageSnapshot } from "./page_snapshot"
import { SnapshotCache } from "./snapshot_cache"

export class PageView extends View {
  snapshotCache = new SnapshotCache(10)
  lastRenderedLocation = new URL(location.href)
  forceReloaded = false

  shouldTransitionTo(newSnapshot) {
    return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions
  }

  renderPage(snapshot, isPreview = false, willRender = true, visit) {
    const shouldMorphPage = this.isPageRefresh(visit) && (visit?.refresh?.method || this.snapshot.refreshMethod) === "morph"
    const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer

    const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender)

    if (!renderer.shouldRender) {
      this.forceReloaded = true
    } else {
      visit?.changeHistory()
    }

    return this.render(renderer)
  }

  renderError(snapshot, visit) {
    visit?.changeHistory()
    const renderer = new ErrorRenderer(this.snapshot, snapshot, false)
    return this.render(renderer)
  }

  clearSnapshotCache() {
    this.snapshotCache.clear()
  }

  async cacheSnapshot(snapshot = this.snapshot) {
    if (snapshot.isCacheable) {
      this.delegate.viewWillCacheSnapshot()
      const { lastRenderedLocation: location } = this
      await nextEventLoopTick()
      const cachedSnapshot = snapshot.clone()
      this.snapshotCache.put(location, cachedSnapshot)
      return cachedSnapshot
    }
  }

  getCachedSnapshotForLocation(location) {
    return this.snapshotCache.get(location)
  }

  isPageRefresh(visit) {
    return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
  }

  shouldPreserveScrollPosition(visit) {
    return this.isPageRefresh(visit) && (visit?.refresh?.scroll || this.snapshot.refreshScroll) === "preserve"
  }

  get snapshot() {
    return PageSnapshot.fromElement(this.element)
  }
}


================================================
FILE: src/core/drive/prefetch_cache.js
================================================
import { LRUCache } from "../lru_cache"
import { toCacheKey } from "../url"

const PREFETCH_DELAY = 100

class PrefetchCache extends LRUCache {
  #prefetchTimeout = null
  #maxAges = {}

  constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
    super(size, toCacheKey)
    this.prefetchDelay = prefetchDelay
  }

  putLater(url, request, ttl) {
    this.#prefetchTimeout = setTimeout(() => {
      request.perform()
      this.put(url, request, ttl)
      this.#prefetchTimeout = null
    }, this.prefetchDelay)
  }

  put(url, request, ttl = cacheTtl) {
    super.put(url, request)
    this.#maxAges[toCacheKey(url)] = new Date(new Date().getTime() + ttl)
  }

  clear() {
    super.clear()
    if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout)
  }

  evict(key) {
    super.evict(key)
    delete this.#maxAges[key]
  }

  has(key) {
    if (super.has(key)) {
      const maxAge = this.#maxAges[toCacheKey(key)]

      return maxAge && maxAge > Date.now()
    } else {
      return false
    }
  }
}

export const cacheTtl = 10 * 1000
export const prefetchCache = new PrefetchCache()


================================================
FILE: src/core/drive/preloader.js
================================================
import { PageSnapshot } from "./page_snapshot"
import { FetchMethod, FetchRequest } from "../../http/fetch_request"

export class Preloader {
  selector = "a[data-turbo-preload]"

  constructor(delegate, snapshotCache) {
    this.delegate = delegate
    this.snapshotCache = snapshotCache
  }

  start() {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", this.#preloadAll)
    } else {
      this.preloadOnLoadLinksForView(document.body)
    }
  }

  stop() {
    document.removeEventListener("DOMContentLoaded", this.#preloadAll)
  }

  preloadOnLoadLinksForView(element) {
    for (const link of element.querySelectorAll(this.selector)) {
      if (this.delegate.shouldPreloadLink(link)) {
        this.preloadURL(link)
      }
    }
  }

  async preloadURL(link) {
    const location = new URL(link.href)

    if (this.snapshotCache.has(location)) {
      return
    }

    const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link)
    await fetchRequest.perform()
  }

  // Fetch request delegate

  prepareRequest(fetchRequest) {
    fetchRequest.headers["X-Sec-Purpose"] = "prefetch"
  }

  async requestSucceededWithResponse(fetchRequest, fetchResponse) {
    try {
      const responseHTML = await fetchResponse.responseHTML
      const snapshot = PageSnapshot.fromHTMLString(responseHTML)

      this.snapshotCache.put(fetchRequest.url, snapshot)
    } catch (_) {
      // If we cannot preload that is ok!
    }
  }

  requestStarted(fetchRequest) {}

  requestErrored(fetchRequest) {}

  requestFinished(fetchRequest) {}

  requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}

  requestFailedWithResponse(fetchRequest, fetchResponse) {}

  #preloadAll = () => {
    this.preloadOnLoadLinksForView(document.body)
  }
}


================================================
FILE: src/core/drive/progress_bar.js
================================================
import { unindent, getCspNonce } from "../../util"

export const ProgressBarID = "turbo-progress-bar"

export class ProgressBar {
  static animationDuration = 300 /*ms*/

  static get defaultCSS() {
    return unindent`
      .turbo-progress-bar {
        position: fixed;
        display: block;
        top: 0;
        left: 0;
        height: 3px;
        background: #0076ff;
        z-index: 2147483647;
        transition:
          width ${ProgressBar.animationDuration}ms ease-out,
          opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
        transform: translate3d(0, 0, 0);
      }
    `
  }

  hiding = false
  value = 0
  visible = false

  constructor() {
    this.stylesheetElement = this.createStylesheetElement()
    this.progressElement = this.createProgressElement()
    this.installStylesheetElement()
    this.setValue(0)
  }

  show() {
    if (!this.visible) {
      this.visible = true
      this.installProgressElement()
      this.startTrickling()
    }
  }

  hide() {
    if (this.visible && !this.hiding) {
      this.hiding = true
      this.fadeProgressElement(() => {
        this.uninstallProgressElement()
        this.stopTrickling()
        this.visible = false
        this.hiding = false
      })
    }
  }

  setValue(value) {
    this.value = value
    this.refresh()
  }

  // Private

  installStylesheetElement() {
    document.head.insertBefore(this.stylesheetElement, document.head.firstChild)
  }

  installProgressElement() {
    this.progressElement.style.width = "0"
    this.progressElement.style.opacity = "1"
    document.documentElement.insertBefore(this.progressElement, document.body)
    this.refresh()
  }

  fadeProgressElement(callback) {
    this.progressElement.style.opacity = "0"
    setTimeout(callback, ProgressBar.animationDuration * 1.5)
  }

  uninstallProgressElement() {
    if (this.progressElement.parentNode) {
      document.documentElement.removeChild(this.progressElement)
    }
  }

  startTrickling() {
    if (!this.trickleInterval) {
      this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration)
    }
  }

  stopTrickling() {
    window.clearInterval(this.trickleInterval)
    delete this.trickleInterval
  }

  trickle = () => {
    this.setValue(this.value + Math.random() / 100)
  }

  refresh() {
    requestAnimationFrame(() => {
      this.progressElement.style.width = `${10 + this.value * 90}%`
    })
  }

  createStylesheetElement() {
    const element = document.createElement("style")
    element.type = "text/css"
    element.textContent = ProgressBar.defaultCSS
    const cspNonce = getCspNonce()
    if (cspNonce) {
      element.nonce = cspNonce
    }
    return element
  }

  createProgressElement() {
    const element = document.createElement("div")
    element.className = "turbo-progress-bar"
    return element
  }
}


================================================
FILE: src/core/drive/snapshot_cache.js
================================================
import { toCacheKey } from "../url"
import { LRUCache } from "../lru_cache"

export class SnapshotCache extends LRUCache {
  constructor(size) {
    super(size, toCacheKey)
  }

  get snapshots() {
    return this.entries
  }
}


================================================
FILE: src/core/drive/view_transitioner.js
================================================
export class ViewTransitioner {
  #viewTransitionStarted = false
  #lastOperation = Promise.resolve()

  renderChange(useViewTransition, render) {
    if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
      this.#viewTransitionStarted = true
      this.#lastOperation = this.#lastOperation.then(async () => {
        await document.startViewTransition(render).finished
      })
    } else {
      this.#lastOperation = this.#lastOperation.then(render)
    }

    return this.#lastOperation
  }

  get viewTransitionsAvailable() {
    return document.startViewTransition
  }
}


================================================
FILE: src/core/drive/visit.js
================================================
import { FetchMethod, FetchRequest } from "../../http/fetch_request"
import { getAnchor } from "../url"
import { PageSnapshot } from "./page_snapshot"
import { getHistoryMethodForAction, uuid } from "../../util"
import { StreamMessage } from "../streams/stream_message"
import { ViewTransitioner } from "./view_transitioner"

const defaultOptions = {
  action: "advance",
  historyChanged: false,
  visitCachedSnapshot: () => {},
  willRender: true,
  updateHistory: true,
  shouldCacheSnapshot: true,
  acceptsStreamResponse: false,
  refresh: {}
}

export const TimingMetric = {
  visitStart: "visitStart",
  requestStart: "requestStart",
  requestEnd: "requestEnd",
  visitEnd: "visitEnd"
}

export const VisitState = {
  initialized: "initialized",
  started: "started",
  canceled: "canceled",
  failed: "failed",
  completed: "completed"
}

export const SystemStatusCode = {
  networkFailure: 0,
  timeoutFailure: -1,
  contentTypeMismatch: -2
}

export const Direction = {
  advance: "forward",
  restore: "back",
  replace: "none"
}

export class Visit {
  identifier = uuid() // Required by turbo-ios
  timingMetrics = {}

  followedRedirect = false
  historyChanged = false
  scrolled = false
  shouldCacheSnapshot = true
  acceptsStreamResponse = false
  snapshotCached = false
  state = VisitState.initialized
  viewTransitioner = new ViewTransitioner()

  constructor(delegate, location, restorationIdentifier, options = {}) {
    this.delegate = delegate
    this.location = location
    this.restorationIdentifier = restorationIdentifier || uuid()

    const {
      action,
      historyChanged,
      referrer,
      snapshot,
      snapshotHTML,
      response,
      visitCachedSnapshot,
      willRender,
      updateHistory,
      shouldCacheSnapshot,
      acceptsStreamResponse,
      direction,
      refresh
    } = {
      ...defaultOptions,
      ...options
    }
    this.action = action
    this.historyChanged = historyChanged
    this.referrer = referrer
    this.snapshot = snapshot
    this.snapshotHTML = snapshotHTML
    this.response = response
    this.isPageRefresh = this.view.isPageRefresh(this)
    this.visitCachedSnapshot = visitCachedSnapshot
    this.willRender = willRender
    this.updateHistory = updateHistory
    this.scrolled = !willRender
    this.shouldCacheSnapshot = shouldCacheSnapshot
    this.acceptsStreamResponse = acceptsStreamResponse
    this.direction = direction || Direction[action]
    this.refresh = refresh
  }

  get adapter() {
    return this.delegate.adapter
  }

  get view() {
    return this.delegate.view
  }

  get history() {
    return this.delegate.history
  }

  get restorationData() {
    return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)
  }

  start() {
    if (this.state == VisitState.initialized) {
      this.recordTimingMetric(TimingMetric.visitStart)
      this.state = VisitState.started
      this.adapter.visitStarted(this)
      this.delegate.visitStarted(this)
    }
  }

  cancel() {
    if (this.state == VisitState.started) {
      if (this.request) {
        this.request.cancel()
      }
      this.cancelRender()
      this.state = VisitState.canceled
    }
  }

  complete() {
    if (this.state == VisitState.started) {
      this.recordTimingMetric(TimingMetric.visitEnd)
      this.adapter.visitCompleted(this)
      this.state = VisitState.completed
      this.followRedirect()

      if (!this.followedRedirect) {
        this.delegate.visitCompleted(this)
      }
    }
  }

  fail() {
    if (this.state == VisitState.started) {
      this.state = VisitState.failed
      this.adapter.visitFailed(this)
      this.delegate.visitCompleted(this)
    }
  }

  changeHistory() {
    if (!this.historyChanged && this.updateHistory) {
      const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action
      const method = getHistoryMethodForAction(actionForHistory)
      this.history.update(method, this.location, this.restorationIdentifier)
      this.historyChanged = true
    }
  }

  issueRequest() {
    if (this.hasPreloadedResponse()) {
      this.simulateRequest()
    } else if (this.shouldIssueRequest() && !this.request) {
      this.request = new FetchRequest(this, FetchMethod.get, this.location)
      this.request.perform()
    }
  }

  simulateRequest() {
    if (this.response) {
      this.startRequest()
      this.recordResponse()
      this.finishRequest()
    }
  }

  startRequest() {
    this.recordTimingMetric(TimingMetric.requestStart)
    this.adapter.visitRequestStarted(this)
  }

  recordResponse(response = this.response) {
    this.response = response
    if (response) {
      const { statusCode } = response
      if (isSuccessful(statusCode)) {
        this.adapter.visitRequestCompleted(this)
      } else {
        this.adapter.visitRequestFailedWithStatusCode(this, statusCode)
      }
    }
  }

  finishRequest() {
    this.recordTimingMetric(TimingMetric.requestEnd)
    this.adapter.visitRequestFinished(this)
  }

  loadResponse() {
    if (this.response) {
      const { statusCode, responseHTML } = this.response
      this.render(async () => {
        if (this.shouldCacheSnapshot) this.cacheSnapshot()
        if (this.view.renderPromise) await this.view.renderPromise

        if (isSuccessful(statusCode) && responseHTML != null) {
          const snapshot = PageSnapshot.fromHTMLString(responseHTML)
          await this.renderPageSnapshot(snapshot, false)

          this.adapter.visitRendered(this)
          this.complete()
        } else {
          await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this)
          this.adapter.visitRendered(this)
          this.fail()
        }
      })
    }
  }

  getCachedSnapshot() {
    const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot()

    if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
      if (this.action == "restore" || snapshot.isPreviewable) {
        return snapshot
      }
    }
  }

  getPreloadedSnapshot() {
    if (this.snapshotHTML) {
      return PageSnapshot.fromHTMLString(this.snapshotHTML)
    }
  }

  hasCachedSnapshot() {
    return this.getCachedSnapshot() != null
  }

  loadCachedSnapshot() {
    const snapshot = this.getCachedSnapshot()
    if (snapshot) {
      const isPreview = this.shouldIssueRequest()
      this.render(async () => {
        this.cacheSnapshot()
        if (this.isPageRefresh) {
          this.adapter.visitRendered(this)
        } else {
          if (this.view.renderPromise) await this.view.renderPromise

          await this.renderPageSnapshot(snapshot, isPreview)

          this.adapter.visitRendered(this)
          if (!isPreview) {
            this.complete()
          }
        }
      })
    }
  }

  followRedirect() {
    if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
      this.adapter.visitProposedToLocation(this.redirectedToLocation, {
        action: "replace",
        response: this.response,
        shouldCacheSnapshot: false,
        willRender: false
      })
      this.followedRedirect = true
    }
  }

  // Fetch request delegate

  prepareRequest(request) {
    if (this.acceptsStreamResponse) {
      request.acceptResponseType(StreamMessage.contentType)
    }
  }

  requestStarted() {
    this.startRequest()
  }

  requestPreventedHandlingResponse(_request, _response) {}

  async requestSucceededWithResponse(request, response) {
    const responseHTML = await response.responseHTML
    const { redirected, statusCode } = response
    if (responseHTML == undefined) {
      this.recordResponse({
        statusCode: SystemStatusCode.contentTypeMismatch,
        redirected
      })
    } else {
      this.redirectedToLocation = response.redirected ? response.location : undefined
      this.recordResponse({ statusCode: statusCode, responseHTML, redirected })
    }
  }

  async requestFailedWithResponse(request, response) {
    const responseHTML = await response.responseHTML
    const { redirected, statusCode } = response
    if (responseHTML == undefined) {
      this.recordResponse({
        statusCode: SystemStatusCode.contentTypeMismatch,
        redirected
      })
    } else {
      this.recordResponse({ statusCode: statusCode, responseHTML, redirected })
    }
  }

  requestErrored(_request, _error) {
    this.recordResponse({
      statusCode: SystemStatusCode.networkFailure,
      redirected: false
    })
  }

  requestFinished() {
    this.finishRequest()
  }

  // Scrolling

  performScroll() {
    if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
      if (this.action == "restore") {
        this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop()
      } else {
        this.scrollToAnchor() || this.view.scrollToTop()
      }

      this.scrolled = true
    }
  }

  scrollToRestoredPosition() {
    const { scrollPosition } = this.restorationData
    if (scrollPosition) {
      this.view.scrollToPosition(scrollPosition)
      return true
    }
  }

  scrollToAnchor() {
    const anchor = getAnchor(this.location)
    if (anchor != null) {
      this.view.scrollToAnchor(anchor)
      return true
    }
  }

  // Instrumentation

  recordTimingMetric(metric) {
    this.timingMetrics[metric] = new Date().getTime()
  }

  getTimingMetrics() {
    return { ...this.timingMetrics }
  }

  // Private

  hasPreloadedResponse() {
    return typeof this.response == "object"
  }

  shouldIssueRequest() {
    if (this.action == "restore") {
      return !this.hasCachedSnapshot()
    } else {
      return this.willRender
    }
  }

  cacheSnapshot() {
    if (!this.snapshotCached) {
      this.view.cacheSnapshot(this.snapshot).then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot))
      this.snapshotCached = true
    }
  }

  async render(callback) {
    this.cancelRender()
    await new Promise((resolve) => {
      this.frame =
        document.visibilityState === "hidden" ? setTimeout(() => resolve(), 0) : requestAnimationFrame(() => resolve())
    })
    await callback()
    delete this.frame
  }

  async renderPageSnapshot(snapshot, isPreview) {
    await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), async () => {
      await this.view.renderPage(snapshot, isPreview, this.willRender, this)
      this.performScroll()
    })
  }

  cancelRender() {
    if (this.frame) {
      cancelAnimationFrame(this.frame)
      delete this.frame
    }
  }
}

function isSuccessful(statusCode) {
  return statusCode >= 200 && statusCode < 300
}


================================================
FILE: src/core/errors.js
================================================
export class TurboFrameMissingError extends Error {}


================================================
FILE: src/core/frames/frame_controller.js
================================================
import { FrameElement, FrameLoadingStyle } from "../../elements/frame_element"
import { FetchMethod, FetchRequest } from "../../http/fetch_request"
import { FetchResponse } from "../../http/fetch_response"
import { AppearanceObserver } from "../../observers/appearance_observer"
import {
  clearBusyState,
  dispatch,
  getAttribute,
  parseHTMLDocument,
  markAsBusy,
  uuid,
  getHistoryMethodForAction,
  getVisitAction
} from "../../util"
import { FormSubmission } from "../drive/form_submission"
import { Snapshot } from "../snapshot"
import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url"
import { FormSubmitObserver } from "../../observers/form_submit_observer"
import { FrameView } from "./frame_view"
import { LinkInterceptor } from "./link_interceptor"
import { FormLinkClickObserver } from "../../observers/form_link_click_observer"
import { FrameRenderer } from "./frame_renderer"
import { MorphingFrameRenderer } from "./morphing_frame_renderer"
import { session } from "../index"
import { StreamMessage } from "../streams/stream_message"
import { PageSnapshot } from "../drive/page_snapshot"
import { TurboFrameMissingError } from "../errors"

export class FrameController {
  fetchResponseLoaded = (_fetchResponse) => Promise.resolve()
  #currentFetchRequest = null
  #resolveVisitPromise = () => {}
  #connected = false
  #hasBeenLoaded = false
  #ignoredAttributes = new Set()
  #shouldMorphFrame = false
  action = null

  constructor(element) {
    this.element = element
    this.view = new FrameView(this, this.element)
    this.appearanceObserver = new AppearanceObserver(this, this.element)
    this.formLinkClickObserver = new FormLinkClickObserver(this, this.element)
    this.linkInterceptor = new LinkInterceptor(this, this.element)
    this.restorationIdentifier = uuid()
    this.formSubmitObserver = new FormSubmitObserver(this, this.element)
  }

  // Frame delegate

  connect() {
    if (!this.#connected) {
      this.#connected = true
      if (this.loadingStyle == FrameLoadingStyle.lazy) {
        this.appearanceObserver.start()
      } else {
        this.#loadSourceURL()
      }
      this.formLinkClickObserver.start()
      this.linkInterceptor.start()
      this.formSubmitObserver.start()
    }
  }

  disconnect() {
    if (this.#connected) {
      this.#connected = false
      this.appearanceObserver.stop()
      this.formLinkClickObserver.stop()
      this.linkInterceptor.stop()
      this.formSubmitObserver.stop()

      if (!this.element.hasAttribute("recurse")) {
        this.#currentFetchRequest?.cancel()
      }
    }
  }

  disabledChanged() {
    if (this.disabled) {
      this.#currentFetchRequest?.cancel()
    } else if (this.loadingStyle == FrameLoadingStyle.eager) {
      this.#loadSourceURL()
    }
  }

  sourceURLChanged() {
    if (this.#isIgnoringChangesTo("src")) return

    if (!this.sourceURL) {
      this.#currentFetchRequest?.cancel()
    }

    if (this.element.isConnected) {
      this.complete = false
    }

    if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) {
      this.#loadSourceURL()
    }
  }

  sourceURLReloaded() {
    const { refresh, src } = this.element

    this.#shouldMorphFrame = src && refresh === "morph"

    this.element.removeAttribute("complete")
    this.element.src = null
    this.element.src = src
    return this.element.loaded
  }

  loadingStyleChanged() {
    if (this.loadingStyle == FrameLoadingStyle.lazy) {
      this.appearanceObserver.start()
    } else {
      this.appearanceObserver.stop()
      this.#loadSourceURL()
    }
  }

  async #loadSourceURL() {
    if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
      this.element.loaded = this.#visit(expandURL(this.sourceURL))
      this.appearanceObserver.stop()
      await this.element.loaded
      this.#hasBeenLoaded = true
    }
  }

  async loadResponse(fetchResponse) {
    if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
      this.sourceURL = fetchResponse.response.url
    }

    try {
      const html = await fetchResponse.responseHTML
      if (html) {
        const document = parseHTMLDocument(html)
        const pageSnapshot = PageSnapshot.fromDocument(document)

        if (pageSnapshot.isVisitable) {
          await this.#loadFrameResponse(fetchResponse, document)
        } else {
          await this.#handleUnvisitableFrameResponse(fetchResponse)
        }
      }
    } finally {
      this.#shouldMorphFrame = false
      this.fetchResponseLoaded = () => Promise.resolve()
    }
  }

  // Appearance observer delegate

  elementAppearedInViewport(element) {
    this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element))
    this.#loadSourceURL()
  }

  // Form link click observer delegate

  willSubmitFormLinkToLocation(link) {
    return this.#shouldInterceptNavigation(link)
  }

  submittedFormLinkToLocation(link, _location, form) {
    const frame = this.#findFrameElement(link)
    if (frame) form.setAttribute("data-turbo-frame", frame.id)
  }

  // Link interceptor delegate

  shouldInterceptLinkClick(element, _location, _event) {
    return this.#shouldInterceptNavigation(element)
  }

  linkClickIntercepted(element, location) {
    this.#navigateFrame(element, location)
  }

  // Form submit observer delegate

  willSubmitForm(element, submitter) {
    return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter)
  }

  formSubmitted(element, submitter) {
    if (this.formSubmission) {
      this.formSubmission.stop()
    }

    this.formSubmission = new FormSubmission(this, element, submitter)

    const { fetchRequest } = this.formSubmission
    const frame = this.#findFrameElement(element, submitter)

    this.prepareRequest(fetchRequest, frame)
    this.formSubmission.start()
  }

  // Fetch request delegate

  prepareRequest(request, frame = this) {
    request.headers["Turbo-Frame"] = frame.id

    if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
      request.acceptResponseType(StreamMessage.contentType)
    }
  }

  requestStarted(_request) {
    markAsBusy(this.element)
  }

  requestPreventedHandlingResponse(_request, _response) {
    this.#resolveVisitPromise()
  }

  async requestSucceededWithResponse(request, response) {
    await this.loadResponse(response)
    this.#resolveVisitPromise()
  }

  async requestFailedWithResponse(request, response) {
    await this.loadResponse(response)
    this.#resolveVisitPromise()
  }

  requestErrored(request, error) {
    console.error(error)
    this.#resolveVisitPromise()
  }

  requestFinished(_request) {
    clearBusyState(this.element)
  }

  // Form submission delegate

  formSubmissionStarted({ formElement }) {
    markAsBusy(formElement, this.#findFrameElement(formElement))
  }

  formSubmissionSucceededWithResponse(formSubmission, response) {
    const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter)

    frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame))
    frame.delegate.loadResponse(response)

    if (!formSubmission.isSafe) {
      session.clearCache()
    }
  }

  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
    this.element.delegate.loadResponse(fetchResponse)
    session.clearCache()
  }

  formSubmissionErrored(formSubmission, error) {
    console.error(error)
  }

  formSubmissionFinished({ formElement }) {
    clearBusyState(formElement, this.#findFrameElement(formElement))
  }

  // View delegate

  allowsImmediateRender({ element: newFrame }, options) {
    const event = dispatch("turbo:before-frame-render", {
      target: this.element,
      detail: { newFrame, ...options },
      cancelable: true
    })

    const {
      defaultPrevented,
      detail: { render }
    } = event

    if (this.view.renderer && render) {
      this.view.renderer.renderElement = render
    }

    return !defaultPrevented
  }

  viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}

  preloadOnLoadLinksForView(element) {
    session.preloadOnLoadLinksForView(element)
  }

  viewInvalidated() {}

  // Frame renderer delegate

  willRenderFrame(currentElement, _newElement) {
    this.previousFrameElement = currentElement.cloneNode(true)
  }

  visitCachedSnapshot = ({ element }) => {
    const frame = element.querySelector("#" + this.element.id)

    if (frame && this.previousFrameElement) {
      frame.replaceChildren(...this.previousFrameElement.children)
    }

    delete this.previousFrameElement
  }

  // Private

  async #loadFrameResponse(fetchResponse, document) {
    const newFrameElement = await this.extractForeignFrameElement(document.body)
    const rendererClass = this.#shouldMorphFrame ? MorphingFrameRenderer : FrameRenderer

    if (newFrameElement) {
      const snapshot = new Snapshot(newFrameElement)
      const renderer = new rendererClass(this, this.view.snapshot, snapshot, false, false)
      if (this.view.renderPromise) await this.view.renderPromise
      this.changeHistory()

      await this.view.render(renderer)
      this.complete = true
      session.frameRendered(fetchResponse, this.element)
      session.frameLoaded(this.element)
      await this.fetchResponseLoaded(fetchResponse)
    } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {
      this.#handleFrameMissingFromResponse(fetchResponse)
    }
  }

  async #visit(url) {
    const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element)

    this.#currentFetchRequest?.cancel()
    this.#currentFetchRequest = request

    return new Promise((resolve) => {
      this.#resolveVisitPromise = () => {
        this.#resolveVisitPromise = () => {}
        this.#currentFetchRequest = null
        resolve()
      }
      request.perform()
    })
  }

  #navigateFrame(element, url, submitter) {
    const frame = this.#findFrameElement(element, submitter)

    frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame))

    this.#withCurrentNavigationElement(element, () => {
      frame.src = url
    })
  }

  proposeVisitIfNavigatedWithAction(frame, action = null) {
    this.action = action

    if (this.action) {
      const pageSnapshot = PageSnapshot.fromElement(frame).clone()
      const { visitCachedSnapshot } = frame.delegate

      frame.delegate.fetchResponseLoaded = async (fetchResponse) => {
        if (frame.src) {
          const { statusCode, redirected } = fetchResponse
          const responseHTML = await fetchResponse.responseHTML
          const response = { statusCode, redirected, responseHTML }
          const options = {
            response,
            visitCachedSnapshot,
            willRender: false,
            updateHistory: false,
            restorationIdentifier: this.restorationIdentifier,
            snapshot: pageSnapshot
          }

          if (this.action) options.action = this.action

          session.visit(frame.src, options)
        }
      }
    }
  }

  changeHistory() {
    if (this.action) {
      const method = getHistoryMethodForAction(this.action)
      session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier)
    }
  }

  async #handleUnvisitableFrameResponse(fetchResponse) {
    console.warn(
      `The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`
    )

    await this.#visitResponse(fetchResponse.response)
  }

  #willHandleFrameMissingFromResponse(fetchResponse) {
    this.element.setAttribute("complete", "")

    const response = fetchResponse.response
    const visit = async (url, options) => {
      if (url instanceof Response) {
        this.#visitResponse(url)
      } else {
        session.visit(url, options)
      }
    }

    const event = dispatch("turbo:frame-missing", {
      target: this.element,
      detail: { response, visit },
      cancelable: true
    })

    return !event.defaultPrevented
  }

  #handleFrameMissingFromResponse(fetchResponse) {
    this.view.missing()
    this.#throwFrameMissingError(fetchResponse)
  }

  #throwFrameMissingError(fetchResponse) {
    const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`
    throw new TurboFrameMissingError(message)
  }

  async #visitResponse(response) {
    const wrapped = new FetchResponse(response)
    const responseHTML = await wrapped.responseHTML
    const { location, redirected, statusCode } = wrapped

    return session.visit(location, { response: { redirected, statusCode, responseHTML } })
  }

  #findFrameElement(element, submitter) {
    const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target")
    const target = this.#getFrameElementById(id)

    return target instanceof FrameElement ? target : this.element
  }

  async extractForeignFrameElement(container) {
    let element
    const id = CSS.escape(this.id)

    try {
      element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL)
      if (element) {
        return element
      }

      element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL)
      if (element) {
        await element.loaded
        return await this.extractForeignFrameElement(element)
      }
    } catch (error) {
      console.error(error)
      return new FrameElement()
    }

    return null
  }

  #formActionIsVisitable(form, submitter) {
    const action = getAction(form, submitter)

    return locationIsVisitable(expandURL(action), this.rootLocation)
  }

  #shouldInterceptNavigation(element, submitter) {
    const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target")

    if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) {
      return false
    }

    if (!this.enabled || id == "_top") {
      return false
    }

    if (id) {
      const frameElement = this.#getFrameElementById(id)
      if (frameElement) {
        return !frameElement.disabled
      } else if (id == "_parent") {
        return false
      }
    }

    if (!session.elementIsNavigatable(element)) {
      return false
    }

    if (submitter && !session.elementIsNavigatable(submitter)) {
      return false
    }

    return true
  }

  // Computed properties

  get id() {
    return this.element.id
  }

  get disabled() {
    return this.element.disabled
  }

  get enabled() {
    return !this.disabled
  }

  get sourceURL() {
    if (this.element.src) {
      return this.element.src
    }
  }

  set sourceURL(sourceURL) {
    this.#ignoringChangesToAttribute("src", () => {
      this.element.src = sourceURL ?? null
    })
  }

  get loadingStyle() {
    return this.element.loading
  }

  get isLoading() {
    return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined
  }

  get complete() {
    return this.element.hasAttribute("complete")
  }

  set complete(value) {
    if (value) {
      this.element.setAttribute("complete", "")
    } else {
      this.element.removeAttribute("complete")
    }
  }

  get isActive() {
    return this.element.isActive && this.#connected
  }

  get rootLocation() {
    const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`)
    const root = meta?.content ?? "/"
    return expandURL(root)
  }

  #isIgnoringChangesTo(attributeName) {
    return this.#ignoredAttributes.has(attributeName)
  }

  #ignoringChangesToAttribute(attributeName, callback) {
    this.#ignoredAttributes.add(attributeName)
    callback()
    this.#ignoredAttributes.delete(attributeName)
  }

  #withCurrentNavigationElement(element, callback) {
    this.currentNavigationElement = element
    callback()
    delete this.currentNavigationElement
  }

  #getFrameElementById(id) {
    if (id != null) {
      const element = id === "_parent" ?
        this.element.parentElement.closest("turbo-frame") :
        document.getElementById(id)
      if (element instanceof FrameElement) {
        return element
      }
    }
  }
}

function activateElement(element, currentURL) {
  if (element) {
    const src = element.getAttribute("src")
    if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) {
      throw new Error(`Matching <turbo-frame id="${element.id}"> element has a source URL which references itself`)
    }
    if (element.ownerDocument !== document) {
      element = document.importNode(element, true)
    }

    if (element instanceof FrameElement) {
      element.connectedCallback()
      element.disconnectedCallback()
      return element
    }
  }
}


================================================
FILE: src/core/frames/frame_redirector.js
================================================
import { FormSubmitObserver } from "../../observers/form_submit_observer"
import { FrameElement } from "../../elements/frame_element"
import { LinkInterceptor } from "./link_interceptor"
import { expandURL, getAction, locationIsVisitable } from "../url"

export class FrameRedirector {
  constructor(session, element) {
    this.session = session
    this.element = element
    this.linkInterceptor = new LinkInterceptor(this, element)
    this.formSubmitObserver = new FormSubmitObserver(this, element)
  }

  start() {
    this.linkInterceptor.start()
    this.formSubmitObserver.start()
  }

  stop() {
    this.linkInterceptor.stop()
    this.formSubmitObserver.stop()
  }

  // Link interceptor delegate

  shouldInterceptLinkClick(element, _location, _event) {
    return this.#shouldRedirect(element)
  }

  linkClickIntercepted(element, url, event) {
    const frame = this.#findFrameElement(element)
    if (frame) {
      frame.delegate.linkClickIntercepted(element, url, event)
    }
  }

  // Form submit observer delegate

  willSubmitForm(element, submitter) {
    return (
      element.closest("turbo-frame") == null &&
      this.#shouldSubmit(element, submitter) &&
      this.#shouldRedirect(element, submitter)
    )
  }

  formSubmitted(element, submitter) {
    const frame = this.#findFrameElement(element, submitter)
    if (frame) {
      frame.delegate.formSubmitted(element, submitter)
    }
  }

  #shouldSubmit(form, submitter) {
    const action = getAction(form, submitter)
    const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`)
    const rootLocation = expandURL(meta?.content ?? "/")

    return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation)
  }

  #shouldRedirect(element, submitter) {
    const isNavigatable =
      element instanceof HTMLFormElement
        ? this.session.submissionIsNavigatable(element, submitter)
        : this.session.elementIsNavigatable(element)

    if (isNavigatable) {
      const frame = this.#findFrameElement(element, submitter)
      return frame ? frame != element.closest("turbo-frame") : false
    } else {
      return false
    }
  }

  #findFrameElement(element, submitter) {
    const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame")
    if (id && id != "_top") {
      const frame = this.element.querySelector(`#${id}:not([disabled])`)
      if (frame instanceof FrameElement) {
        return frame
      }
    }
  }
}


================================================
FILE: src/core/frames/frame_renderer.js
================================================
import { activateScriptElement, nextRepaint } from "../../util"
import { Renderer } from "../renderer"

export class FrameRenderer extends Renderer {
  static renderElement(currentElement, newElement) {
    const destinationRange = document.createRange()
    destinationRange.selectNodeContents(currentElement)
    destinationRange.deleteContents()

    const frameElement = newElement
    const sourceRange = frameElement.ownerDocument?.createRange()
    if (sourceRange) {
      sourceRange.selectNodeContents(frameElement)
      currentElement.appendChild(sourceRange.extractContents())
    }
  }

  constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
    super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender)
    this.delegate = delegate
  }

  get shouldRender() {
    return true
  }

  async render() {
    await nextRepaint()
    this.preservingPermanentElements(() => {
      this.loadFrameElement()
    })
    this.scrollFrameIntoView()
    await nextRepaint()
    this.focusFirstAutofocusableElement()
    await nextRepaint()
    this.activateScriptElements()
  }

  loadFrameElement() {
    this.delegate.willRenderFrame(this.currentElement, this.newElement)
    this.renderElement(this.currentElement, this.newElement)
  }

  scrollFrameIntoView() {
    if (this.currentElement.autoscroll || this.newElement.autoscroll) {
      const element = this.currentElement.firstElementChild
      const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end")
      const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto")

      if (element) {
        element.scrollIntoView({ block, behavior })
        return true
      }
    }
    return false
  }

  activateScriptElements() {
    for (const inertScriptElement of this.newScriptElements) {
      const activatedScriptElement = activateScriptElement(inertScriptElement)
      inertScriptElement.replaceWith(activatedScriptElement)
    }
  }

  get newScriptElements() {
    return this.currentElement.querySelectorAll("script")
  }
}

function readScrollLogicalPosition(value, defaultValue) {
  if (value == "end" || value == "start" || value == "center" || value == "nearest") {
    return value
  } else {
    return defaultValue
  }
}

function readScrollBehavior(value, defaultValue) {
  if (value == "auto" || value == "smooth") {
    return value
  } else {
    return defaultValue
  }
}


================================================
FILE: src/core/frames/frame_view.js
================================================
import { Snapshot } from "../snapshot"
import { View } from "../view"

export class FrameView extends View {
  missing() {
    this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`
  }

  get snapshot() {
    return new Snapshot(this.element)
  }
}


================================================
FILE: src/core/frames/link_interceptor.js
================================================
import { findLinkFromClickTarget } from "../../util"

export class LinkInterceptor {
  constructor(delegate, element) {
    this.delegate = delegate
    this.element = element
  }

  start() {
    this.element.addEventListener("click", this.clickBubbled)
    document.addEventListener("turbo:click", this.linkClicked)
    document.addEventListener("turbo:before-visit", this.willVisit)
  }

  stop() {
    this.element.removeEventListener("click", this.clickBubbled)
    document.removeEventListener("turbo:click", this.linkClicked)
    document.removeEventListener("turbo:before-visit", this.willVisit)
  }

  clickBubbled = (event) => {
    if (this.clickEventIsSignificant(event)) {
      this.clickEvent = event
    } else {
      delete this.clickEvent
    }
  }

  linkClicked = (event) => {
    if (this.clickEvent && this.clickEventIsSignificant(event)) {
      if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
        this.clickEvent.preventDefault()
        event.preventDefault()
        this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent)
      }
    }
    delete this.clickEvent
  }

  willVisit = (_event) => {
    delete this.clickEvent
  }

  clickEventIsSignificant(event) {
    const target = event.composed ? event.target?.parentElement : event.target
    const element = findLinkFromClickTarget(target) || target

    return element instanceof Element && element.closest("turbo-frame, html") == this.element
  }
}


================================================
FILE: src/core/frames/morphing_frame_renderer.js
================================================
import { FrameRenderer } from "./frame_renderer"
import { morphChildren, shouldRefreshFrameWithMorphing, closestFrameReloadableWithMorphing } from "../morphing"
import { dispatch } from "../../util"

export class MorphingFrameRenderer extends FrameRenderer {
  static renderElement(currentElement, newElement) {
    dispatch("turbo:before-frame-morph", {
      target: currentElement,
      detail: { currentElement, newElement }
    })

    morphChildren(currentElement, newElement, {
      callbacks: {
        beforeNodeMorphed: (node, newNode) => {
          if (
            shouldRefreshFrameWithMorphing(node, newNode) &&
              closestFrameReloadableWithMorphing(node) === currentElement
          ) {
            node.reload()
            return false
          }
          return true
        }
      }
    })
  }

  async preservingPermanentElements(callback) {
    return await callback()
  }
}



================================================
FILE: src/core/index.js
================================================
import { Session } from "./session"
import { PageRenderer } from "./drive/page_renderer"
import { PageSnapshot } from "./drive/page_snapshot"
import { FrameRenderer } from "./frames/frame_renderer"
import { fetch, recentRequests } from "../http/fetch"
import { config } from "./config"
import { MorphingPageRenderer } from "./drive/morphing_page_renderer"
import { MorphingFrameRenderer } from "./frames/morphing_frame_renderer"

export { morphChildren, morphElements } from "./morphing"
export { PageRenderer, PageSnapshot, FrameRenderer, fetch, config }

const session = new Session(recentRequests)

// Rename `navigator` to avoid shadowing `window.navigator`
const { cache, navigator: sessionNavigator } = session
export { session, cache, sessionNavigator as navigator }

/**
 * Starts the main session.
 * This initialises any necessary observers such as those to monitor
 * link interactions.
 */
export function start() {
  session.start()
}

/**
 * Registers an adapter for the main session.
 *
 * @param adapter Adapter to register
 */
export function registerAdapter(adapter) {
  session.registerAdapter(adapter)
}

/**
 * Performs an application visit to the given location.
 *
 * @param location Location to visit (a URL or path)
 * @param options Options to apply
 * @param options.action Type of history navigation to apply ("restore",
 * "replace" or "advance")
 * @param options.historyChanged Specifies whether the browser history has
 * already been changed for this visit or not
 * @param options.referrer Specifies the referrer of this visit such that
 * navigations to the same page will not result in a new history entry.
 * @param options.snapshotHTML Cached snapshot to render
 * @param options.response Response of the specified location
 */
export function visit(location, options) {
  session.visit(location, options)
}

/**
 * Connects a stream source to the main session.
 *
 * @param source Stream source to connect
 */
export function connectStreamSource(source) {
  session.connectStreamSource(source)
}

/**
 * Disconnects a stream source from the main session.
 *
 * @param source Stream source to disconnect
 */
export function disconnectStreamSource(source) {
  session.disconnectStreamSource(source)
}

/**
 * Renders a stream message to the main session by appending it to the
 * current document.
 *
 * @param message Message to render
 */
export function renderStreamMessage(message) {
  session.renderStreamMessage(message)
}

/**
 * Sets the delay after which the progress bar will appear during navigation.
 *
 * The progress bar appears after 500ms by default.
 *
 * Note that this method has no effect when used with the iOS or Android
 * adapters.
 *
 * @param delay Time to delay in milliseconds
 */
export function setProgressBarDelay(delay) {
  console.warn(
    "Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
  )
  config.drive.progressBarDelay = delay
}

export function setConfirmMethod(confirmMethod) {
  console.warn(
    "Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
  )
  config.forms.confirm = confirmMethod
}

export function setFormMode(mode) {
  console.warn(
    "Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
  )
  config.forms.mode = mode
}

/**
 * Morph the state of the currentBody based on the attributes and contents of
 * the newBody. Morphing body elements may dispatch turbo:morph,
 * turbo:before-morph-element, turbo:before-morph-attribute, and
 * turbo:morph-element events.
 *
 * @param currentBody HTMLBodyElement destination of morphing changes
 * @param newBody HTMLBodyElement source of morphing changes
 */
export function morphBodyElements(currentBody, newBody) {
  MorphingPageRenderer.renderElement(currentBody, newBody)
}

/**
 * Morph the child elements of the currentFrame based on the child elements of
 * the newFrame. Morphing turbo-frame elements may dispatch turbo:before-frame-morph,
 * turbo:before-morph-element, turbo:before-morph-attribute, and
 * turbo:morph-element events.
 *
 * @param currentFrame FrameElement destination of morphing children changes
 * @param newFrame FrameElement source of morphing children changes
 */
export function morphTurboFrameElements(currentFrame, newFrame) {
  MorphingFrameRenderer.renderElement(currentFrame, newFrame)
}


================================================
FILE: src/core/lru_cache.js
================================================
const identity = key => key

export class LRUCache {
  keys = []
  entries = {}
  #toCacheKey

  constructor(size, toCacheKey = identity) {
    this.size = size
    this.#toCacheKey = toCacheKey
  }

  has(key) {
    return this.#toCacheKey(key) in this.entries
  }

  get(key) {
    if (this.has(key)) {
      const entry = this.read(key)
      this.touch(key)
      return entry
    }
  }

  put(key, entry) {
    this.write(key, entry)
    this.touch(key)
    return entry
  }

  clear() {
    for (const key of Object.keys(this.entries)) {
      this.evict(key)
    }
  }

  // Private

  read(key) {
    return this.entries[this.#toCacheKey(key)]
  }

  write(key, entry) {
    this.entries[this.#toCacheKey(key)] = entry
  }

  touch(key) {
    key = this.#toCacheKey(key)
    const index = this.keys.indexOf(key)
    if (index > -1) this.keys.splice(index, 1)
    this.keys.unshift(key)
    this.trim()
  }

  trim() {
    for (const key of this.keys.splice(this.size)) {
      this.evict(key)
    }
  }

  evict(key) {
    delete this.entries[key]
  }
}


================================================
FILE: src/core/morphing.js
================================================
import { Idiomorph } from "idiomorph"
import { FrameElement } from "../elements/frame_element"
import { dispatch } from "../util"
import { urlsAreEqual } from "./url"

/**
 * Morph the state of the currentElement based on the attributes and contents of
 * the newElement. Morphing may dispatch turbo:before-morph-element,
 * turbo:before-morph-attribute, and turbo:morph-element events.
 *
 * @param currentElement Element destination of morphing changes
 * @param newElement Element source of morphing changes
 */
export function morphElements(currentElement, newElement, { callbacks, ...options } = {}) {
  Idiomorph.morph(currentElement, newElement, {
    ...options,
    callbacks: new DefaultIdiomorphCallbacks(callbacks)
  })
}

/**
 * Morph the child elements of the currentElement based on the child elements of
 * the newElement. Morphing children may dispatch turbo:before-morph-element,
 * turbo:before-morph-attribute, and turbo:morph-element events.
 *
 * @param currentElement Element destination of morphing children changes
 * @param newElement Element source of morphing children changes
 */
export function morphChildren(currentElement, newElement, options = {}) {
  morphElements(currentElement, newElement.childNodes, {
    ...options,
    morphStyle: "innerHTML"
  })
}

export function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
  return currentFrame instanceof FrameElement &&
    currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) &&
    !currentFrame.closest("[data-turbo-permanent]")
}

function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
  // newFrame cannot yet be an instance of FrameElement because custom
  // elements don't get initialized until they're attached to the DOM, so
  // test its Element#nodeName instead
  return newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && currentFrame.id === newFrame.id &&
  (!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src")))
}

export function closestFrameReloadableWithMorphing(node) {
  return node.parentElement.closest("turbo-frame[src][refresh=morph]")
}

class DefaultIdiomorphCallbacks {
  #beforeNodeMorphed

  constructor({ beforeNodeMorphed } = {}) {
    this.#beforeNodeMorphed = beforeNodeMorphed || (() => true)
  }

  beforeNodeAdded = (node) => {
    return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
  }

  beforeNodeMorphed = (currentElement, newElement) => {
    if (currentElement instanceof Element) {
      if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
        const event = dispatch("turbo:before-morph-element", {
          cancelable: true,
          target: currentElement,
          detail: { currentElement, newElement }
        })

        return !event.defaultPrevented
      } else {
        return false
      }
    }
  }

  beforeAttributeUpdated = (attributeName, target, mutationType) => {
    const event = dispatch("turbo:before-morph-attribute", {
      cancelable: true,
      target,
      detail: { attributeName, mutationType }
    })

    return !event.defaultPrevented
  }

  beforeNodeRemoved = (node) => {
    return this.beforeNodeMorphed(node)
  }

  afterNodeMorphed = (currentElement, newElement) => {
    if (currentElement instanceof Element) {
      dispatch("turbo:morph-element", {
        target: currentElement,
        detail: { currentElement, newElement }
      })
    }
  }
}


================================================
FILE: src/core/native/browser_adapter.js
================================================
import { ProgressBar } from "../drive/progress_bar"
import { SystemStatusCode } from "../drive/visit"
import { uuid, dispatch } from "../../util"
import { locationIsVisitable } from "../url"

export class BrowserAdapter {
  progressBar = new ProgressBar()

  constructor(session) {
    this.session = session
  }

  visitProposedToLocation(location, options) {
    if (locationIsVisitable(location, this.navigator.rootLocation)) {
      this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options)
    } else {
      window.location.href = location.toString()
    }
  }

  visitStarted(visit) {
    this.location = visit.location
    this.redirectedToLocation = null

    visit.loadCachedSnapshot()
    visit.issueRequest()
  }

  visitRequestStarted(visit) {
    this.progressBar.setValue(0)
    if (visit.hasCachedSnapshot() || visit.action != "restore") {
      this.showVisitProgressBarAfterDelay()
    } else {
      this.showProgressBar()
    }
  }

  visitRequestCompleted(visit) {
    visit.loadResponse()

    if (visit.response.redirected) {
      this.redirectedToLocation = visit.redirectedToLocation
    }
  }

  visitRequestFailedWithStatusCode(visit, statusCode) {
    switch (statusCode) {
      case SystemStatusCode.networkFailure:
      case SystemStatusCode.timeoutFailure:
      case SystemStatusCode.contentTypeMismatch:
        return this.reload({
          reason: "request_failed",
          context: {
            statusCode
          }
        })
      default:
        return visit.loadResponse()
    }
  }

  visitRequestFinished(_visit) {}

  visitCompleted(_visit) {
    this.progressBar.setValue(1)
    this.hideVisitProgressBar()
  }

  pageInvalidated(reason) {
    this.reload(reason)
  }

  visitFailed(_visit) {
    this.progressBar.setValue(1)
    this.hideVisitProgressBar()
  }

  visitRendered(_visit) {}

  // Link prefetching

  linkPrefetchingIsEnabledForLocation(location) {
    return true
  }

  // Form Submission Delegate

  formSubmissionStarted(_formSubmission) {
    this.progressBar.setValue(0)
    this.showFormProgressBarAfterDelay()
  }

  formSubmissionFinished(_formSubmission) {
    this.progressBar.setValue(1)
    this.hideFormProgressBar()
  }

  // Private

  showVisitProgressBarAfterDelay() {
    this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay)
  }

  hideVisitProgressBar() {
    this.progressBar.hide()
    if (this.visitProgressBarTimeout != null) {
      window.clearTimeout(this.visitProgressBarTimeout)
      delete this.visitProgressBarTimeout
    }
  }

  showFormProgressBarAfterDelay() {
    if (this.formProgressBarTimeout == null) {
      this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay)
    }
  }

  hideFormProgressBar() {
    this.progressBar.hide()
    if (this.formProgressBarTimeout != null) {
      window.clearTimeout(this.formProgressBarTimeout)
      delete this.formProgressBarTimeout
    }
  }

  showProgressBar = () => {
    this.progressBar.show()
  }

  reload(reason) {
    dispatch("turbo:reload", { detail: reason })

    window.location.href = (this.redirectedToLocation || this.location)?.toString() || window.location.href
  }

  get navigator() {
    return this.session.navigator
  }
}


================================================
FILE: src/core/renderer.js
================================================
import { Bardo } from "./bardo"

export class Renderer {
  #activeElement = null

  static renderElement(currentElement, newElement) {
    // Abstract method
  }

  constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
    this.currentSnapshot = currentSnapshot
    this.newSnapshot = newSnapshot
    this.isPreview = isPreview
    this.willRender = willRender
    this.renderElement = this.constructor.renderElement
    this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject }))
  }

  get shouldRender() {
    return true
  }

  get shouldAutofocus() {
    return true
  }

  get reloadReason() {
    return
  }

  prepareToRender() {
    return
  }

  render() {
    // Abstract method
  }

  finishRendering() {
    if (this.resolvingFunctions) {
      this.resolvingFunctions.resolve()
      delete this.resolvingFunctions
    }
  }

  async preservingPermanentElements(callback) {
    await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback)
  }

  focusFirstAutofocusableElement() {
    if (this.shouldAutofocus) {
      const element = this.connectedSnapshot.firstAutofocusableElement
      if (element) {
        element.focus()
      }
    }
  }

  // Bardo delegate

  enteringBardo(currentPermanentElement) {
    if (this.#activeElement) return

    if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
      this.#activeElement = this.currentSnapshot.activeElement
    }
  }

  leavingBardo(currentPermanentElement) {
    if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {
      this.#activeElement.focus()

      this.#activeElement = null
    }
  }

  get connectedSnapshot() {
    return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot
  }

  get currentElement() {
    return this.currentSnapshot.element
  }

  get newElement() {
    return this.newSnapshot.element
  }

  get permanentElementMap() {
    return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot)
  }

  get renderMethod() {
    return "replace"
  }
}


================================================
FILE: src/core/session.js
================================================
import { BrowserAdapter } from "./native/browser_adapter"
import { CacheObserver } from "../observers/cache_observer"
import { FormSubmitObserver } from "../observers/form_submit_observer"
import { FrameRedirector } from "./frames/frame_redirector"
import { History } from "./drive/history"
import { LinkPrefetchObserver } from "../observers/link_prefetch_observer"
import { LinkClickObserver } from "../observers/link_click_observer"
import { FormLinkClickObserver } from "../observers/form_link_click_observer"
import { getAction, expandURL, locationIsVisitable } from "./url"
import { Navigator } from "./drive/navigator"
import { PageObserver } from "../observers/page_observer"
import { ScrollObserver } from "../observers/scroll_observer"
import { StreamMessage } from "./streams/stream_message"
import { StreamMessageRenderer } from "./streams/stream_message_renderer"
import { StreamObserver } from "../observers/stream_observer"
import { clearBusyState, dispatch, findClosestRecursively, getVisitAction, markAsBusy, debounce } from "../util"
import { PageView } from "./drive/page_view"
import { FrameElement } from "../elements/frame_element"
import { Preloader } from "./drive/preloader"
import { Cache } from "./cache"
import { config } from "./config"

export class Session {
  navigator = new Navigator(this)
  history = new History(this)
  view = new PageView(this, document.documentElement)
  adapter = new BrowserAdapter(this)

  pageObserver = new PageObserver(this)
  cacheObserver = new CacheObserver()
  linkPrefetchObserver = new LinkPrefetchObserver(this, document)
  linkClickObserver = new LinkClickObserver(this, window)
  formSubmitObserver = new FormSubmitObserver(this, document)
  scrollObserver = new ScrollObserver(this)
  streamObserver = new StreamObserver(this)
  formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement)
  frameRedirector = new FrameRedirector(this, document.documentElement)
  streamMessageRenderer = new StreamMessageRenderer()
  cache = new Cache(this)

  enabled = true
  started = false
  #pageRefreshDebouncePeriod = 150

  constructor(recentRequests) {
    this.recentRequests = recentRequests
    this.preloader = new Preloader(this, this.view.snapshotCache)
    this.debouncedRefresh = this.refresh
    this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod
  }

  start() {
    if (!this.started) {
      this.pageObserver.start()
      this.cacheObserver.start()
      this.linkPrefetchObserver.start()
      this.formLinkClickObserver.start()
      this.linkClickObserver.start()
      this.formSubmitObserver.start()
      this.scrollObserver.start()
      this.streamObserver.start()
      this.frameRedirector.start()
      this.history.start()
      this.preloader.start()
      this.started = true
      this.enabled = true
    }
  }

  disable() {
    this.enabled = false
  }

  stop() {
    if (this.started) {
      this.pageObserver.stop()
      this.cacheObserver.stop()
      this.linkPrefetchObserver.stop()
      this.formLinkClickObserver.stop()
      this.linkClickObserver.stop()
      this.formSubmitObserver.stop()
      this.scrollObserver.stop()
      this.streamObserver.stop()
      this.frameRedirector.stop()
      this.history.stop()
      this.preloader.stop()
      this.started = false
    }
  }

  registerAdapter(adapter) {
    this.adapter = adapter
  }

  visit(location, options = {}) {
    const frameElement = options.frame ? document.getElementById(options.frame) : null

    if (frameElement instanceof FrameElement) {
      const action = options.action || getVisitAction(frameElement)

      frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action)
      frameElement.src = location.toString()
    } else {
      this.navigator.proposeVisit(expandURL(location), options)
    }
  }

  refresh(url, options = {}) {
    options = typeof options === "string" ? { requestId: options } : options

    const { method, requestId, scroll } = options
    const isRecentRequest = requestId && this.recentRequests.has(requestId)
    const isCurrentUrl = url === document.baseURI
    if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
      this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } })
    }
  }

  connectStreamSource(source) {
    this.streamObserver.connectStreamSource(source)
  }

  disconnectStreamSource(source) {
    this.streamObserver.disconnectStreamSource(source)
  }

  renderStreamMessage(message) {
    this.streamMessageRenderer.render(StreamMessage.wrap(message))
  }

  clearCache() {
    this.view.clearSnapshotCache()
  }

  setProgressBarDelay(delay) {
    console.warn(
      "Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`"
    )

    this.progressBarDelay = delay
  }

  set progressBarDelay(delay) {
    config.drive.progressBarDelay = delay
  }

  get progressBarDelay() {
    return config.drive.progressBarDelay
  }

  set drive(value) {
    config.drive.enabled = value
  }

  get drive() {
    return config.drive.enabled
  }

  set formMode(value) {
    config.forms.mode = value
  }

  get formMode() {
    return config.forms.mode
  }

  get location() {
    return this.history.location
  }

  get restorationIdentifier() {
    return this.history.restorationIdentifier
  }

  get pageRefreshDebouncePeriod() {
    return this.#pageRefreshDebouncePeriod
  }

  set pageRefreshDebouncePeriod(value) {
    this.refresh = debounce(this.debouncedRefresh.bind(this), value)
    this.#pageRefreshDebouncePeriod = value
  }

  // Preloader delegate

  shouldPreloadLink(element) {
    const isUnsafe = element.hasAttribute("data-turbo-method")
    const isStream = element.hasAttribute("data-turbo-stream")
    const frameTarget = element.getAttribute("data-turbo-frame")
    const frame = frameTarget == "_top" ?
      null :
      document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])")

    if (isUnsafe || isStream || frame instanceof FrameElement) {
      return false
    } else {
      const location = new URL(element.href)

      return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)
    }
  }

  // History delegate

  historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
    if (this.enabled) {
      this.navigator.startVisit(location, restorationIdentifier, {
        action: "restore",
        historyChanged: true,
        direction
      })
    } else {
      this.adapter.pageInvalidated({
        reason: "turbo_disabled"
      })
    }
  }

  historyPoppedWithEmptyState(location) {
    this.history.replace(location)
    this.view.lastRenderedLocation = location
    this.view.cacheSnapshot()
  }

  // Scroll observer delegate

  scrollPositionChanged(position) {
    this.history.updateRestorationData({ scrollPosition: position })
  }

  // Form click observer delegate

  willSubmitFormLinkToLocation(link, location) {
    return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation)
  }

  submittedFormLinkToLocation() {}

  // Link hover observer delegate

  canPrefetchRequestToLocation(link, location) {
    return (
      this.elementIsNavigatable(link) &&
      locationIsVisitable(location, this.snapshot.rootLocation) &&
      this.navigator.linkPrefetchingIsEnabledForLocation(location)
    )
  }

  // Link click observer delegate

  willFollowLinkToLocation(link, location, event) {
    return (
      this.elementIsNavigatable(link) &&
      locationIsVisitable(location, this.snapshot.rootLocation) &&
      this.applicationAllowsFollowingLinkToLocation(link, location, event)
    )
  }

  followedLinkToLocation(link, location) {
    const action = this.getActionForLink(link)
    const acceptsStreamResponse = link.hasAttribute("data-turbo-stream")

    this.visit(location.href, { action, acceptsStreamResponse })
  }

  // Navigator delegate

  allowsVisitingLocationWithAction(location, action) {
    return this.applicationAllowsVisitingLocation(location)
  }

  visitProposedToLocation(location, options) {
    extendURLWithDeprecatedProperties(location)
    this.adapter.visitProposedToLocation(location, options)
  }

  // Visit delegate

  visitStarted(visit) {
    if (!visit.acceptsStreamResponse) {
      markAsBusy(document.documentElement)
      this.view.markVisitDirection(visit.direction)
    }
    extendURLWithDeprecatedProperties(visit.location)
    this.notifyApplicationAfterVisitingLocation(visit.location, visit.action)
  }

  visitCompleted(visit) {
    this.view.unmarkVisitDirection()
    clearBusyState(document.documentElement)
    this.notifyApplicationAfterPageLoad(visit.getTimingMetrics())
  }

  // Form submit observer delegate

  willSubmitForm(form, submitter) {
    const action = getAction(form, submitter)

    return (
      this.submissionIsNavigatable(form, submitter) &&
      locationIsVisitable(expandURL(action), this.snapshot.rootLocation)
    )
  }

  formSubmitted(form, submitter) {
    this.navigator.submitForm(form, submitter)
  }

  // Page observer delegate

  pageBecameInteractive() {
    this.view.lastRenderedLocation = this.location
    this.notifyApplicationAfterPageLoad()
  }

  pageLoaded() {
    this.history.assumeControlOfScrollRestoration()
  }

  pageWillUnload() {
    this.history.relinquishControlOfScrollRestoration()
  }

  // Stream observer delegate

  receivedMessageFromStream(message) {
    this.renderStreamMessage(message)
  }

  // Page view delegate

  viewWillCacheSnapshot() {
    this.notifyApplicationBeforeCachingSnapshot()
  }

  allowsImmediateRender({ element }, options) {
    const event = this.notifyApplicationBeforeRender(element, options)
    const {
      defaultPrevented,
      detail: { render }
    } = event

    if (this.view.renderer && render) {
      this.view.renderer.renderElement = render
    }

    return !defaultPrevented
  }

  viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
    this.view.lastRenderedLocation = this.history.location
    this.notifyApplicationAfterRender(renderMethod)
  }

  preloadOnLoadLinksForView(element) {
    this.preloader.preloadOnLoadLinksForView(element)
  }

  viewInvalidated(reason) {
    this.adapter.pageInvalidated(reason)
  }

  // Frame element

  frameLoaded(frame) {
    this.notifyApplicationAfterFrameLoad(frame)
  }

  frameRendered(fetchResponse, frame) {
    this.notifyApplicationAfterFrameRender(fetchResponse, frame)
  }

  // Application events

  applicationAllowsFollowingLinkToLocation(link, location, ev) {
    const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev)
    return !event.defaultPrevented
  }

  applicationAllowsVisitingLocation(location) {
    const event = this.notifyApplicationBeforeVisitingLocation(location)
    return !event.defaultPrevented
  }

  notifyApplicationAfterClickingLinkToLocation(link, location, event) {
    return dispatch("turbo:click", {
      target: link,
      detail: { url: location.href, originalEvent: event },
      cancelable: true
    })
  }

  notifyApplicationBeforeVisitingLocation(location) {
    return dispatch("turbo:before-visit", {
      detail: { url: location.href },
      cancelable: true
    })
  }

  notifyApplicationAfterVisitingLocation(location, action) {
    return dispatch("turbo:visit", { detail: { url: location.href, action } })
  }

  notifyApplicationBeforeCachingSnapshot() {
    return dispatch("turbo:before-cache")
  }

  notifyApplicationBeforeRender(newBody, options) {
    return dispatch("turbo:before-render", {
      detail: { newBody, ...options },
      cancelable: true
    })
  }

  notifyApplicationAfterRender(renderMethod) {
    return dispatch("turbo:render", { detail: { renderMethod } })
  }

  notifyApplicationAfterPageLoad(timing = {}) {
    return dispatch("turbo:load", {
      detail: { url: this.location.href, timing }
    })
  }

  notifyApplicationAfterFrameLoad(frame) {
    return dispatch("turbo:frame-load", { target: frame })
  }

  notifyApplicationAfterFrameRender(fetchResponse, frame) {
    return dispatch("turbo:frame-render", {
      detail: { fetchResponse },
      target: frame,
      cancelable: true
    })
  }

  // Helpers

  submissionIsNavigatable(form, submitter) {
    if (config.forms.mode == "off") {
      return false
    } else {
      const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true

      if (config.forms.mode == "optin") {
        return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null
      } else {
        return submitterIsNavigatable && this.elementIsNavigatable(form)
      }
    }
  }

  elementIsNavigatable(element) {
    const container = findClosestRecursively(element, "[data-turbo]")
    const withinFrame = findClosestRecursively(element, "turbo-frame")

    // Check if Drive is enabled on the session or we're within a Frame.
    if (config.drive.enabled || withinFrame) {
      // Element is navigatable by default, unless `data-turbo="false"`.
      if (container) {
        return container.getAttribute("data-turbo") != "false"
      } else {
        return true
      }
    } else {
      // Element isn't navigatable by default, unless `data-turbo="true"`.
      if (container) {
        return container.getAttribute("data-turbo") == "true"
      } else {
        return false
      }
    }
  }

  // Private

  getActionForLink(link) {
    return getVisitAction(link) || "advance"
  }

  get snapshot() {
    return this.view.snapshot
  }
}

// Older versions of the Turbo Native adapters referenced the
// `Location#absoluteURL` property in their implementations of
// the `Adapter#visitProposedToLocation()` and `#visitStarted()`
// methods. The Location class has since been removed in favor
// of the DOM URL API, and accordingly all Adapter methods now
// receive URL objects.
//
// We alias #absoluteURL to #toString() here to avoid crashing
// older adapters which do not expect URL objects. We should
// consider removing this support at some point in the future.

function extendURLWithDeprecatedProperties(url) {
  Object.defineProperties(url, deprecatedLocationPropertyDescriptors)
}

const deprecatedLocationPropertyDescriptors = {
  absoluteURL: {
    get() {
      return this.toString()
    }
  }
}


================================================
FILE: src/core/snapshot.js
================================================
import { queryAutofocusableElement } from "../util"

export class Snapshot {
  constructor(element) {
    this.element = element
  }

  get activeElement() {
    return this.element.ownerDocument.activeElement
  }

  get children() {
    return [...this.element.children]
  }

  hasAnchor(anchor) {
    return this.getElementForAnchor(anchor) != null
  }

  getElementForAnchor(anchor) {
    return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null
  }

  get isConnected() {
    return this.element.isConnected
  }

  get firstAutofocusableElement() {
    return queryAutofocusableElement(this.element)
  }

  get permanentElements() {
    return queryPermanentElementsAll(this.element)
  }

  getPermanentElementById(id) {
    return getPermanentElementById(this.element, id)
  }

  getPermanentElementMapForSnapshot(snapshot) {
    const permanentElementMap = {}

    for (const currentPermanentElement of this.permanentElements) {
      const { id } = currentPermanentElement
      const newPermanentElement = snapshot.getPermanentElementById(id)
      if (newPermanentElement) {
        permanentElementMap[id] = [currentPermanentElement, newPermanentElement]
      }
    }

    return permanentElementMap
  }
}

export function getPermanentElementById(node, id) {
  return node.querySelector(`#${id}[data-turbo-permanent]`)
}

export function queryPermanentElementsAll(node) {
  return node.querySelectorAll("[id][data-turbo-permanent]")
}


================================================
FILE: src/core/streams/stream_actions.js
================================================
import { session } from "../"
import { morphElements, morphChildren } from "../morphing"

export const StreamActions = {
  after() {
    this.removeDuplicateTargetSiblings()
    this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling))
  },

  append() {
    this.removeDuplicateTargetChildren()
    this.targetElements.forEach((e) => e.append(this.templateContent))
  },

  before() {
    this.removeDuplicateTargetSiblings()
    this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e))
  },

  prepend() {
    this.removeDuplicateTargetChildren()
    this.targetElements.forEach((e) => e.prepend(this.templateContent))
  },

  remove() {
    this.targetElements.forEach((e) => e.remove())
  },

  replace() {
    const method = this.getAttribute("method")

    this.targetElements.forEach((targetElement) => {
      if (method === "morph") {
        morphElements(targetElement, this.templateContent)
      } else {
        targetElement.replaceWith(this.templateContent)
      }
    })
  },

  update() {
    const method = this.getAttribute("method")

    this.targetElements.forEach((targetElement) => {
      if (method === "morph") {
        morphChildren(targetElement, this.templateContent)
      } else {
        targetElement.innerHTML = ""
        targetElement.append(this.templateContent)
      }
    })
  },

  refresh() {
    const method = this.getAttribute("method")
    const requestId = this.requestId
    const scroll = this.getAttribute("scroll")

    session.refresh(this.baseURI, { method, requestId, scroll })
  }
}


================================================
FILE: src/core/streams/stream_message.js
================================================
import { activateScriptElement, createDocumentFragment } from "../../util"

export class StreamMessage {
  static contentType = "text/vnd.turbo-stream.html"

  static wrap(message) {
    if (typeof message == "string") {
      return new this(createDocumentFragment(message))
    } else {
      return message
    }
  }

  constructor(fragment) {
    this.fragment = importStreamElements(fragment)
  }
}

function importStreamElements(fragment) {
  for (const element of fragment.querySelectorAll("turbo-stream")) {
    const streamElement = document.importNode(element, true)

    for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
      inertScriptElement.replaceWith(activateScriptElement(inertScriptElement))
    }

    element.replaceWith(streamElement)
  }

  return fragment
}


================================================
FILE: src/core/streams/stream_message_renderer.js
================================================
import { Bardo } from "../bardo"
import { getPermanentElementById, queryPermanentElementsAll } from "../snapshot"
import { around, elementIsFocusable, nextRepaint, queryAutofocusableElement, uuid } from "../../util"

export class StreamMessageRenderer {
  render({ fragment }) {
    Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => {
      withAutofocusFromFragment(fragment, () => {
        withPreservedFocus(() => {
          document.documentElement.appendChild(fragment)
        })
      })
    })
  }

  // Bardo delegate

  enteringBardo(currentPermanentElement, newPermanentElement) {
    newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true))
  }

  leavingBardo() {}
}

function getPermanentElementMapForFragment(fragment) {
  const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement)
  const permanentElementMap = {}
  for (const permanentElementInDocument of permanentElementsInDocument) {
    const { id } = permanentElementInDocument

    for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
      const elementInStream = getPermanentElementById(streamElement.templateElement.content, id)

      if (elementInStream) {
        permanentElementMap[id] = [permanentElementInDocument, elementInStream]
      }
    }
  }

  return permanentElementMap
}

async function withAutofocusFromFragment(fragment, callback) {
  const generatedID = `turbo-stream-autofocus-${uuid()}`
  const turboStreams = fragment.querySelectorAll("turbo-stream")
  const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams)
  let willAutofocusId = null

  if (elementWithAutofocus) {
    if (elementWithAutofocus.id) {
      willAutofocusId = elementWithAutofocus.id
    } else {
      willAutofocusId = generatedID
    }

    elementWithAutofocus.id = willAutofocusId
  }

  callback()
  await nextRepaint()

  const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body

  if (hasNoActiveElement && willAutofocusId) {
    const elementToAutofocus = document.getElementById(willAutofocusId)

    if (elementIsFocusable(elementToAutofocus)) {
      elementToAutofocus.focus()
    }
    if (elementToAutofocus && elementToAutofocus.id == generatedID) {
      elementToAutofocus.removeAttribute("id")
    }
  }
}

async function withPreservedFocus(callback) {
  const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, () => document.activeElement)

  const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id

  if (restoreFocusTo) {
    const elementToFocus = document.getElementById(restoreFocusTo)

    if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
      elementToFocus.focus()
    }
  }
}

function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
  for (const streamElement of nodeListOfStreamElements) {
    const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content)

    if (elementWithAutofocus) return elementWithAutofocus
  }

  return null
}


================================================
FILE: src/core/url.js
================================================
import { config } from "./config"

export function expandURL(locatable) {
  return new URL(locatable.toString(), document.baseURI)
}

export function getAnchor(url) {
  let anchorMatch
  if (url.hash) {
    return url.hash.slice(1)
    // eslint-disable-next-line no-cond-assign
  } else if ((anchorMatch = url.href.match(/#(.*)$/))) {
    return anchorMatch[1]
  }
}

export function getAction(form, submitter) {
  const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action

  return expandURL(action)
}

export function getExtension(url) {
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || ""
}

export function isPrefixedBy(baseURL, url) {
  const prefix = addTrailingSlash(url.origin + url.pathname)
  return addTrailingSlash(baseURL.href) === prefix || baseURL.href.startsWith(prefix)
}

export function locationIsVisitable(location, rootLocation) {
  return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location))
}

export function getLocationForLink(link) {
  return expandURL(link.getAttribute("href") || "")
}

export function getRequestURL(url) {
  const anchor = getAnchor(url)
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
}

export function toCacheKey(url) {
  return getRequestURL(url)
}

export function urlsAreEqual(left, right) {
  return expandURL(left).href == expandURL(right).href
}

function getPathComponents(url) {
  return url.pathname.split("/").slice(1)
}

function getLastPathComponent(url) {
  return getPathComponents(url).slice(-1)[0]
}

function addTrailingSlash(value) {
  return value.endsWith("/") ? value : value + "/"
}


================================================
FILE: src/core/view.js
================================================
import { getAnchor } from "./url"

export class View {
  #resolveRenderPromise = (_value) => {}
  #resolveInterceptionPromise = (_value) => {}

  constructor(delegate, element) {
    this.delegate = delegate
    this.element = element
  }

  // Scrolling

  scrollToAnchor(anchor) {
    const element = this.snapshot.getElementForAnchor(anchor)
    if (element) {
      this.focusElement(element)
      this.scrollToElement(element)
    } else {
      this.scrollToPosition({ x: 0, y: 0 })
    }
  }

  scrollToAnchorFromLocation(location) {
    this.scrollToAnchor(getAnchor(location))
  }

  scrollToElement(element) {
    element.scrollIntoView()
  }

  focusElement(element) {
    if (element instanceof HTMLElement) {
      if (element.hasAttribute("tabindex")) {
        element.focus()
      } else {
        element.setAttribute("tabindex", "-1")
        element.focus()
        element.removeAttribute("tabindex")
      }
    }
  }

  scrollToPosition({ x, y }) {
    this.scrollRoot.scrollTo(x, y)
  }

  scrollToTop() {
    this.scrollToPosition({ x: 0, y: 0 })
  }

  get scrollRoot() {
    return window
  }

  // Rendering

  async render(renderer) {
    const { isPreview, shouldRender, willRender, newSnapshot: snapshot } = renderer

    // A workaround to ignore tracked element mismatch reloads when performing
    // a promoted Visit from a frame navigation
    const shouldInvalidate = willRender

    if (shouldRender) {
      try {
        this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve))
        this.renderer = renderer
        await this.prepareToRenderSnapshot(renderer)

        const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve))
        const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement, renderMethod: this.renderer.renderMethod }
        const immediateRender = this.delegate.allowsImmediateRender(snapshot, options)
        if (!immediateRender) await renderInterception

        await this.renderSnapshot(renderer)
        this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod)
        this.delegate.preloadOnLoadLinksForView(this.element)
        this.finishRenderingSnapshot(renderer)
      } finally {
        delete this.renderer
        this.#resolveRenderPromise(undefined)
        delete this.renderPromise
      }
    } else if (shouldInvalidate) {
      this.invalidate(renderer.reloadReason)
    }
  }

  invalidate(reason) {
    this.delegate.viewInvalidated(reason)
  }

  async prepareToRenderSnapshot(renderer) {
    this.markAsPreview(renderer.isPreview)
    await renderer.prepareToRender()
  }

  markAsPreview(isPreview) {
    if (isPreview) {
      this.element.setAttribute("data-turbo-preview", "")
    } else {
      this.element.removeAttribute("data-turbo-preview")
    }
  }

  markVisitDirection(direction) {
    this.element.setAttribute("data-turbo-visit-direction", direction)
  }

  unmarkVisitDirection() {
    this.element.removeAttribute("data-turbo-visit-direction")
  }

  async renderSnapshot(renderer) {
    await renderer.render()
  }

  finishRenderingSnapshot(renderer) {
    renderer.finishRendering()
  }
}


================================================
FILE: src/elements/frame_element.js
================================================
export const FrameLoadingStyle = {
  eager: "eager",
  lazy: "lazy"
}

/**
 * Contains a fragment of HTML which is updated based on navigation within
 * it (e.g. via links or form submissions).
 *
 * @customElement turbo-frame
 * @example
 *   <turbo-frame id="messages">
 *     <a href="/messages/expanded">
 *       Show all expanded messages in this frame.
 *     </a>
 *
 *     <form action="/messages">
 *       Show response from this form within this frame.
 *     </form>
 *   </turbo-frame>
 */
export class FrameElement extends HTMLElement {
  static delegateConstructor = undefined

  loaded = Promise.resolve()

  static get observedAttributes() {
    return ["disabled", "loading", "src"]
  }

  constructor() {
    super()
    this.delegate = new FrameElement.delegateConstructor(this)
  }

  connectedCallback() {
    this.delegate.connect()
  }

  disconnectedCallback() {
    this.delegate.disconnect()
  }

  reload() {
    return this.delegate.sourceURLReloaded()
  }

  attributeChangedCallback(name) {
    if (name == "loading") {
      this.delegate.loadingStyleChanged()
    } else if (name == "src") {
      this.delegate.sourceURLChanged()
    } else if (name == "disabled") {
      this.delegate.disabledChanged()
    }
  }

  /**
   * Gets the URL to lazily load source HTML from
   */
  get src() {
    return this.getAttribute("src")
  }

  /**
   * Sets the URL to lazily load source HTML from
   */
  set src(value) {
    if (value) {
      this.setAttribute("src", value)
    } else {
      this.removeAttribute("src")
    }
  }

  /**
   * Gets the refresh mode for the frame.
   */
  get refresh() {
    return this.getAttribute("refresh")
  }

  /**
   * Sets the refresh mode for the frame.
   */
  set refresh(value) {
    if (value) {
      this.setAttribute("refresh", value)
    } else {
      this.removeAttribute("refresh")
    }
  }

  get shouldReloadWithMorph() {
    return this.src && this.refresh === "morph"
  }

  /**
   * Determines if the element is loading
   */
  get loading() {
    return frameLoadingStyleFromString(this.getAttribute("loading") || "")
  }

  /**
   * Sets the value of if the element is loading
   */
  set loading(value) {
    if (value) {
      this.setAttribute("loading", value)
    } else {
      this.removeAttribute("loading")
    }
  }

  /**
   * Gets the disabled state of the frame.
   *
   * If disabled, no requests will be intercepted by the frame.
   */
  get disabled() {
    return this.hasAttribute("disabled")
  }

  /**
   * Sets the disabled state of the frame.
   *
   * If disabled, no requests will be intercepted by the frame.
   */
  set disabled(value) {
    if (value) {
      this.setAttribute("disabled", "")
    } else {
      this.removeAttribute("disabled")
    }
  }

  /**
   * Gets the autoscroll state of the frame.
   *
   * If true, the frame will be scrolled into view automatically on update.
   */
  get autoscroll() {
    return this.hasAttribute("autoscroll")
  }

  /**
   * Sets the autoscroll state of the frame.
   *
   * If true, the frame will be scrolled into view automatically on update.
   */
  set autoscroll(value) {
    if (value) {
      this.setAttribute("autoscroll", "")
    } else {
      this.removeAttribute("autoscroll")
    }
  }

  /**
   * Determines if the element has finished loading
   */
  get complete() {
    return !this.delegate.isLoading
  }

  /**
   * Gets the active state of the frame.
   *
   * If inactive, source changes will not be observed.
   */
  get isActive() {
    return this.ownerDocument === document && !this.isPreview
  }

  /**
   * Sets the active state of the frame.
   *
   * If inactive, source changes will not be observed.
   */
  get isPreview() {
    return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview")
  }
}

function frameLoadingStyleFromString(style) {
  switch (style.toLowerCase()) {
    case "lazy":
      return FrameLoadingStyle.lazy
    default:
      return FrameLoadingStyle.eager
  }
}


================================================
FILE: src/elements/index.js
================================================
import { FrameController } from "../core/frames/frame_controller"
import { FrameElement } from "./frame_element"
import { StreamElement } from "./stream_element"
import { StreamSourceElement } from "./stream_source_element"

FrameElement.delegateConstructor = FrameController

export * from "./frame_element"
export * from "./stream_element"
export * from "./stream_source_element"

if (customElements.get("turbo-frame") === undefined) {
  customElements.define("turbo-frame", FrameElement)
}

if (customElements.get("turbo-stream") === undefined) {
  customElements.define("turbo-stream", StreamElement)
}

if (customElements.get("turbo-stream-source") === undefined) {
  customElements.define("turbo-stream-source", StreamSourceElement)
}


================================================
FILE: src/elements/stream_element.js
================================================
import { StreamActions } from "../core/streams/stream_actions"
import { nextRepaint } from "../util"

// <turbo-stream action=replace target=id><template>...

/**
 * Renders updates to the page from a stream of messages.
 *
 * Using the `action` attribute, this can be configured one of eight ways:
 *
 * - `after` - inserts the result after the target
 * - `append` - appends the result to the target
 * - `before` - inserts the result before the target
 * - `prepend` - prepends the result to the target
 * - `refresh` - initiates a page refresh
 * - `remove` - removes the target
 * - `replace` - replaces the outer HTML of the target
 * - `update` - replaces the inner HTML of the target
 *
 * @customElement turbo-stream
 * @example
 *   <turbo-stream action="append" target="dom_id">
 *     <template>
 *       Content to append to target designated with the dom_id.
 *     </template>
 *   </turbo-stream>
 */
export class StreamElement extends HTMLElement {
  static async renderElement(newElement) {
    await newElement.performAction()
  }

  async connectedCallback() {
    try {
      await this.render()
    } catch (error) {
      console.error(error)
    } finally {
      this.disconnect()
    }
  }

  async render() {
    return (this.renderPromise ??= (async () => {
      const event = this.beforeRenderEvent

      if (this.dispatchEvent(event)) {
        await nextRepaint()
        await event.detail.render(this)
      }
    })())
  }

  disconnect() {
    try {
      this.remove()
      // eslint-disable-next-line no-empty
    } catch {}
  }

  /**
   * Removes duplicate children (by ID)
   */
  removeDuplicateTargetChildren() {
    this.duplicateChildren.forEach((c) => c.remove())
  }

  /**
   * Gets the list of duplicate children (i.e. those with the same ID)
   */
  get duplicateChildren() {
    const existingChildren = this.targetElements.flatMap((e) => [...e.children]).filter((c) => !!c.getAttribute("id"))
    const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.getAttribute("id")).map((c) => c.getAttribute("id"))

    return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute("id")))
  }

  /**
  * Removes duplicate siblings (by ID)
  */
  removeDuplicateTargetSiblings() {
    this.duplicateSiblings.forEach((c) => c.remove())
  }

  /**
  * Gets the list of duplicate siblings (i.e. those with the same ID)
  */
  get duplicateSiblings() {
    const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id)
    const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id)

    return existingChildren.filter((c) => newChildrenIds.includes(c.id))
  }

  /**
   * Gets the action function to be performed.
   */
  get performAction() {
    if (this.action) {
      const actionFunction = StreamActions[this.action]
      if (actionFunction) {
        return actionFunction
      }
      this.#raise("unknown action")
    }
    this.#raise("action attribute is missing")
  }

  /**
   * Gets the target elements which the template will be rendered to.
   */
  get targetElements() {
    if (this.target) {
      return this.targetElementsById
    } else if (this.targets) {
      return this.targetElementsByQuery
    } else {
      this.#raise("target or targets attribute is missing")
    }
  }

  /**
   * Gets the contents of the main `<template>`.
   */
  get templateContent() {
    return this.templateElement.content.cloneNode(true)
  }

  /**
   * Gets the main `<template>` used for rendering
   */
  get templateElement() {
    if (this.firstElementChild === null) {
      const template = this.ownerDocument.createElement("template")
      this.appendChild(template)
      return template
    } else if (this.firstElementChild instanceof HTMLTemplateElement) {
      return this.firstElementChild
    }
    this.#raise("first child element must be a <template> element")
  }

  /**
   * Gets the current action.
   */
  get action() {
    return this.getAttribute("action")
  }

  /**
   * Gets the current target (an element ID) to which the result will
   * be rendered.
   */
  get target() {
    return this.getAttribute("target")
  }

  /**
   * Gets the current "targets" selector (a CSS selector)
   */
  get targets() {
    return this.getAttribute("targets")
  }

  /**
   * Reads the request-id attribute
   */
  get requestId() {
    return this.getAttribute("request-id")
  }

  #raise(message) {
    throw new Error(`${this.description}: ${message}`)
  }

  get description() {
    return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? "<turbo-stream>"
  }

  get beforeRenderEvent() {
    return new CustomEvent("turbo:before-stream-render", {
      bubbles: true,
      cancelable: true,
      detail: { newStream: this, render: StreamElement.renderElement }
    })
  }

  get targetElementsById() {
    const element = this.ownerDocument?.getElementById(this.target)

    if (element !== null) {
      return [element]
    } else {
      return []
    }
  }

  get targetElementsByQuery() {
    const elements = this.ownerDocument?.querySelectorAll(this.targets)

    if (elements.length !== 0) {
      return Array.prototype.slice.call(elements)
    } else {
      return []
    }
  }
}


================================================
FILE: src/elements/stream_source_element.js
================================================
import { connectStreamSource, disconnectStreamSource } from "../core/index"

export class StreamSourceElement extends HTMLElement {
  streamSource = null

  connectedCallback() {
    this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src)

    connectStreamSource(this.streamSource)
  }

  disconnectedCallback() {
    if (this.streamSource) {
      this.streamSource.close()

      disconnectStreamSource(this.streamSource)
    }
  }

  get src() {
    return this.getAttribute("src") || ""
  }
}


================================================
FILE: src/http/fetch.js
================================================
import { uuid } from "../util"
import { LimitedSet } from "../core/drive/limited_set"

export const recentRequests = new LimitedSet(20)

function fetchWithTurboHeaders(url, options = {}) {
  const modifiedHeaders = new Headers(options.headers || {})
  const requestUID = uuid()
  recentRequests.add(requestUID)
  modifiedHeaders.append("X-Turbo-Request-Id", requestUID)

  return window.fetch(url, {
    ...options,
    headers: modifiedHeaders
  })
}

export { fetchWithTurboHeaders as fetch }


================================================
FILE: src/http/fetch_request.js
================================================
import { FetchResponse } from "./fetch_response"
import { expandURL } from "../core/url"
import { dispatch } from "../util"
import { fetch } from "./fetch"

export function fetchMethodFromString(method) {
  switch (method.toLowerCase()) {
    case "get":
      return FetchMethod.get
    case "post":
      return FetchMethod.post
    case "put":
      return FetchMethod.put
    case "patch":
      return FetchMethod.patch
    case "delete":
      return FetchMethod.delete
  }
}

export const FetchMethod = {
  get: "get",
  post: "post",
  put: "put",
  patch: "patch",
  delete: "delete"
}

export function fetchEnctypeFromString(encoding) {
  switch (encoding.toLowerCase()) {
    case FetchEnctype.multipart:
      return FetchEnctype.multipart
    case FetchEnctype.plain:
      return FetchEnctype.plain
    default:
      return FetchEnctype.urlEncoded
  }
}

export const FetchEnctype = {
  urlEncoded: "application/x-www-form-urlencoded",
  multipart: "multipart/form-data",
  plain: "text/plain"
}

export class FetchRequest {
  abortController = new AbortController()
  #resolveRequestPromise = (_value) => {}

  constructor(delegate, method, location, requestBody = new URLSearchParams(), target = null, enctype = FetchEnctype.urlEncoded) {
    const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype)

    this.delegate = delegate
    this.url = url
    this.target = target
    this.fetchOptions = {
      credentials: "same-origin",
      redirect: "follow",
      method: method.toUpperCase(),
      headers: { ...this.defaultHeaders },
      body: body,
      signal: this.abortSignal,
      referrer: this.delegate.referrer?.href
    }
    this.enctype = enctype
  }

  get method() {
    return this.fetchOptions.method
  }

  set method(value) {
    const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData()
    const fetchMethod = fetchMethodFromString(value) || FetchMethod.get

    this.url.search = ""

    const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype)

    this.url = url
    this.fetchOptions.body = body
    this.fetchOptions.method = fetchMethod.toUpperCase()
  }

  get headers() {
    return this.fetchOptions.headers
  }

  set headers(value) {
    this.fetchOptions.headers = value
  }

  get body() {
    if (this.isSafe) {
      return this.url.searchParams
    } else {
      return this.fetchOptions.body
    }
  }

  set body(value) {
    this.fetchOptions.body = value
  }

  get location() {
    return this.url
  }

  get params() {
    return this.url.searchParams
  }

  get entries() {
    return this.body ? Array.from(this.body.entries()) : []
  }

  cancel() {
    this.abortController.abort()
  }

  async perform() {
    const { fetchOptions } = this
    this.delegate.prepareRequest(this)
    const event = await this.#allowRequestToBeIntercepted(fetchOptions)
    try {
      this.delegate.requestStarted(this)

      if (event.detail.fetchRequest) {
        this.response = event.detail.fetchRequest.response
      } else {
        this.response = fetch(this.url.href, fetchOptions)
      }

      const response = await this.response
      return await this.receive(response)
    } catch (error) {
      if (error.name !== "AbortError") {
        if (this.#willDelegateErrorHandling(error)) {
          this.delegate.requestErrored(this, error)
        }
        throw error
      }
    } finally {
      this.delegate.requestFinished(this)
    }
  }

  async receive(response) {
    const fetchResponse = new FetchResponse(response)
    const event = dispatch("turbo:before-fetch-response", {
      cancelable: true,
      detail: { fetchResponse },
      target: this.target
    })
    if (event.defaultPrevented) {
      this.delegate.requestPreventedHandlingResponse(this, fetchResponse)
    } else if (fetchResponse.succeeded) {
      this.delegate.requestSucceededWithResponse(this, fetchResponse)
    } else {
      this.delegate.requestFailedWithResponse(this, fetchResponse)
    }
    return fetchResponse
  }

  get defaultHeaders() {
    return {
      Accept: "text/html, application/xhtml+xml"
    }
  }

  get isSafe() {
    return isSafe(this.method)
  }

  get abortSignal() {
    return this.abortController.signal
  }

  acceptResponseType(mimeType) {
    this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ")
  }

  async #allowRequestToBeIntercepted(fetchOptions) {
    const requestInterception = new Promise((resolve) => (this.#resolveRequestPromise = resolve))
    const event = dispatch("turbo:before-fetch-request", {
      cancelable: true,
      detail: {
        fetchOptions,
        url: this.url,
        resume: this.#resolveRequestPromise
      },
      target: this.target
    })
    this.url = event.detail.url
    if (event.defaultPrevented) await requestInterception

    return event
  }

  #willDelegateErrorHandling(error) {
    const event = dispatch("turbo:fetch-request-error", {
      target: this.target,
      cancelable: true,
      detail: { request: this, error: error }
    })

    return !event.defaultPrevented
  }
}

export function isSafe(fetchMethod) {
  return fetchMethodFromString(fetchMethod) == FetchMethod.get
}

function buildResourceAndBody(resource, method, requestBody, enctype) {
  const searchParams =
    Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams

  if (isSafe(method)) {
    return [mergeIntoURLSearchParams(resource, searchParams), null]
  } else if (enctype == FetchEnctype.urlEncoded) {
    return [resource, searchParams]
  } else {
    return [resource, requestBody]
  }
}

function entriesExcludingFiles(requestBody) {
  const entries = []

  for (const [name, value] of requestBody) {
    if (value instanceof File) continue
    else entries.push([name, value])
  }

  return entries
}

function mergeIntoURLSearchParams(url, requestBody) {
  const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody))

  url.search = searchParams.toString()

  return url
}


================================================
FILE: src/http/fetch_response.js
================================================
import { expandURL } from "../core/url"

export class FetchResponse {
  constructor(response) {
    this.response = response
  }

  get succeeded() {
    return this.response.ok
  }

  get failed() {
    return !this.succeeded
  }

  get clientError() {
    return this.statusCode >= 400 && this.statusCode <= 499
  }

  get serverError() {
    return this.statusCode >= 500 && this.statusCode <= 599
  }

  get redirected() {
    return this.response.redirected
  }

  get location() {
    return expandURL(this.response.url)
  }

  get isHTML() {
    return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/)
  }

  get statusCode() {
    return this.response.status
  }

  get contentType() {
    return this.header("Content-Type")
  }

  get responseText() {
    return this.response.clone().text()
  }

  get responseHTML() {
    if (this.isHTML) {
      return this.response.clone().text()
    } else {
      return Promise.resolve(undefined)
    }
  }

  header(name) {
    return this.response.headers.get(name)
  }
}


================================================
FILE: src/http/index.js
================================================
export * from "./fetch_request"
export * from "./fetch_response"


================================================
FILE: src/index.js
================================================
import "./polyfills"
import "./elements"
import "./script_warning"
import { StreamActions } from "./core/streams/stream_actions"

import * as Turbo from "./core"

window.Turbo = { ...Turbo, StreamActions }
Turbo.start()

export { StreamActions }
export * from "./core"
export * from "./elements"
export * from "./http"


================================================
FILE: src/observers/appearance_observer.js
================================================
export class AppearanceObserver {
  started = false

  constructor(delegate, element) {
    this.delegate = delegate
    this.element = element
    this.intersectionObserver = new IntersectionObserver(this.intersect)
  }

  start() {
    if (!this.started) {
      this.started = true
      this.intersectionObserver.observe(this.element)
    }
  }

  stop() {
    if (this.started) {
      this.started = false
      this.intersectionObserver.unobserve(this.element)
    }
  }

  intersect = (entries) => {
    const lastEntry = entries.slice(-1)[0]
    if (lastEntry?.isIntersecting) {
      this.delegate.elementAppearedInViewport(this.element)
    }
  }
}


================================================
FILE: src/observers/cache_observer.js
================================================
export class CacheObserver {
  selector = "[data-turbo-temporary]"

  started = false

  start() {
    if (!this.started) {
      this.started = true
      addEventListener("turbo:before-cache", this.removeTemporaryElements, false)
    }
  }

  stop() {
    if (this.started) {
      this.started = false
      removeEventListener("turbo:before-cache", this.removeTemporaryElements, false)
    }
  }

  removeTemporaryElements = (_event) => {
    for (const element of this.temporaryElements) {
      element.remove()
    }
  }

  get temporaryElements() {
    return [...document.querySelectorAll(this.selector)]
  }
}


================================================
FILE: src/observers/form_link_click_observer.js
================================================
import { LinkClickObserver } from "./link_click_observer"
import { getVisitAction } from "../util"

export class FormLinkClickObserver {
  constructor(delegate, element) {
    this.delegate = delegate
    this.linkInterceptor = new LinkClickObserver(this, element)
  }

  start() {
    this.linkInterceptor.start()
  }

  stop() {
    this.linkInterceptor.stop()
  }

  // Link hover observer delegate

  canPrefetchRequestToLocation(link, location) {
    return false
  }

  prefetchAndCacheRequestToLocation(link, location) {
    return
  }

  // Link click observer delegate

  willFollowLinkToLocation(link, location, originalEvent) {
    return (
      this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
      (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"))
    )
  }

  followedLinkToLocation(link, location) {
    const form = document.createElement("form")

    const type = "hidden"
    for (const [name, value] of location.searchParams) {
      form.append(Object.assign(document.createElement("input"), { type, name, value }))
    }

    const action = Object.assign(location, { search: "" })
    form.setAttribute("data-turbo", "true")
    form.setAttribute("action", action.href)
    form.setAttribute("hidden", "")

    const method = link.getAttribute("data-turbo-method")
    if (method) form.setAttribute("method", method)

    const turboFrame = link.getAttribute("data-turbo-frame")
    if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame)

    const turboAction = getVisitAction(link)
    if (turboAction) form.setAttribute("data-turbo-action", turboAction)

    const turboConfirm = link.getAttribute("data-turbo-confirm")
    if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm)

    const turboStream = link.hasAttribute("data-turbo-stream")
    if (turboStream) form.setAttribute("data-turbo-stream", "")

    this.delegate.submittedFormLinkToLocation(link, location, form)

    document.body.appendChild(form)
    form.addEventListener("turbo:submit-end", () => form.remove(), { once: true })
    requestAnimationFrame(() => form.requestSubmit())
  }
}


================================================
FILE: src/observers/form_submit_observer.js
================================================
import { doesNotTargetIFrame } from "../util"

export class FormSubmitObserver {
  started = false

  constructor(delegate, eventTarget) {
    this.delegate = delegate
    this.eventTarget = eventTarget
  }

  start() {
    if (!this.started) {
      this.eventTarget.addEventListener("submit", this.submitCaptured, true)
      this.started = true
    }
  }

  stop() {
    if (this.started) {
      this.eventTarget.removeEventListener("submit", this.submitCaptured, true)
      this.started = false
    }
  }

  submitCaptured = () => {
    this.eventTarget.removeEventListener("submit", this.submitBubbled, false)
    this.eventTarget.addEventListener("submit", this.submitBubbled, false)
  }

  submitBubbled = (event) => {
    if (!event.defaultPrevented) {
      const form = event.target instanceof HTMLFormElement ? event.target : undefined
      const submitter = event.submitter || undefined

      if (
        form &&
        submissionDoesNotDismissDialog(form, submitter) &&
        submissionDoesNotTargetIFrame(form, submitter) &&
        this.delegate.willSubmitForm(form, submitter)
      ) {
        event.preventDefault()
        event.stopImmediatePropagation()
        this.delegate.formSubmitted(form, submitter)
      }
    }
  }
}

function submissionDoesNotDismissDialog(form, submitter) {
  const method = submitter?.getAttribute("formmethod") || form.getAttribute("method")

  return method != "dialog"
}

function submissionDoesNotTargetIFrame(form, submitter) {
  const target = submitter?.getAttribute("formtarget") || form.getAttribute("target")

  return doesNotTargetIFrame(target)
}


================================================
FILE: src/observers/link_click_observer.js
================================================
import { getLocationForLink } from "../core/url"
import { doesNotTargetIFrame, findLinkFromClickTarget } from "../util"

export class LinkClickObserver {
  started = false

  constructor(delegate, eventTarget) {
    this.delegate = delegate
    this.eventTarget = eventTarget
  }

  start() {
    if (!this.started) {
      this.eventTarget.addEventListener("click", this.clickCaptured, true)
      this.started = true
    }
  }

  stop() {
    if (this.started) {
      this.eventTarget.removeEventListener("click", this.clickCaptured, true)
      this.started = false
    }
  }

  clickCaptured = () => {
    this.eventTarget.removeEventListener("click", this.clickBubbled, false)
    this.eventTarget.addEventListener("click", this.clickBubbled, false)
  }

  clickBubbled = (event) => {
    if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
      const target = (event.composedPath && event.composedPath()[0]) || event.target
      const link = findLinkFromClickTarget(target)
      if (link && doesNotTargetIFrame(link.target)) {
        const location = getLocationForLink(link)
        if (this.delegate.willFollowLinkToLocation(link, location, event)) {
          event.preventDefault()
          this.delegate.followedLinkToLocation(link, location)
        }
      }
    }
  }

  clickEventIsSignificant(event) {
    return !(
      (event.target && event.target.isContentEditable) ||
      event.defaultPrevented ||
      event.which > 1 ||
      event.altKey ||
      event.ctrlKey ||
      event.metaKey ||
      event.shiftKey
    )
  }
}


================================================
FILE: src/observers/link_prefetch_observer.js
================================================
import { getLocationForLink } from "../core/url"
import {
  dispatch,
  getMetaContent,
  findClosestRecursively
} from "../util"

import { FetchMethod, FetchRequest } from "../http/fetch_request"
import { prefetchCache, cacheTtl } from "../core/drive/prefetch_cache"

export class LinkPrefetchObserver {
  started = false
  #prefetchedLink = null

  constructor(delegate, eventTarget) {
    this.delegate = delegate
    this.eventTarget = eventTarget
  }

  start() {
    if (this.started) return

    if (this.eventTarget.readyState === "loading") {
      this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, { once: true })
    } else {
      this.#enable()
    }
  }

  stop() {
    if (!this.started) return

    this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
      capture: true,
      passive: true
    })
    this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
      capture: true,
      passive: true
    })

    this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true)
    this.started = false
  }

  #enable = () => {
    this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
      capture: true,
      passive: true
    })
    this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
      capture: true,
      passive: true
    })

    this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true)
    this.started = true
  }

  #tryToPrefetchRequest = (event) => {
    if (getMetaContent("turbo-prefetch") === "false") return

    const target = event.target
    const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])")

    if (isLink && this.#isPrefetchable(target)) {
      const link = target
      const location = getLocationForLink(link)

      if (this.delegate.canPrefetchRequestToLocation(link, location)) {
        this.#prefetchedLink = link

        const fetchRequest = new FetchRequest(
          this,
          FetchMethod.get,
          location,
          new URLSearchParams(),
          target
        )

        fetchRequest.fetchOptions.priority = "low"

        prefetchCache.putLater(location, fetchRequest, this.#cacheTtl)
      }
    }
  }

  #cancelRequestIfObsolete = (event) => {
    if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest()
  }

  #cancelPrefetchRequest = () => {
    prefetchCache.clear()
    this.#prefetchedLink = null
  }

  #tryToUsePrefetchedRequest = (event) => {
    if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
      const cached = prefetchCache.get(event.detail.url)

      if (cached) {
        // User clicked link, use cache response
        event.detail.fetchRequest = cached
      }

      prefetchCache.clear()
    }
  }

  prepareRequest(request) {
    const link = request.target

    request.headers["X-Sec-Purpose"] = "prefetch"

    const turboFrame = link.closest("turbo-frame")
    const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id

    if (turboFrameTarget && turboFrameTarget !== "_top") {
      request.headers["Turbo-Frame"] = turboFrameTarget
    }
  }

  // Fetch request interface

  requestSucceededWithResponse() {}

  requestStarted(fetchRequest) {}

  requestErrored(fetchRequest) {}

  requestFinished(fetchRequest) {}

  requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}

  requestFailedWithResponse(fetchRequest, fetchResponse) {}

  get #cacheTtl() {
    return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl
  }

  #isPrefetchable(link) {
    const href = link.getAttribute("href")

    if (!href) return false

    if (unfetchableLink(link)) return false
    if (linkToTheSamePage(link)) return false
    if (linkOptsOut(link)) return false
    if (nonSafeLink(link)) return false
    if (eventPrevented(link)) return false

    return true
  }
}

const unfetchableLink = (link) => {
  return link.origin !== document.location.origin || !["http:", "https:"].includes(link.protocol) || link.hasAttribute("target")
}

const linkToTheSamePage = (link) => {
  return (link.pathname + link.search === document.location.pathname + document.location.search) || link.href.startsWith("#")
}

const linkOptsOut = (link) => {
  if (link.getAttribute("data-turbo-prefetch") === "false") return true
  if (link.getAttribute("data-turbo") === "false") return true

  const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]")
  if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true

  return false
}

const nonSafeLink = (link) => {
  const turboMethod = link.getAttribute("data-turbo-method")
  if (turboMethod && turboMethod.toLowerCase() !== "get") return true

  if (isUJS(link)) return true
  if (link.hasAttribute("data-turbo-confirm")) return true
  if (link.hasAttribute("data-turbo-stream")) return true

  return false
}

const isUJS = (link) => {
  return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method")
}

const eventPrevented = (link) => {
  const event = dispatch("turbo:before-prefetch", { target: link, cancelable: true })
  return event.defaultPrevented
}


================================================
FILE: src/observers/page_observer.js
================================================
export const PageStage = {
  initial: 0,
  loading: 1,
  interactive: 2,
  complete: 3
}

export class PageObserver {
  stage = PageStage.initial
  started = false

  constructor(delegate) {
    this.delegate = delegate
  }

  start() {
    if (!this.started) {
      if (this.stage == PageStage.initial) {
        this.stage = PageStage.loading
      }
      document.addEventListener("readystatechange", this.interpretReadyState, false)
      addEventListener("pagehide", this.pageWillUnload, false)
      this.started = true
    }
  }

  stop() {
    if (this.started) {
      document.removeEventListener("readystatechange", this.interpretReadyState, false)
      removeEventListener("pagehide", this.pageWillUnload, false)
      this.started = false
    }
  }

  interpretReadyState = () => {
    const { readyState } = this
    if (readyState == "interactive") {
      this.pageIsInteractive()
    } else if (readyState == "complete") {
      this.pageIsComplete()
    }
  }

  pageIsInteractive() {
    if (this.stage == PageStage.loading) {
      this.stage = PageStage.interactive
      this.delegate.pageBecameInteractive()
    }
  }

  pageIsComplete() {
    this.pageIsInteractive()
    if (this.stage == PageStage.interactive) {
      this.stage = PageStage.complete
      this.delegate.pageLoaded()
    }
  }

  pageWillUnload = () => {
    this.delegate.pageWillUnload()
  }

  get readyState() {
    return document.readyState
  }
}


================================================
FILE: src/observers/scroll_observer.js
================================================
export class ScrollObserver {
  started = false

  constructor(delegate) {
    this.delegate = delegate
  }

  start() {
    if (!this.started) {
      addEventListener("scroll", this.onScroll, false)
      this.onScroll()
      this.started = true
    }
  }

  stop() {
    if (this.started) {
      removeEventListener("scroll", this.onScroll, false)
      this.started = false
    }
  }

  onScroll = () => {
    this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset })
  }

  // Private

  updatePosition(position) {
    this.delegate.scrollPositionChanged(position)
  }
}


================================================
FILE: src/observers/stream_observer.js
================================================
import { FetchResponse } from "../http/fetch_response"
import { StreamMessage } from "../core/streams/stream_message"

export class StreamObserver {
  sources = new Set()
  #started = false

  constructor(delegate) {
    this.delegate = delegate
  }

  start() {
    if (!this.#started) {
      this.#started = true
      addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false)
    }
  }

  stop() {
    if (this.#started) {
      this.#started = false
      removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false)
    }
  }

  connectStreamSource(source) {
    if (!this.streamSourceIsConnected(source)) {
      this.sources.add(source)
      source.addEventListener("message", this.receiveMessageEvent, false)
    }
  }

  disconnectStreamSource(source) {
    if (this.streamSourceIsConnected(source)) {
      this.sources.delete(source)
      source.removeEventListener("message", this.receiveMessageEvent, false)
    }
  }

  streamSourceIsConnected(source) {
    return this.sources.has(source)
  }

  inspectFetchResponse = (event) => {
    const response = fetchResponseFromEvent(event)
    if (response && fetchResponseIsStream(response)) {
      event.preventDefault()
      this.receiveMessageResponse(response)
    }
  }

  receiveMessageEvent = (event) => {
    if (this.#started && typeof event.data == "string") {
      this.receiveMessageHTML(event.data)
    }
  }

  async receiveMessageResponse(response) {
    const html = await response.responseHTML
    if (html) {
      this.receiveMessageHTML(html)
    }
  }

  receiveMessageHTML(html) {
    this.delegate.receivedMessageFromStream(StreamMessage.wrap(html))
  }
}

function fetchResponseFromEvent(event) {
  const fetchResponse = event.detail?.fetchResponse
  if (fetchResponse instanceof FetchResponse) {
    return fetchResponse
  }
}

function fetchResponseIsStream(response) {
  const contentType = response.contentType ?? ""
  return contentType.startsWith(StreamMessage.contentType)
}


================================================
FILE: src/polyfills/index.js
================================================


================================================
FILE: src/script_warning.js
================================================
import { unindent } from "./util"
;(() => {
  const scriptElement = document.currentScript
  if (!scriptElement) return
  if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return

  let element = scriptElement.parentElement
  while (element) {
    if (element == document.body) {
      return console.warn(
        unindent`
        You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!

        Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.

        For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements

        ——
        Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
      `,
        scriptElement.outerHTML
      )
    }

    element = element.parentElement
  }
})()


================================================
FILE: src/tests/fixtures/422.html
================================================
<html>
  <head>
    <title>Unprocessable Content</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
  </head>
  <body>
    <h1>Unprocessable Content</h1>

    <turbo-frame id="frame">
      <h2>Frame: Unprocessable Content</h2>
    </turbo-frame>
  </body>
</html>


================================================
FILE: src/tests/fixtures/422_morph.html
================================================
<html>
  <head>
    <meta name="turbo-refresh-method" content="morph">
    <meta name="turbo-refresh-scroll" content="preserve">

    <title>Unprocessable Content</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
  </head>
  <body>
    <h1>Unprocessable Content</h1>

    <turbo-frame id="frame">
      <h2>Frame: Unprocessable Content</h2>
    </turbo-frame>
  </body>
</html>


================================================
FILE: src/tests/fixtures/422_tall.html
================================================
<html>
  <head>
    <title>Unprocessable Content</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
  </head>
  <body>
    <main style="height: 1000vh">
      <h1>Unprocessable Content</h1>
      <turbo-frame id="frame">
        <h2>Frame: Unprocessable Content</h2>
      </turbo-frame>
    </main>
  </body>
</html>


================================================
FILE: src/tests/fixtures/500.html
================================================
<html>
  <head>
    <title>Internal Server Error</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
  </head>
  <body>
    <h1>Internal Server Error</h1>

    <turbo-frame id="frame">
      <h2>Frame: Internal Server Error</h2>
    </turbo-frame>
  </body>
</html>


================================================
FILE: src/tests/fixtures/additional_assets.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Additional assets</title>
    <link rel="stylesheet" type="text/css" href="/src/tests/fixtures/test.css">
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
    <noscript>
      <link rel="stylesheet" type="text/css" href="/src/tests/fixtures/noscript.css">
    </noscript>
  </head>
  <body>
    <h1>Additional assets</h1>
  </body>
</html>


================================================
FILE: src/tests/fixtures/additional_script.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Additional assets</title>
    <link rel="stylesheet" type="text/css" href="/src/tests/fixtures/test.css">
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
    <script id="additional" src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>Additional assets</h1>
  </body>
</html>


================================================
FILE: src/tests/fixtures/async_script.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Turbo</title>
    <script src="/src/tests/fixtures/test.js"></script>
    <script>
      addEventListener("DOMContentLoaded", function() {
        setTimeout(function() {
          var script = document.createElement("script")
          script.src = "/dist/turbo.es2017-umd.js"
          script.setAttribute("async", "")
          document.head.appendChild(script)
        }, 1)
      })
    </script>
  </head>
  <body>
    <section>
      <h1>Async script</h1>
      <p><a href="async_script_2.html" id="async-link">Async script 2</a></p>
    </section>
  </body>
</html>


================================================
FILE: src/tests/fixtures/async_script_2.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Turbo</title>
    <script src="/src/tests/fixtures/test.js"></script>
    <script src="/dist/turbo.es2017-umd.js" async></script>
  </head>
  <body>
    <section>
      <h1>Async script 2</h1>
    </section>
  </body>
</html>


================================================
FILE: src/tests/fixtures/autofocus-inert.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Autofocus</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>Autofocus With Inert Elements</h1>
    <dialog>
      <button id="dialog-autofocus-element" autofocus>dialog[autofocus]</button>
    </dialog>
    <details>
      <button id="details-autofocus-element" autofocus>details[autofocus]</button>
    </details>
    <div hidden>
      <button id="hidden-autofocus-element" autofocus>div[hidden][autofocus]</button>
    </div>
    <div inert>
      <button id="inert-autofocus-element" autofocus>div[inert][autofocus]</button>
    </div>
    <button id="disabled-autofocus-element" disabled autofocus>button[disabled][autofocus]</button>
    <form disabled>
      <button id="visible-autofocus-element" autofocus>button[autofocus]</button>
    </form>
  </body>
</html>


================================================
FILE: src/tests/fixtures/autofocus.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Autofocus</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
    <meta name="turbo-refresh-method" content="morph">
    <meta name="turbo-refresh-scroll" content="preserve">
  </head>
  <body>
    <h1>Autofocus</h1>

    <button autofocus id="first-autofocus-element" type="button">First [autofocus]</button>
    <button autofocus id="second-autofocus-element" type="button">Second [autofocus]</button>

    <a id="autofocus-inert-link" href="/src/tests/fixtures/autofocus-inert.html">autofocus-inert.html link</a>
    <a id="frame-outer-link" href="/src/tests/fixtures/frames/form.html" data-turbo-frame="frame">Outer #frame link to frames/form.html</a>

    <turbo-frame id="frame">
      <a id="frame-inner-link" href="/src/tests/fixtures/frames/form.html">Inner #frame link to frames/form.html</a>
    </turbo-frame>

    <turbo-frame id="drives-frame" target="frame">
      <a id="drives-frame-target-link" href="/src/tests/fixtures/frames/form.html">#drives-frame link to frames/form.html</a>
    </turbo-frame>

    <form id="form" action="/__turbo/refresh" method="post" class="redirect">
      <input id="form-text" type="text" name="text" value="" autofocus>
      <input type="hidden" name="path" value="/src/tests/fixtures/autofocus.html">
      <input type="hidden" name="sleep" value="50">
      <input id="form-submit" type="submit" value="form[method=post]">
    </form>
  </body>
</html>


================================================
FILE: src/tests/fixtures/bare.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Bare</title>
  </head>
  <body>
  </body>
</html>


================================================
FILE: src/tests/fixtures/body_noscript.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Body noscript test</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>Body noscript test</h1>
    <p><a id="back-link" href="/src/tests/fixtures/rendering.html">Go back</a></p>
    <noscript>
      <link rel="stylesheet" type="text/css" href="/src/tests/fixtures/noscript.css">
    </noscript>
  </body>
</html>


================================================
FILE: src/tests/fixtures/body_noscript_with_content.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Body noscript with content</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>Body noscript with content</h1>
    <p><a id="back-link" href="/src/tests/fixtures/rendering.html">Go back</a></p>
    <noscript id="lazy-load-noscript">
      <img src="/src/tests/fixtures/logo.png" alt="Lazy loaded image">
    </noscript>
    <noscript id="mixed-noscript">
      <img src="/src/tests/fixtures/logo.png" alt="Another image">
      <link rel="stylesheet" type="text/css" href="/src/tests/fixtures/noscript.css">
    </noscript>
    <noscript id="alternate-stylesheet-noscript">
      <img src="/src/tests/fixtures/logo.png" alt="Alternate stylesheet image">
      <link rel="alternate stylesheet" type="text/css" href="/src/tests/fixtures/noscript.css">
    </noscript>
  </body>
</html>


================================================
FILE: src/tests/fixtures/body_script.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Body script</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
    <meta name="turbo-cache-control" content="no-preview">
  </head>
  <body>
    <h1>Body script</h1>
    <script>
      if ("bodyScriptEvaluationCount" in window) {
        window.bodyScriptEvaluationCount++
      } else {
        window.bodyScriptEvaluationCount = 1
      }
    </script>
  </body>
</html>


================================================
FILE: src/tests/fixtures/cache_observer.html
================================================
<!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <title>Turbo</title>
     <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
     <script src="/src/tests/fixtures/test.js"></script>
   </head>
   <body>
     <section>
       <h1>Cache Observer</h1>
       <div id="temporary" data-turbo-temporary>data-turbo-temporary</div>
       <p><a id="link" href="/src/tests/fixtures/rendering.html">rendering</a></p>
       <p><a id="redirect-here-link" href="/__turbo/redirect?path=/src/tests/fixtures/cache_observer.html">Redirection link back to here</a></p>
     </section>
   </body>
 </html>


================================================
FILE: src/tests/fixtures/dir_rtl.html
================================================
<!doctype html>
<html dir="rtl">
  <head>
    <meta charset="utf-8" />
    <title>Turbo</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>html[dir="rtl"]</h1>
  </body>
</html>


================================================
FILE: src/tests/fixtures/drive.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Drive</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>Drive</h1>

    <div>
      <a id="drive_enabled" href="/src/tests/fixtures/drive.html">Drive enabled link</a>
      <a id="drive_enabled_external" href="https://example.com">Drive enabled external link</a>
    </div>

    <div data-turbo="false">
      <a id="drive_disabled" href="/src/tests/fixtures/drive.html">Drive disabled link</a>
    </div>
  </body>
</html>


================================================
FILE: src/tests/fixtures/drive_disabled.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Drive (Disabled by Default)</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
    <script type="module">
      addEventListener("click", event => {
        if (event.target.id == "requestSubmit") {
          event.preventDefault()
          const form = event.target.closest('form')
          form.requestSubmit()
        }
      })
    </script>
    <script>
      Turbo.config.drive.enabled = false
    </script>
  </head>
  <body>
    <h1>Drive (Disabled by Default)</h1>

    <div data-turbo="true">
      <a id="drive_enabled" href="/src/tests/fixtures/drive_disabled.html">Drive enabled link</a>
    </div>

    <div>
      <a id="drive_disabled" href="/src/tests/fixtures/drive_disabled.html">Drive disabled link</a>
    </div>

    <form action="/__turbo/redirect" method="post" id="no_submitter_drive_enabled" data-turbo="true">
      <input type="hidden" name="path" value="/src/tests/fixtures/form.html">
      <input type="hidden" name="greeting" value="Hello from a redirect">
      <a href="#" id="requestSubmit">Drive enabled submit via JS</a>
    </form>

    <turbo-frame id="frame">
      <h2>Hello from a frame</h2>

      <a href="/src/tests/fixtures/drive_disabled.html">Navigate #frame</a>
      <form action="/src/tests/fixtures/drive_disabled.html">
        <button>Navigate #frame</button>
      </form>
      <custom-link-element link="/src/tests/fixtures/frames/frame.html">
        <span id="frame-navigation-with-slot">Link in slot</span>
      </custom-link-element>
    </turbo-frame>
  </body>
</html>


================================================
FILE: src/tests/fixtures/es_locale.html
================================================
<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <title>Turbo</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>html[lang="es"]</h1>
  </body>
</html>


================================================
FILE: src/tests/fixtures/esm.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>ESM</title>
    <script type="module" data-turbo-track="reload">
      import "/dist/turbo.es2017-esm.js"
    </script>
    <script src="/src/tests/fixtures/test.js"></script>
    <meta name="test" content="foo">
  </head>
  <body>
    <h1>ESM</h1>
  </body>
</html>


================================================
FILE: src/tests/fixtures/eval_false_script.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>data-turbo-eval=false script</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
  </head>
  <body>
    <h1>data-turbo-eval=false script</h1>
    <script data-turbo-eval="false">
      if ("bodyScriptEvaluationCount" in window) {
        window.bodyScriptEvaluationCount++
      } else {
        window.bodyScriptEvaluationCount = 1
      }
    </script>
  </body>
</html>


================================================
FILE: src/tests/fixtures/form.html
================================================
<!DOCTYPE html>
<html id="html" data-skip-event-details="turbo:submit-start turbo:submit-end turbo:fetch-request-error">
  <head>
    <meta charset="utf-8">
    <title>Form</title>
    <script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
    <script src="/src/tests/fixtures/test.js"></script>
    <style id="form-fixture-styles">
      dialog {
        display: block;
        position: static;
      }
    </style>
  </head>
  <body>
    <h1>Form</h1>
    <div id="standard">
      <form id="standard-form" action="/__turbo/redirect" method="post" class="redirect">
        <input type="hidden" name="path" value="/src/tests/fixtures/form.html">
        <input type="hidden" name="greeting" v
Download .txt
gitextract_vgl1u45s/

├── .devcontainer/
│   ├── Dockerfile
│   └── devcontainer.json
├── .eslintignore
├── .eslintrc.js
├── .github/
│   ├── scripts/
│   │   └── publish-dev-build
│   └── workflows/
│       ├── ci.yml
│       └── dev-builds.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│   ├── launch.json
│   └── tasks.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── MIT-LICENSE
├── README.md
├── package.json
├── playwright.config.js
├── rollup.config.js
├── src/
│   ├── core/
│   │   ├── bardo.js
│   │   ├── cache.js
│   │   ├── config/
│   │   │   ├── drive.js
│   │   │   ├── forms.js
│   │   │   └── index.js
│   │   ├── drive/
│   │   │   ├── error_renderer.js
│   │   │   ├── form_submission.js
│   │   │   ├── head_snapshot.js
│   │   │   ├── history.js
│   │   │   ├── limited_set.js
│   │   │   ├── morphing_page_renderer.js
│   │   │   ├── navigator.js
│   │   │   ├── page_renderer.js
│   │   │   ├── page_snapshot.js
│   │   │   ├── page_view.js
│   │   │   ├── prefetch_cache.js
│   │   │   ├── preloader.js
│   │   │   ├── progress_bar.js
│   │   │   ├── snapshot_cache.js
│   │   │   ├── view_transitioner.js
│   │   │   └── visit.js
│   │   ├── errors.js
│   │   ├── frames/
│   │   │   ├── frame_controller.js
│   │   │   ├── frame_redirector.js
│   │   │   ├── frame_renderer.js
│   │   │   ├── frame_view.js
│   │   │   ├── link_interceptor.js
│   │   │   └── morphing_frame_renderer.js
│   │   ├── index.js
│   │   ├── lru_cache.js
│   │   ├── morphing.js
│   │   ├── native/
│   │   │   └── browser_adapter.js
│   │   ├── renderer.js
│   │   ├── session.js
│   │   ├── snapshot.js
│   │   ├── streams/
│   │   │   ├── stream_actions.js
│   │   │   ├── stream_message.js
│   │   │   └── stream_message_renderer.js
│   │   ├── url.js
│   │   └── view.js
│   ├── elements/
│   │   ├── frame_element.js
│   │   ├── index.js
│   │   ├── stream_element.js
│   │   └── stream_source_element.js
│   ├── http/
│   │   ├── fetch.js
│   │   ├── fetch_request.js
│   │   ├── fetch_response.js
│   │   └── index.js
│   ├── index.js
│   ├── observers/
│   │   ├── appearance_observer.js
│   │   ├── cache_observer.js
│   │   ├── form_link_click_observer.js
│   │   ├── form_submit_observer.js
│   │   ├── link_click_observer.js
│   │   ├── link_prefetch_observer.js
│   │   ├── page_observer.js
│   │   ├── scroll_observer.js
│   │   └── stream_observer.js
│   ├── polyfills/
│   │   └── index.js
│   ├── script_warning.js
│   ├── tests/
│   │   ├── fixtures/
│   │   │   ├── 422.html
│   │   │   ├── 422_morph.html
│   │   │   ├── 422_tall.html
│   │   │   ├── 500.html
│   │   │   ├── additional_assets.html
│   │   │   ├── additional_script.html
│   │   │   ├── async_script.html
│   │   │   ├── async_script_2.html
│   │   │   ├── autofocus-inert.html
│   │   │   ├── autofocus.html
│   │   │   ├── bare.html
│   │   │   ├── body_noscript.html
│   │   │   ├── body_noscript_with_content.html
│   │   │   ├── body_script.html
│   │   │   ├── cache_observer.html
│   │   │   ├── dir_rtl.html
│   │   │   ├── drive.html
│   │   │   ├── drive_disabled.html
│   │   │   ├── es_locale.html
│   │   │   ├── esm.html
│   │   │   ├── eval_false_script.html
│   │   │   ├── form.html
│   │   │   ├── form_mode.html
│   │   │   ├── frame_navigation.html
│   │   │   ├── frame_preloading.html
│   │   │   ├── frame_refresh_after_navigation.html
│   │   │   ├── frame_refresh_morph.html
│   │   │   ├── frame_refresh_reload.html
│   │   │   ├── frames/
│   │   │   │   ├── body_script.html
│   │   │   │   ├── body_script_2.html
│   │   │   │   ├── empty_head.html
│   │   │   │   ├── eval_false_script.html
│   │   │   │   ├── form-redirect.html
│   │   │   │   ├── form-redirected.html
│   │   │   │   ├── form.html
│   │   │   │   ├── frame.html
│   │   │   │   ├── frame_for_eager.html
│   │   │   │   ├── hello.html
│   │   │   │   ├── parent.html
│   │   │   │   ├── part.html
│   │   │   │   ├── preloading.html
│   │   │   │   ├── recursive.html
│   │   │   │   ├── self.html
│   │   │   │   └── unvisitable.html
│   │   │   ├── frames.html
│   │   │   ├── greetings.ejs
│   │   │   ├── head_script.html
│   │   │   ├── headers.html
│   │   │   ├── hot_preloading.html
│   │   │   ├── hover_to_prefetch.html
│   │   │   ├── hover_to_prefetch_custom_cache_time.html
│   │   │   ├── hover_to_prefetch_disabled.html
│   │   │   ├── hover_to_prefetch_iframe.html
│   │   │   ├── hover_to_prefetch_without_meta_tag_with_link_to_with_meta_tag.html
│   │   │   ├── link_redirect.html
│   │   │   ├── link_redirect_target.html
│   │   │   ├── loading.html
│   │   │   ├── navigation.html
│   │   │   ├── noscript.css
│   │   │   ├── one.html
│   │   │   ├── page_refresh.html
│   │   │   ├── page_refresh_replace.html
│   │   │   ├── page_refresh_scroll_reset.html
│   │   │   ├── page_refresh_stream_action.html
│   │   │   ├── page_refreshed.html
│   │   │   ├── page_with_eager_frame.html
│   │   │   ├── pausable_rendering.html
│   │   │   ├── pausable_requests.html
│   │   │   ├── permanent_children.html
│   │   │   ├── permanent_element.html
│   │   │   ├── prefetched.html
│   │   │   ├── preloaded.html
│   │   │   ├── preloading.html
│   │   │   ├── remote_permanent_frame.html
│   │   │   ├── rendering.html
│   │   │   ├── response.js
│   │   │   ├── root/
│   │   │   │   ├── index.html
│   │   │   │   └── page.html
│   │   │   ├── scroll/
│   │   │   │   ├── one.html
│   │   │   │   └── two.html
│   │   │   ├── scroll_restoration.html
│   │   │   ├── stream.html
│   │   │   ├── stylesheets/
│   │   │   │   ├── common.css
│   │   │   │   ├── left.css
│   │   │   │   ├── left.html
│   │   │   │   ├── right.css
│   │   │   │   └── right.html
│   │   │   ├── tabs/
│   │   │   │   ├── three.html
│   │   │   │   └── two.html
│   │   │   ├── tabs.html
│   │   │   ├── target.html
│   │   │   ├── test.css
│   │   │   ├── test.js
│   │   │   ├── tracked_asset_change.html
│   │   │   ├── tracked_nonce_change.html
│   │   │   ├── transitions/
│   │   │   │   ├── left.html
│   │   │   │   ├── left_legacy.html
│   │   │   │   ├── other.html
│   │   │   │   ├── right.html
│   │   │   │   └── right_legacy.html
│   │   │   ├── two.html
│   │   │   ├── ujs.html
│   │   │   ├── umd.html
│   │   │   ├── video.webm
│   │   │   ├── visit.html
│   │   │   └── visit_control_reload.html
│   │   ├── functional/
│   │   │   ├── async_script_tests.js
│   │   │   ├── autofocus_tests.js
│   │   │   ├── cache_observer_tests.js
│   │   │   ├── drive_disabled_tests.js
│   │   │   ├── drive_stylesheet_merging_tests.js
│   │   │   ├── drive_tests.js
│   │   │   ├── drive_view_transition_legacy_tests.js
│   │   │   ├── drive_view_transition_tests.js
│   │   │   ├── form_mode_tests.js
│   │   │   ├── form_submission_tests.js
│   │   │   ├── frame_navigation_tests.js
│   │   │   ├── frame_tests.js
│   │   │   ├── import_tests.js
│   │   │   ├── link_prefetch_observer_tests.js
│   │   │   ├── loading_tests.js
│   │   │   ├── navigation_tests.js
│   │   │   ├── page_refresh_stream_action_tests.js
│   │   │   ├── page_refresh_tests.js
│   │   │   ├── pausable_rendering_tests.js
│   │   │   ├── pausable_requests_tests.js
│   │   │   ├── preloader_tests.js
│   │   │   ├── rendering_tests.js
│   │   │   ├── root_tests.js
│   │   │   ├── scroll_restoration_tests.js
│   │   │   ├── stream_tests.js
│   │   │   └── visit_tests.js
│   │   ├── helpers/
│   │   │   ├── dom_test_case.js
│   │   │   └── page.js
│   │   ├── integration/
│   │   │   └── ujs_tests.js
│   │   ├── server.mjs
│   │   └── unit/
│   │       ├── deprecated_adapter_support_tests.js
│   │       ├── export_tests.js
│   │       ├── limited_set_tests.js
│   │       ├── native_adapter_support_tests.js
│   │       └── stream_element_tests.js
│   └── util.js
└── web-test-runner.config.mjs
Download .txt
SYMBOL INDEX (862 symbols across 72 files)

FILE: src/core/bardo.js
  class Bardo (line 1) | class Bardo {
    method preservingPermanentElements (line 2) | static async preservingPermanentElements(delegate, permanentElementMap...
    method constructor (line 9) | constructor(delegate, permanentElementMap) {
    method enter (line 14) | enter() {
    method leave (line 22) | leave() {
    method replaceNewPermanentElementWithPlaceholder (line 31) | replaceNewPermanentElementWithPlaceholder(permanentElement) {
    method replaceCurrentPermanentElementWithClone (line 36) | replaceCurrentPermanentElementWithClone(permanentElement) {
    method replacePlaceholderWithPermanentElement (line 41) | replacePlaceholderWithPermanentElement(permanentElement) {
    method getPlaceholderById (line 46) | getPlaceholderById(id) {
    method placeholders (line 50) | get placeholders() {
  function createPlaceholderForPermanentElement (line 55) | function createPlaceholderForPermanentElement(permanentElement) {

FILE: src/core/cache.js
  class Cache (line 3) | class Cache {
    method constructor (line 4) | constructor(session) {
    method clear (line 8) | clear() {
    method resetCacheControl (line 12) | resetCacheControl() {
    method exemptPageFromCache (line 16) | exemptPageFromCache() {
    method exemptPageFromPreview (line 20) | exemptPageFromPreview() {
    method #setCacheControl (line 24) | #setCacheControl(value) {

FILE: src/core/config/forms.js
  class Config (line 22) | class Config {
    method constructor (line 25) | constructor(config) {
    method submitter (line 29) | get submitter() {
    method submitter (line 33) | set submitter(value) {

FILE: src/core/drive/error_renderer.js
  class ErrorRenderer (line 4) | class ErrorRenderer extends Renderer {
    method renderElement (line 5) | static renderElement(currentElement, newElement) {
    method render (line 11) | async render() {
    method replaceHeadAndBody (line 16) | replaceHeadAndBody() {
    method activateScriptElements (line 22) | activateScriptElements() {
    method newHead (line 32) | get newHead() {
    method scriptElements (line 36) | get scriptElements() {

FILE: src/core/drive/form_submission.js
  class FormSubmission (line 23) | class FormSubmission {
    method confirmMethod (line 26) | static confirmMethod(message) {
    method constructor (line 30) | constructor(delegate, formElement, submitter, mustRedirect = false) {
    method method (line 43) | get method() {
    method method (line 47) | set method(value) {
    method action (line 51) | get action() {
    method action (line 55) | set action(value) {
    method body (line 59) | get body() {
    method enctype (line 63) | get enctype() {
    method isSafe (line 67) | get isSafe() {
    method location (line 71) | get location() {
    method start (line 77) | async start() {
    method stop (line 98) | stop() {
    method prepareRequest (line 109) | prepareRequest(request) {
    method requestStarted (line 122) | requestStarted(_request) {
    method requestPreventedHandlingResponse (line 134) | requestPreventedHandlingResponse(request, response) {
    method requestSucceededWithResponse (line 140) | requestSucceededWithResponse(request, response) {
    method requestFailedWithResponse (line 158) | requestFailedWithResponse(request, response) {
    method requestErrored (line 163) | requestErrored(request, error) {
    method requestFinished (line 168) | requestFinished(_request) {
    method setSubmitsWith (line 182) | setSubmitsWith() {
    method resetSubmitterText (line 195) | resetSubmitterText() {
    method requestMustRedirect (line 206) | requestMustRedirect(request) {
    method requestAcceptsTurboStreamResponse (line 210) | requestAcceptsTurboStreamResponse(request) {
    method submitsWith (line 214) | get submitsWith() {
  function buildFormData (line 219) | function buildFormData(formElement, submitter) {
  function getCookieValue (line 231) | function getCookieValue(cookieName) {
  function responseSucceededWithoutRedirect (line 242) | function responseSucceededWithoutRedirect(response) {
  function getFormAction (line 246) | function getFormAction(formElement, submitter) {
  function getAction (line 256) | function getAction(formAction, fetchMethod) {
  function getMethod (line 266) | function getMethod(formElement, submitter) {
  function getEnctype (line 271) | function getEnctype(formElement, submitter) {

FILE: src/core/drive/head_snapshot.js
  class HeadSnapshot (line 4) | class HeadSnapshot extends Snapshot {
    method trackedElementSignature (line 27) | get trackedElementSignature() {
    method getScriptElementsNotInSnapshot (line 33) | getScriptElementsNotInSnapshot(snapshot) {
    method getStylesheetElementsNotInSnapshot (line 37) | getStylesheetElementsNotInSnapshot(snapshot) {
    method getElementsMatchingTypeNotInSnapshot (line 41) | getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
    method provisionalElements (line 49) | get provisionalElements() {
    method getMetaValue (line 62) | getMetaValue(name) {
    method findMetaElementByName (line 67) | findMetaElementByName(name) {
  function elementType (line 77) | function elementType(element) {
  function elementIsTracked (line 85) | function elementIsTracked(element) {
  function elementIsScript (line 89) | function elementIsScript(element) {
  function elementIsNoscript (line 94) | function elementIsNoscript(element) {
  function elementIsMetaElementWithName (line 99) | function elementIsMetaElementWithName(element, name) {
  function elementWithoutNonce (line 104) | function elementWithoutNonce(element) {

FILE: src/core/drive/history.js
  class History (line 3) | class History {
    method constructor (line 10) | constructor(delegate) {
    method start (line 14) | start() {
    method stop (line 23) | stop() {
    method push (line 30) | push(location, restorationIdentifier) {
    method replace (line 34) | replace(location, restorationIdentifier) {
    method update (line 38) | update(method, location, restorationIdentifier = uuid()) {
    method getRestorationDataForIdentifier (line 49) | getRestorationDataForIdentifier(restorationIdentifier) {
    method updateRestorationData (line 53) | updateRestorationData(additionalData) {
    method assumeControlOfScrollRestoration (line 64) | assumeControlOfScrollRestoration() {
    method relinquishControlOfScrollRestoration (line 71) | relinquishControlOfScrollRestoration() {

FILE: src/core/drive/limited_set.js
  class LimitedSet (line 1) | class LimitedSet extends Set {
    method constructor (line 2) | constructor(maxSize) {
    method add (line 7) | add(value) {

FILE: src/core/drive/morphing_page_renderer.js
  class MorphingPageRenderer (line 5) | class MorphingPageRenderer extends PageRenderer {
    method renderElement (line 6) | static renderElement(currentElement, newElement) {
    method preservingPermanentElements (line 25) | async preservingPermanentElements(callback) {
    method renderMethod (line 29) | get renderMethod() {
    method shouldAutofocus (line 33) | get shouldAutofocus() {

FILE: src/core/drive/navigator.js
  class Navigator (line 7) | class Navigator {
    method constructor (line 8) | constructor(delegate) {
    method proposeVisit (line 12) | proposeVisit(location, options = {}) {
    method startVisit (line 18) | startVisit(locatable, restorationIdentifier, options = {}) {
    method submitForm (line 27) | submitForm(form, submitter) {
    method stop (line 34) | stop() {
    method adapter (line 46) | get adapter() {
    method view (line 50) | get view() {
    method rootLocation (line 54) | get rootLocation() {
    method history (line 58) | get history() {
    method formSubmissionStarted (line 64) | formSubmissionStarted(formSubmission) {
    method formSubmissionSucceededWithResponse (line 71) | async formSubmissionSucceededWithResponse(formSubmission, fetchRespons...
    method formSubmissionFailedWithResponse (line 92) | async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
    method formSubmissionErrored (line 109) | formSubmissionErrored(formSubmission, error) {
    method formSubmissionFinished (line 113) | formSubmissionFinished(formSubmission) {
    method linkPrefetchingIsEnabledForLocation (line 122) | linkPrefetchingIsEnabledForLocation(location) {
    method visitStarted (line 133) | visitStarted(visit) {
    method visitCompleted (line 137) | visitCompleted(visit) {
    method locationWithActionIsSamePage (line 144) | locationWithActionIsSamePage(location, action) {
    method location (line 150) | get location() {
    method restorationIdentifier (line 154) | get restorationIdentifier() {
    method #getActionForFormSubmission (line 158) | #getActionForFormSubmission(formSubmission, fetchResponse) {
    method #getDefaultAction (line 163) | #getDefaultAction(fetchResponse) {

FILE: src/core/drive/page_renderer.js
  class PageRenderer (line 4) | class PageRenderer extends Renderer {
    method renderElement (line 5) | static renderElement(currentElement, newElement) {
    method shouldRender (line 13) | get shouldRender() {
    method reloadReason (line 17) | get reloadReason() {
    method prepareToRender (line 31) | async prepareToRender() {
    method render (line 36) | async render() {
    method finishRendering (line 42) | finishRendering() {
    method currentHeadSnapshot (line 49) | get currentHeadSnapshot() {
    method newHeadSnapshot (line 53) | get newHeadSnapshot() {
    method newElement (line 57) | get newElement() {
    method #setLanguage (line 61) | #setLanguage() {
    method mergeHead (line 77) | async mergeHead() {
    method replaceBody (line 90) | async replaceBody() {
    method trackedElementsAreIdentical (line 97) | get trackedElementsAreIdentical() {
    method copyNewHeadStylesheetElements (line 101) | async copyNewHeadStylesheetElements() {
    method copyNewHeadScriptElements (line 113) | copyNewHeadScriptElements() {
    method removeUnusedDynamicStylesheetElements (line 119) | removeUnusedDynamicStylesheetElements() {
    method mergeProvisionalElements (line 125) | async mergeProvisionalElements() {
    method isCurrentElementInElementList (line 139) | isCurrentElementInElementList(element, elementList) {
    method removeCurrentHeadProvisionalElements (line 162) | removeCurrentHeadProvisionalElements() {
    method copyNewHeadProvisionalElements (line 168) | copyNewHeadProvisionalElements() {
    method activateNewBody (line 174) | activateNewBody() {
    method deactivateNoscriptStylesheetElements (line 180) | deactivateNoscriptStylesheetElements() {
    method activateNewBodyScriptElements (line 190) | activateNewBodyScriptElements() {
    method assignNewBody (line 197) | async assignNewBody() {
    method unusedDynamicStylesheetElements (line 201) | get unusedDynamicStylesheetElements() {
    method oldHeadStylesheetElements (line 207) | get oldHeadStylesheetElements() {
    method newHeadStylesheetElements (line 211) | get newHeadStylesheetElements() {
    method newHeadScriptElements (line 215) | get newHeadScriptElements() {
    method currentHeadProvisionalElements (line 219) | get currentHeadProvisionalElements() {
    method newHeadProvisionalElements (line 223) | get newHeadProvisionalElements() {
    method newBodyScriptElements (line 227) | get newBodyScriptElements() {

FILE: src/core/drive/page_snapshot.js
  class PageSnapshot (line 6) | class PageSnapshot extends Snapshot {
    method fromHTMLString (line 7) | static fromHTMLString(html = "") {
    method fromElement (line 11) | static fromElement(element) {
    method fromDocument (line 15) | static fromDocument({ documentElement, body, head }) {
    method constructor (line 19) | constructor(documentElement, body, headSnapshot) {
    method clone (line 25) | clone() {
    method lang (line 52) | get lang() {
    method dir (line 56) | get dir() {
    method headElement (line 60) | get headElement() {
    method rootLocation (line 64) | get rootLocation() {
    method cacheControlValue (line 69) | get cacheControlValue() {
    method isPreviewable (line 73) | get isPreviewable() {
    method isCacheable (line 77) | get isCacheable() {
    method isVisitable (line 81) | get isVisitable() {
    method prefersViewTransitions (line 85) | get prefersViewTransitions() {
    method refreshMethod (line 90) | get refreshMethod() {
    method refreshScroll (line 94) | get refreshScroll() {
    method getSetting (line 100) | getSetting(name) {

FILE: src/core/drive/page_view.js
  class PageView (line 9) | class PageView extends View {
    method shouldTransitionTo (line 14) | shouldTransitionTo(newSnapshot) {
    method renderPage (line 18) | renderPage(snapshot, isPreview = false, willRender = true, visit) {
    method renderError (line 33) | renderError(snapshot, visit) {
    method clearSnapshotCache (line 39) | clearSnapshotCache() {
    method cacheSnapshot (line 43) | async cacheSnapshot(snapshot = this.snapshot) {
    method getCachedSnapshotForLocation (line 54) | getCachedSnapshotForLocation(location) {
    method isPageRefresh (line 58) | isPageRefresh(visit) {
    method shouldPreserveScrollPosition (line 62) | shouldPreserveScrollPosition(visit) {
    method snapshot (line 66) | get snapshot() {

FILE: src/core/drive/prefetch_cache.js
  constant PREFETCH_DELAY (line 4) | const PREFETCH_DELAY = 100
  class PrefetchCache (line 6) | class PrefetchCache extends LRUCache {
    method constructor (line 10) | constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
    method putLater (line 15) | putLater(url, request, ttl) {
    method put (line 23) | put(url, request, ttl = cacheTtl) {
    method clear (line 28) | clear() {
    method evict (line 33) | evict(key) {
    method has (line 38) | has(key) {

FILE: src/core/drive/preloader.js
  class Preloader (line 4) | class Preloader {
    method constructor (line 7) | constructor(delegate, snapshotCache) {
    method start (line 12) | start() {
    method stop (line 20) | stop() {
    method preloadOnLoadLinksForView (line 24) | preloadOnLoadLinksForView(element) {
    method preloadURL (line 32) | async preloadURL(link) {
    method prepareRequest (line 45) | prepareRequest(fetchRequest) {
    method requestSucceededWithResponse (line 49) | async requestSucceededWithResponse(fetchRequest, fetchResponse) {
    method requestStarted (line 60) | requestStarted(fetchRequest) {}
    method requestErrored (line 62) | requestErrored(fetchRequest) {}
    method requestFinished (line 64) | requestFinished(fetchRequest) {}
    method requestPreventedHandlingResponse (line 66) | requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
    method requestFailedWithResponse (line 68) | requestFailedWithResponse(fetchRequest, fetchResponse) {}

FILE: src/core/drive/progress_bar.js
  class ProgressBar (line 5) | class ProgressBar {
    method defaultCSS (line 8) | static get defaultCSS() {
    method constructor (line 30) | constructor() {
    method show (line 37) | show() {
    method hide (line 45) | hide() {
    method setValue (line 57) | setValue(value) {
    method installStylesheetElement (line 64) | installStylesheetElement() {
    method installProgressElement (line 68) | installProgressElement() {
    method fadeProgressElement (line 75) | fadeProgressElement(callback) {
    method uninstallProgressElement (line 80) | uninstallProgressElement() {
    method startTrickling (line 86) | startTrickling() {
    method stopTrickling (line 92) | stopTrickling() {
    method refresh (line 101) | refresh() {
    method createStylesheetElement (line 107) | createStylesheetElement() {
    method createProgressElement (line 118) | createProgressElement() {

FILE: src/core/drive/snapshot_cache.js
  class SnapshotCache (line 4) | class SnapshotCache extends LRUCache {
    method constructor (line 5) | constructor(size) {
    method snapshots (line 9) | get snapshots() {

FILE: src/core/drive/view_transitioner.js
  class ViewTransitioner (line 1) | class ViewTransitioner {
    method renderChange (line 5) | renderChange(useViewTransition, render) {
    method viewTransitionsAvailable (line 18) | get viewTransitionsAvailable() {

FILE: src/core/drive/visit.js
  class Visit (line 46) | class Visit {
    method constructor (line 59) | constructor(delegate, location, restorationIdentifier, options = {}) {
    method adapter (line 99) | get adapter() {
    method view (line 103) | get view() {
    method history (line 107) | get history() {
    method restorationData (line 111) | get restorationData() {
    method start (line 115) | start() {
    method cancel (line 124) | cancel() {
    method complete (line 134) | complete() {
    method fail (line 147) | fail() {
    method changeHistory (line 155) | changeHistory() {
    method issueRequest (line 164) | issueRequest() {
    method simulateRequest (line 173) | simulateRequest() {
    method startRequest (line 181) | startRequest() {
    method recordResponse (line 186) | recordResponse(response = this.response) {
    method finishRequest (line 198) | finishRequest() {
    method loadResponse (line 203) | loadResponse() {
    method getCachedSnapshot (line 225) | getCachedSnapshot() {
    method getPreloadedSnapshot (line 235) | getPreloadedSnapshot() {
    method hasCachedSnapshot (line 241) | hasCachedSnapshot() {
    method loadCachedSnapshot (line 245) | loadCachedSnapshot() {
    method followRedirect (line 267) | followRedirect() {
    method prepareRequest (line 281) | prepareRequest(request) {
    method requestStarted (line 287) | requestStarted() {
    method requestPreventedHandlingResponse (line 291) | requestPreventedHandlingResponse(_request, _response) {}
    method requestSucceededWithResponse (line 293) | async requestSucceededWithResponse(request, response) {
    method requestFailedWithResponse (line 307) | async requestFailedWithResponse(request, response) {
    method requestErrored (line 320) | requestErrored(_request, _error) {
    method requestFinished (line 327) | requestFinished() {
    method performScroll (line 333) | performScroll() {
    method scrollToRestoredPosition (line 345) | scrollToRestoredPosition() {
    method scrollToAnchor (line 353) | scrollToAnchor() {
    method recordTimingMetric (line 363) | recordTimingMetric(metric) {
    method getTimingMetrics (line 367) | getTimingMetrics() {
    method hasPreloadedResponse (line 373) | hasPreloadedResponse() {
    method shouldIssueRequest (line 377) | shouldIssueRequest() {
    method cacheSnapshot (line 385) | cacheSnapshot() {
    method render (line 392) | async render(callback) {
    method renderPageSnapshot (line 402) | async renderPageSnapshot(snapshot, isPreview) {
    method cancelRender (line 409) | cancelRender() {
  function isSuccessful (line 417) | function isSuccessful(statusCode) {

FILE: src/core/errors.js
  class TurboFrameMissingError (line 1) | class TurboFrameMissingError extends Error {}

FILE: src/core/frames/frame_controller.js
  class FrameController (line 29) | class FrameController {
    method constructor (line 39) | constructor(element) {
    method connect (line 51) | connect() {
    method disconnect (line 65) | disconnect() {
    method disabledChanged (line 79) | disabledChanged() {
    method sourceURLChanged (line 87) | sourceURLChanged() {
    method sourceURLReloaded (line 103) | sourceURLReloaded() {
    method loadingStyleChanged (line 114) | loadingStyleChanged() {
    method #loadSourceURL (line 123) | async #loadSourceURL() {
    method loadResponse (line 132) | async loadResponse(fetchResponse) {
    method elementAppearedInViewport (line 157) | elementAppearedInViewport(element) {
    method willSubmitFormLinkToLocation (line 164) | willSubmitFormLinkToLocation(link) {
    method submittedFormLinkToLocation (line 168) | submittedFormLinkToLocation(link, _location, form) {
    method shouldInterceptLinkClick (line 175) | shouldInterceptLinkClick(element, _location, _event) {
    method linkClickIntercepted (line 179) | linkClickIntercepted(element, location) {
    method willSubmitForm (line 185) | willSubmitForm(element, submitter) {
    method formSubmitted (line 189) | formSubmitted(element, submitter) {
    method prepareRequest (line 205) | prepareRequest(request, frame = this) {
    method requestStarted (line 213) | requestStarted(_request) {
    method requestPreventedHandlingResponse (line 217) | requestPreventedHandlingResponse(_request, _response) {
    method requestSucceededWithResponse (line 221) | async requestSucceededWithResponse(request, response) {
    method requestFailedWithResponse (line 226) | async requestFailedWithResponse(request, response) {
    method requestErrored (line 231) | requestErrored(request, error) {
    method requestFinished (line 236) | requestFinished(_request) {
    method formSubmissionStarted (line 242) | formSubmissionStarted({ formElement }) {
    method formSubmissionSucceededWithResponse (line 246) | formSubmissionSucceededWithResponse(formSubmission, response) {
    method formSubmissionFailedWithResponse (line 257) | formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
    method formSubmissionErrored (line 262) | formSubmissionErrored(formSubmission, error) {
    method formSubmissionFinished (line 266) | formSubmissionFinished({ formElement }) {
    method allowsImmediateRender (line 272) | allowsImmediateRender({ element: newFrame }, options) {
    method viewRenderedSnapshot (line 291) | viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}
    method preloadOnLoadLinksForView (line 293) | preloadOnLoadLinksForView(element) {
    method viewInvalidated (line 297) | viewInvalidated() {}
    method willRenderFrame (line 301) | willRenderFrame(currentElement, _newElement) {
    method #loadFrameResponse (line 317) | async #loadFrameResponse(fetchResponse, document) {
    method #visit (line 337) | async #visit(url) {
    method #navigateFrame (line 353) | #navigateFrame(element, url, submitter) {
    method proposeVisitIfNavigatedWithAction (line 363) | proposeVisitIfNavigatedWithAction(frame, action = null) {
    method changeHistory (line 392) | changeHistory() {
    method #handleUnvisitableFrameResponse (line 399) | async #handleUnvisitableFrameResponse(fetchResponse) {
    method #willHandleFrameMissingFromResponse (line 407) | #willHandleFrameMissingFromResponse(fetchResponse) {
    method #handleFrameMissingFromResponse (line 428) | #handleFrameMissingFromResponse(fetchResponse) {
    method #throwFrameMissingError (line 433) | #throwFrameMissingError(fetchResponse) {
    method #visitResponse (line 438) | async #visitResponse(response) {
    method #findFrameElement (line 446) | #findFrameElement(element, submitter) {
    method extractForeignFrameElement (line 453) | async extractForeignFrameElement(container) {
    method #formActionIsVisitable (line 476) | #formActionIsVisitable(form, submitter) {
    method #shouldInterceptNavigation (line 482) | #shouldInterceptNavigation(element, submitter) {
    method id (line 515) | get id() {
    method disabled (line 519) | get disabled() {
    method enabled (line 523) | get enabled() {
    method sourceURL (line 527) | get sourceURL() {
    method sourceURL (line 533) | set sourceURL(sourceURL) {
    method loadingStyle (line 539) | get loadingStyle() {
    method isLoading (line 543) | get isLoading() {
    method complete (line 547) | get complete() {
    method complete (line 551) | set complete(value) {
    method isActive (line 559) | get isActive() {
    method rootLocation (line 563) | get rootLocation() {
    method #isIgnoringChangesTo (line 569) | #isIgnoringChangesTo(attributeName) {
    method #ignoringChangesToAttribute (line 573) | #ignoringChangesToAttribute(attributeName, callback) {
    method #withCurrentNavigationElement (line 579) | #withCurrentNavigationElement(element, callback) {
    method #getFrameElementById (line 585) | #getFrameElementById(id) {
  function activateElement (line 597) | function activateElement(element, currentURL) {

FILE: src/core/frames/frame_redirector.js
  class FrameRedirector (line 6) | class FrameRedirector {
    method constructor (line 7) | constructor(session, element) {
    method start (line 14) | start() {
    method stop (line 19) | stop() {
    method shouldInterceptLinkClick (line 26) | shouldInterceptLinkClick(element, _location, _event) {
    method linkClickIntercepted (line 30) | linkClickIntercepted(element, url, event) {
    method willSubmitForm (line 39) | willSubmitForm(element, submitter) {
    method formSubmitted (line 47) | formSubmitted(element, submitter) {
    method #shouldSubmit (line 54) | #shouldSubmit(form, submitter) {
    method #shouldRedirect (line 62) | #shouldRedirect(element, submitter) {
    method #findFrameElement (line 76) | #findFrameElement(element, submitter) {

FILE: src/core/frames/frame_renderer.js
  class FrameRenderer (line 4) | class FrameRenderer extends Renderer {
    method renderElement (line 5) | static renderElement(currentElement, newElement) {
    method constructor (line 18) | constructor(delegate, currentSnapshot, newSnapshot, renderElement, isP...
    method shouldRender (line 23) | get shouldRender() {
    method render (line 27) | async render() {
    method loadFrameElement (line 39) | loadFrameElement() {
    method scrollFrameIntoView (line 44) | scrollFrameIntoView() {
    method activateScriptElements (line 58) | activateScriptElements() {
    method newScriptElements (line 65) | get newScriptElements() {
  function readScrollLogicalPosition (line 70) | function readScrollLogicalPosition(value, defaultValue) {
  function readScrollBehavior (line 78) | function readScrollBehavior(value, defaultValue) {

FILE: src/core/frames/frame_view.js
  class FrameView (line 4) | class FrameView extends View {
    method missing (line 5) | missing() {
    method snapshot (line 9) | get snapshot() {

FILE: src/core/frames/link_interceptor.js
  class LinkInterceptor (line 3) | class LinkInterceptor {
    method constructor (line 4) | constructor(delegate, element) {
    method start (line 9) | start() {
    method stop (line 15) | stop() {
    method clickEventIsSignificant (line 44) | clickEventIsSignificant(event) {

FILE: src/core/frames/morphing_frame_renderer.js
  class MorphingFrameRenderer (line 5) | class MorphingFrameRenderer extends FrameRenderer {
    method renderElement (line 6) | static renderElement(currentElement, newElement) {
    method preservingPermanentElements (line 28) | async preservingPermanentElements(callback) {

FILE: src/core/index.js
  function start (line 24) | function start() {
  function registerAdapter (line 33) | function registerAdapter(adapter) {
  function visit (line 51) | function visit(location, options) {
  function connectStreamSource (line 60) | function connectStreamSource(source) {
  function disconnectStreamSource (line 69) | function disconnectStreamSource(source) {
  function renderStreamMessage (line 79) | function renderStreamMessage(message) {
  function setProgressBarDelay (line 93) | function setProgressBarDelay(delay) {
  function setConfirmMethod (line 100) | function setConfirmMethod(confirmMethod) {
  function setFormMode (line 107) | function setFormMode(mode) {
  function morphBodyElements (line 123) | function morphBodyElements(currentBody, newBody) {
  function morphTurboFrameElements (line 136) | function morphTurboFrameElements(currentFrame, newFrame) {

FILE: src/core/lru_cache.js
  class LRUCache (line 3) | class LRUCache {
    method constructor (line 8) | constructor(size, toCacheKey = identity) {
    method has (line 13) | has(key) {
    method get (line 17) | get(key) {
    method put (line 25) | put(key, entry) {
    method clear (line 31) | clear() {
    method read (line 39) | read(key) {
    method write (line 43) | write(key, entry) {
    method touch (line 47) | touch(key) {
    method trim (line 55) | trim() {
    method evict (line 61) | evict(key) {

FILE: src/core/morphing.js
  function morphElements (line 14) | function morphElements(currentElement, newElement, { callbacks, ...optio...
  function morphChildren (line 29) | function morphChildren(currentElement, newElement, options = {}) {
  function shouldRefreshFrameWithMorphing (line 36) | function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
  function areFramesCompatibleForRefreshing (line 42) | function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
  function closestFrameReloadableWithMorphing (line 50) | function closestFrameReloadableWithMorphing(node) {
  class DefaultIdiomorphCallbacks (line 54) | class DefaultIdiomorphCallbacks {
    method constructor (line 57) | constructor({ beforeNodeMorphed } = {}) {

FILE: src/core/native/browser_adapter.js
  class BrowserAdapter (line 6) | class BrowserAdapter {
    method constructor (line 9) | constructor(session) {
    method visitProposedToLocation (line 13) | visitProposedToLocation(location, options) {
    method visitStarted (line 21) | visitStarted(visit) {
    method visitRequestStarted (line 29) | visitRequestStarted(visit) {
    method visitRequestCompleted (line 38) | visitRequestCompleted(visit) {
    method visitRequestFailedWithStatusCode (line 46) | visitRequestFailedWithStatusCode(visit, statusCode) {
    method visitRequestFinished (line 62) | visitRequestFinished(_visit) {}
    method visitCompleted (line 64) | visitCompleted(_visit) {
    method pageInvalidated (line 69) | pageInvalidated(reason) {
    method visitFailed (line 73) | visitFailed(_visit) {
    method visitRendered (line 78) | visitRendered(_visit) {}
    method linkPrefetchingIsEnabledForLocation (line 82) | linkPrefetchingIsEnabledForLocation(location) {
    method formSubmissionStarted (line 88) | formSubmissionStarted(_formSubmission) {
    method formSubmissionFinished (line 93) | formSubmissionFinished(_formSubmission) {
    method showVisitProgressBarAfterDelay (line 100) | showVisitProgressBarAfterDelay() {
    method hideVisitProgressBar (line 104) | hideVisitProgressBar() {
    method showFormProgressBarAfterDelay (line 112) | showFormProgressBarAfterDelay() {
    method hideFormProgressBar (line 118) | hideFormProgressBar() {
    method reload (line 130) | reload(reason) {
    method navigator (line 136) | get navigator() {

FILE: src/core/renderer.js
  class Renderer (line 3) | class Renderer {
    method renderElement (line 6) | static renderElement(currentElement, newElement) {
    method constructor (line 10) | constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
    method shouldRender (line 19) | get shouldRender() {
    method shouldAutofocus (line 23) | get shouldAutofocus() {
    method reloadReason (line 27) | get reloadReason() {
    method prepareToRender (line 31) | prepareToRender() {
    method render (line 35) | render() {
    method finishRendering (line 39) | finishRendering() {
    method preservingPermanentElements (line 46) | async preservingPermanentElements(callback) {
    method focusFirstAutofocusableElement (line 50) | focusFirstAutofocusableElement() {
    method enteringBardo (line 61) | enteringBardo(currentPermanentElement) {
    method leavingBardo (line 69) | leavingBardo(currentPermanentElement) {
    method connectedSnapshot (line 77) | get connectedSnapshot() {
    method currentElement (line 81) | get currentElement() {
    method newElement (line 85) | get newElement() {
    method permanentElementMap (line 89) | get permanentElementMap() {
    method renderMethod (line 93) | get renderMethod() {

FILE: src/core/session.js
  class Session (line 23) | class Session {
    method constructor (line 45) | constructor(recentRequests) {
    method start (line 52) | start() {
    method disable (line 70) | disable() {
    method stop (line 74) | stop() {
    method registerAdapter (line 91) | registerAdapter(adapter) {
    method visit (line 95) | visit(location, options = {}) {
    method refresh (line 108) | refresh(url, options = {}) {
    method connectStreamSource (line 119) | connectStreamSource(source) {
    method disconnectStreamSource (line 123) | disconnectStreamSource(source) {
    method renderStreamMessage (line 127) | renderStreamMessage(message) {
    method clearCache (line 131) | clearCache() {
    method setProgressBarDelay (line 135) | setProgressBarDelay(delay) {
    method progressBarDelay (line 143) | set progressBarDelay(delay) {
    method progressBarDelay (line 147) | get progressBarDelay() {
    method drive (line 151) | set drive(value) {
    method drive (line 155) | get drive() {
    method formMode (line 159) | set formMode(value) {
    method formMode (line 163) | get formMode() {
    method location (line 167) | get location() {
    method restorationIdentifier (line 171) | get restorationIdentifier() {
    method pageRefreshDebouncePeriod (line 175) | get pageRefreshDebouncePeriod() {
    method pageRefreshDebouncePeriod (line 179) | set pageRefreshDebouncePeriod(value) {
    method shouldPreloadLink (line 186) | shouldPreloadLink(element) {
    method historyPoppedToLocationWithRestorationIdentifierAndDirection (line 205) | historyPoppedToLocationWithRestorationIdentifierAndDirection(location,...
    method historyPoppedWithEmptyState (line 219) | historyPoppedWithEmptyState(location) {
    method scrollPositionChanged (line 227) | scrollPositionChanged(position) {
    method willSubmitFormLinkToLocation (line 233) | willSubmitFormLinkToLocation(link, location) {
    method submittedFormLinkToLocation (line 237) | submittedFormLinkToLocation() {}
    method canPrefetchRequestToLocation (line 241) | canPrefetchRequestToLocation(link, location) {
    method willFollowLinkToLocation (line 251) | willFollowLinkToLocation(link, location, event) {
    method followedLinkToLocation (line 259) | followedLinkToLocation(link, location) {
    method allowsVisitingLocationWithAction (line 268) | allowsVisitingLocationWithAction(location, action) {
    method visitProposedToLocation (line 272) | visitProposedToLocation(location, options) {
    method visitStarted (line 279) | visitStarted(visit) {
    method visitCompleted (line 288) | visitCompleted(visit) {
    method willSubmitForm (line 296) | willSubmitForm(form, submitter) {
    method formSubmitted (line 305) | formSubmitted(form, submitter) {
    method pageBecameInteractive (line 311) | pageBecameInteractive() {
    method pageLoaded (line 316) | pageLoaded() {
    method pageWillUnload (line 320) | pageWillUnload() {
    method receivedMessageFromStream (line 326) | receivedMessageFromStream(message) {
    method viewWillCacheSnapshot (line 332) | viewWillCacheSnapshot() {
    method allowsImmediateRender (line 336) | allowsImmediateRender({ element }, options) {
    method viewRenderedSnapshot (line 350) | viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
    method preloadOnLoadLinksForView (line 355) | preloadOnLoadLinksForView(element) {
    method viewInvalidated (line 359) | viewInvalidated(reason) {
    method frameLoaded (line 365) | frameLoaded(frame) {
    method frameRendered (line 369) | frameRendered(fetchResponse, frame) {
    method applicationAllowsFollowingLinkToLocation (line 375) | applicationAllowsFollowingLinkToLocation(link, location, ev) {
    method applicationAllowsVisitingLocation (line 380) | applicationAllowsVisitingLocation(location) {
    method notifyApplicationAfterClickingLinkToLocation (line 385) | notifyApplicationAfterClickingLinkToLocation(link, location, event) {
    method notifyApplicationBeforeVisitingLocation (line 393) | notifyApplicationBeforeVisitingLocation(location) {
    method notifyApplicationAfterVisitingLocation (line 400) | notifyApplicationAfterVisitingLocation(location, action) {
    method notifyApplicationBeforeCachingSnapshot (line 404) | notifyApplicationBeforeCachingSnapshot() {
    method notifyApplicationBeforeRender (line 408) | notifyApplicationBeforeRender(newBody, options) {
    method notifyApplicationAfterRender (line 415) | notifyApplicationAfterRender(renderMethod) {
    method notifyApplicationAfterPageLoad (line 419) | notifyApplicationAfterPageLoad(timing = {}) {
    method notifyApplicationAfterFrameLoad (line 425) | notifyApplicationAfterFrameLoad(frame) {
    method notifyApplicationAfterFrameRender (line 429) | notifyApplicationAfterFrameRender(fetchResponse, frame) {
    method submissionIsNavigatable (line 439) | submissionIsNavigatable(form, submitter) {
    method elementIsNavigatable (line 453) | elementIsNavigatable(element) {
    method getActionForLink (line 477) | getActionForLink(link) {
    method snapshot (line 481) | get snapshot() {
  function extendURLWithDeprecatedProperties (line 497) | function extendURLWithDeprecatedProperties(url) {
  method get (line 503) | get() {

FILE: src/core/snapshot.js
  class Snapshot (line 3) | class Snapshot {
    method constructor (line 4) | constructor(element) {
    method activeElement (line 8) | get activeElement() {
    method children (line 12) | get children() {
    method hasAnchor (line 16) | hasAnchor(anchor) {
    method getElementForAnchor (line 20) | getElementForAnchor(anchor) {
    method isConnected (line 24) | get isConnected() {
    method firstAutofocusableElement (line 28) | get firstAutofocusableElement() {
    method permanentElements (line 32) | get permanentElements() {
    method getPermanentElementById (line 36) | getPermanentElementById(id) {
    method getPermanentElementMapForSnapshot (line 40) | getPermanentElementMapForSnapshot(snapshot) {
  function getPermanentElementById (line 55) | function getPermanentElementById(node, id) {
  function queryPermanentElementsAll (line 59) | function queryPermanentElementsAll(node) {

FILE: src/core/streams/stream_actions.js
  method after (line 5) | after() {
  method append (line 10) | append() {
  method before (line 15) | before() {
  method prepend (line 20) | prepend() {
  method remove (line 25) | remove() {
  method replace (line 29) | replace() {
  method update (line 41) | update() {
  method refresh (line 54) | refresh() {

FILE: src/core/streams/stream_message.js
  class StreamMessage (line 3) | class StreamMessage {
    method wrap (line 6) | static wrap(message) {
    method constructor (line 14) | constructor(fragment) {
  function importStreamElements (line 19) | function importStreamElements(fragment) {

FILE: src/core/streams/stream_message_renderer.js
  class StreamMessageRenderer (line 5) | class StreamMessageRenderer {
    method render (line 6) | render({ fragment }) {
    method enteringBardo (line 18) | enteringBardo(currentPermanentElement, newPermanentElement) {
    method leavingBardo (line 22) | leavingBardo() {}
  function getPermanentElementMapForFragment (line 25) | function getPermanentElementMapForFragment(fragment) {
  function withAutofocusFromFragment (line 43) | async function withAutofocusFromFragment(fragment, callback) {
  function withPreservedFocus (line 76) | async function withPreservedFocus(callback) {
  function firstAutofocusableElementInStreams (line 90) | function firstAutofocusableElementInStreams(nodeListOfStreamElements) {

FILE: src/core/url.js
  function expandURL (line 3) | function expandURL(locatable) {
  function getAnchor (line 7) | function getAnchor(url) {
  function getAction (line 17) | function getAction(form, submitter) {
  function getExtension (line 23) | function getExtension(url) {
  function isPrefixedBy (line 27) | function isPrefixedBy(baseURL, url) {
  function locationIsVisitable (line 32) | function locationIsVisitable(location, rootLocation) {
  function getLocationForLink (line 36) | function getLocationForLink(link) {
  function getRequestURL (line 40) | function getRequestURL(url) {
  function toCacheKey (line 45) | function toCacheKey(url) {
  function urlsAreEqual (line 49) | function urlsAreEqual(left, right) {
  function getPathComponents (line 53) | function getPathComponents(url) {
  function getLastPathComponent (line 57) | function getLastPathComponent(url) {
  function addTrailingSlash (line 61) | function addTrailingSlash(value) {

FILE: src/core/view.js
  class View (line 3) | class View {
    method constructor (line 7) | constructor(delegate, element) {
    method scrollToAnchor (line 14) | scrollToAnchor(anchor) {
    method scrollToAnchorFromLocation (line 24) | scrollToAnchorFromLocation(location) {
    method scrollToElement (line 28) | scrollToElement(element) {
    method focusElement (line 32) | focusElement(element) {
    method scrollToPosition (line 44) | scrollToPosition({ x, y }) {
    method scrollToTop (line 48) | scrollToTop() {
    method scrollRoot (line 52) | get scrollRoot() {
    method render (line 58) | async render(renderer) {
    method invalidate (line 90) | invalidate(reason) {
    method prepareToRenderSnapshot (line 94) | async prepareToRenderSnapshot(renderer) {
    method markAsPreview (line 99) | markAsPreview(isPreview) {
    method markVisitDirection (line 107) | markVisitDirection(direction) {
    method unmarkVisitDirection (line 111) | unmarkVisitDirection() {
    method renderSnapshot (line 115) | async renderSnapshot(renderer) {
    method finishRenderingSnapshot (line 119) | finishRenderingSnapshot(renderer) {

FILE: src/elements/frame_element.js
  class FrameElement (line 22) | class FrameElement extends HTMLElement {
    method observedAttributes (line 27) | static get observedAttributes() {
    method constructor (line 31) | constructor() {
    method connectedCallback (line 36) | connectedCallback() {
    method disconnectedCallback (line 40) | disconnectedCallback() {
    method reload (line 44) | reload() {
    method attributeChangedCallback (line 48) | attributeChangedCallback(name) {
    method src (line 61) | get src() {
    method src (line 68) | set src(value) {
    method refresh (line 79) | get refresh() {
    method refresh (line 86) | set refresh(value) {
    method shouldReloadWithMorph (line 94) | get shouldReloadWithMorph() {
    method loading (line 101) | get loading() {
    method loading (line 108) | set loading(value) {
    method disabled (line 121) | get disabled() {
    method disabled (line 130) | set disabled(value) {
    method autoscroll (line 143) | get autoscroll() {
    method autoscroll (line 152) | set autoscroll(value) {
    method complete (line 163) | get complete() {
    method isActive (line 172) | get isActive() {
    method isPreview (line 181) | get isPreview() {
  function frameLoadingStyleFromString (line 186) | function frameLoadingStyleFromString(style) {

FILE: src/elements/stream_element.js
  class StreamElement (line 28) | class StreamElement extends HTMLElement {
    method renderElement (line 29) | static async renderElement(newElement) {
    method connectedCallback (line 33) | async connectedCallback() {
    method render (line 43) | async render() {
    method disconnect (line 54) | disconnect() {
    method removeDuplicateTargetChildren (line 64) | removeDuplicateTargetChildren() {
    method duplicateChildren (line 71) | get duplicateChildren() {
    method removeDuplicateTargetSiblings (line 81) | removeDuplicateTargetSiblings() {
    method duplicateSiblings (line 88) | get duplicateSiblings() {
    method performAction (line 98) | get performAction() {
    method targetElements (line 112) | get targetElements() {
    method templateContent (line 125) | get templateContent() {
    method templateElement (line 132) | get templateElement() {
    method action (line 146) | get action() {
    method target (line 154) | get target() {
    method targets (line 161) | get targets() {
    method requestId (line 168) | get requestId() {
    method #raise (line 172) | #raise(message) {
    method description (line 176) | get description() {
    method beforeRenderEvent (line 180) | get beforeRenderEvent() {
    method targetElementsById (line 188) | get targetElementsById() {
    method targetElementsByQuery (line 198) | get targetElementsByQuery() {

FILE: src/elements/stream_source_element.js
  class StreamSourceElement (line 3) | class StreamSourceElement extends HTMLElement {
    method connectedCallback (line 6) | connectedCallback() {
    method disconnectedCallback (line 12) | disconnectedCallback() {
    method src (line 20) | get src() {

FILE: src/http/fetch.js
  function fetchWithTurboHeaders (line 6) | function fetchWithTurboHeaders(url, options = {}) {

FILE: src/http/fetch_request.js
  function fetchMethodFromString (line 6) | function fetchMethodFromString(method) {
  function fetchEnctypeFromString (line 29) | function fetchEnctypeFromString(encoding) {
  class FetchRequest (line 46) | class FetchRequest {
    method constructor (line 50) | constructor(delegate, method, location, requestBody = new URLSearchPar...
    method method (line 68) | get method() {
    method method (line 72) | set method(value) {
    method headers (line 85) | get headers() {
    method headers (line 89) | set headers(value) {
    method body (line 93) | get body() {
    method body (line 101) | set body(value) {
    method location (line 105) | get location() {
    method params (line 109) | get params() {
    method entries (line 113) | get entries() {
    method cancel (line 117) | cancel() {
    method perform (line 121) | async perform() {
    method receive (line 148) | async receive(response) {
    method defaultHeaders (line 165) | get defaultHeaders() {
    method isSafe (line 171) | get isSafe() {
    method abortSignal (line 175) | get abortSignal() {
    method acceptResponseType (line 179) | acceptResponseType(mimeType) {
    method #allowRequestToBeIntercepted (line 183) | async #allowRequestToBeIntercepted(fetchOptions) {
    method #willDelegateErrorHandling (line 200) | #willDelegateErrorHandling(error) {
  function isSafe (line 211) | function isSafe(fetchMethod) {
  function buildResourceAndBody (line 215) | function buildResourceAndBody(resource, method, requestBody, enctype) {
  function entriesExcludingFiles (line 228) | function entriesExcludingFiles(requestBody) {
  function mergeIntoURLSearchParams (line 239) | function mergeIntoURLSearchParams(url, requestBody) {

FILE: src/http/fetch_response.js
  class FetchResponse (line 3) | class FetchResponse {
    method constructor (line 4) | constructor(response) {
    method succeeded (line 8) | get succeeded() {
    method failed (line 12) | get failed() {
    method clientError (line 16) | get clientError() {
    method serverError (line 20) | get serverError() {
    method redirected (line 24) | get redirected() {
    method location (line 28) | get location() {
    method isHTML (line 32) | get isHTML() {
    method statusCode (line 36) | get statusCode() {
    method contentType (line 40) | get contentType() {
    method responseText (line 44) | get responseText() {
    method responseHTML (line 48) | get responseHTML() {
    method header (line 56) | header(name) {

FILE: src/observers/appearance_observer.js
  class AppearanceObserver (line 1) | class AppearanceObserver {
    method constructor (line 4) | constructor(delegate, element) {
    method start (line 10) | start() {
    method stop (line 17) | stop() {

FILE: src/observers/cache_observer.js
  class CacheObserver (line 1) | class CacheObserver {
    method start (line 6) | start() {
    method stop (line 13) | stop() {
    method temporaryElements (line 26) | get temporaryElements() {

FILE: src/observers/form_link_click_observer.js
  class FormLinkClickObserver (line 4) | class FormLinkClickObserver {
    method constructor (line 5) | constructor(delegate, element) {
    method start (line 10) | start() {
    method stop (line 14) | stop() {
    method canPrefetchRequestToLocation (line 20) | canPrefetchRequestToLocation(link, location) {
    method prefetchAndCacheRequestToLocation (line 24) | prefetchAndCacheRequestToLocation(link, location) {
    method willFollowLinkToLocation (line 30) | willFollowLinkToLocation(link, location, originalEvent) {
    method followedLinkToLocation (line 37) | followedLinkToLocation(link, location) {

FILE: src/observers/form_submit_observer.js
  class FormSubmitObserver (line 3) | class FormSubmitObserver {
    method constructor (line 6) | constructor(delegate, eventTarget) {
    method start (line 11) | start() {
    method stop (line 18) | stop() {
  function submissionDoesNotDismissDialog (line 49) | function submissionDoesNotDismissDialog(form, submitter) {
  function submissionDoesNotTargetIFrame (line 55) | function submissionDoesNotTargetIFrame(form, submitter) {

FILE: src/observers/link_click_observer.js
  class LinkClickObserver (line 4) | class LinkClickObserver {
    method constructor (line 7) | constructor(delegate, eventTarget) {
    method start (line 12) | start() {
    method stop (line 19) | stop() {
    method clickEventIsSignificant (line 45) | clickEventIsSignificant(event) {

FILE: src/observers/link_prefetch_observer.js
  class LinkPrefetchObserver (line 11) | class LinkPrefetchObserver {
    method constructor (line 15) | constructor(delegate, eventTarget) {
    method start (line 20) | start() {
    method stop (line 30) | stop() {
    method prepareRequest (line 110) | prepareRequest(request) {
    method requestSucceededWithResponse (line 125) | requestSucceededWithResponse() {}
    method requestStarted (line 127) | requestStarted(fetchRequest) {}
    method requestErrored (line 129) | requestErrored(fetchRequest) {}
    method requestFinished (line 131) | requestFinished(fetchRequest) {}
    method requestPreventedHandlingResponse (line 133) | requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
    method requestFailedWithResponse (line 135) | requestFailedWithResponse(fetchRequest, fetchResponse) {}
    method #cacheTtl (line 137) | get #cacheTtl() {
    method #isPrefetchable (line 141) | #isPrefetchable(link) {

FILE: src/observers/page_observer.js
  class PageObserver (line 8) | class PageObserver {
    method constructor (line 12) | constructor(delegate) {
    method start (line 16) | start() {
    method stop (line 27) | stop() {
    method pageIsInteractive (line 44) | pageIsInteractive() {
    method pageIsComplete (line 51) | pageIsComplete() {
    method readyState (line 63) | get readyState() {

FILE: src/observers/scroll_observer.js
  class ScrollObserver (line 1) | class ScrollObserver {
    method constructor (line 4) | constructor(delegate) {
    method start (line 8) | start() {
    method stop (line 16) | stop() {
    method updatePosition (line 29) | updatePosition(position) {

FILE: src/observers/stream_observer.js
  class StreamObserver (line 4) | class StreamObserver {
    method constructor (line 8) | constructor(delegate) {
    method start (line 12) | start() {
    method stop (line 19) | stop() {
    method connectStreamSource (line 26) | connectStreamSource(source) {
    method disconnectStreamSource (line 33) | disconnectStreamSource(source) {
    method streamSourceIsConnected (line 40) | streamSourceIsConnected(source) {
    method receiveMessageResponse (line 58) | async receiveMessageResponse(response) {
    method receiveMessageHTML (line 65) | receiveMessageHTML(html) {
  function fetchResponseFromEvent (line 70) | function fetchResponseFromEvent(event) {
  function fetchResponseIsStream (line 77) | function fetchResponseIsStream(response) {

FILE: src/tests/fixtures/test.js
  function serializeToChannel (line 2) | function serializeToChannel(object, visited = new Set()) {
  function eventListener (line 35) | function eventListener(event) {
  method constructor (line 102) | constructor() {
  method connectedCallback (line 106) | connectedCallback() {
  method constructor (line 119) | constructor() {
  method constructor (line 133) | constructor() {
  method connectedCallback (line 137) | connectedCallback() {

FILE: src/tests/functional/form_mode_tests.js
  function gotoPageWithFormMode (line 67) | async function gotoPageWithFormMode(page, formMode) {
  function formSubmitStarted (line 72) | function formSubmitStarted(page) {

FILE: src/tests/functional/form_submission_tests.js
  function formSubmitStarted (line 1248) | function formSubmitStarted(page) {
  function formSubmitEnded (line 1252) | function formSubmitEnded(page) {

FILE: src/tests/functional/frame_tests.js
  function withoutChangingEventListenersCount (line 1045) | async function withoutChangingEventListenersCount(page, callback) {
  function frameScriptEvaluationCount (line 1090) | function frameScriptEvaluationCount(page) {

FILE: src/tests/functional/import_tests.js
  function assertTurboInterface (line 13) | async function assertTurboInterface(page) {
  function assertTypeOf (line 31) | async function assertTypeOf(page, propertyName, propertyType) {

FILE: src/tests/functional/page_refresh_stream_action_tests.js
  function fetchRequestId (line 66) | async function fetchRequestId(page) {
  function setLongerPageRefreshDebouncePeriod (line 73) | async function setLongerPageRefreshDebouncePeriod(page, period = 500) {

FILE: src/tests/functional/page_refresh_tests.js
  function assertPageScroll (line 424) | async function assertPageScroll(page, top, left) {

FILE: src/tests/functional/preloader_tests.js
  function urlInSnapshotCache (line 94) | function urlInSnapshotCache(page, href) {

FILE: src/tests/functional/rendering_tests.js
  function deepElementsEqual (line 611) | function deepElementsEqual(page, left, right) {
  function headScriptEvaluationCount (line 618) | function headScriptEvaluationCount(page) {
  function bodyScriptEvaluationCount (line 622) | function bodyScriptEvaluationCount(page) {
  function isStylesheetEvaluated (line 626) | function isStylesheetEvaluated(page) {
  function isNoscriptStylesheetEvaluated (line 632) | function isNoscriptStylesheetEvaluated(page) {
  function modifyBodyAfterRemoval (line 638) | function modifyBodyAfterRemoval(page) {

FILE: src/tests/functional/stream_tests.js
  method customUpdate (line 80) | customUpdate(newStream) {

FILE: src/tests/functional/visit_tests.js
  function cancelNextVisit (line 250) | function cancelNextVisit(page) {
  function contentTypeOfURL (line 254) | function contentTypeOfURL(url) {
  function visitLocation (line 343) | async function visitLocation(page, location) {
  function assertVisitDirectionAttribute (line 347) | async function assertVisitDirectionAttribute(page, direction) {

FILE: src/tests/helpers/dom_test_case.js
  class DOMTestCase (line 1) | class DOMTestCase {
    method setup (line 4) | async setup() {
    method teardown (line 9) | async teardown() {
    method append (line 14) | append(node) {
    method find (line 18) | find(selector) {
    method fixtureHTML (line 22) | get fixtureHTML() {
    method fixtureHTML (line 26) | set fixtureHTML(html) {

FILE: src/tests/helpers/page.js
  function attributeForSelector (line 1) | function attributeForSelector(page, selector, attributeName) {
  function cancelNextEvent (line 5) | function cancelNextEvent(page, eventName) {
  function clickWithoutScrolling (line 12) | function clickWithoutScrolling(page, selector, options = {}) {
  function clearLocalStorage (line 18) | function clearLocalStorage(page) {
  function disposeAll (line 22) | function disposeAll(...handles) {
  function getComputedStyle (line 26) | function getComputedStyle(page, selector, propertyName) {
  function cssClassIsDefined (line 36) | function cssClassIsDefined(page, className) {
  function getFromLocalStorage (line 48) | function getFromLocalStorage(page, key) {
  function getSearchParam (line 52) | function getSearchParam(url, key) {
  function hasSelector (line 56) | async function hasSelector(page, selector) {
  function isScrolledToSelector (line 60) | async function isScrolledToSelector(page, selector) {
  function nextBeat (line 75) | function nextBeat() {
  function nextBody (line 79) | function nextBody(_page, timeout = 500) {
  function reloadPage (line 83) | async function reloadPage(page, timeout = 500) {
  function nextPageRefresh (line 90) | async function nextPageRefresh(page, timeout = 500) {
  function nextEventNamed (line 98) | async function nextEventNamed(page, eventName, expectedDetail = {}) {
  function nextEventOnTarget (line 113) | async function nextEventOnTarget(page, elementId, eventName) {
  function listenForEventOnTarget (line 126) | async function listenForEventOnTarget(page, elementId, eventName) {
  function nextBodyMutation (line 138) | async function nextBodyMutation(page) {
  function noNextBodyMutation (line 150) | async function noNextBodyMutation(page) {
  function nextAttributeMutationNamed (line 155) | async function nextAttributeMutationNamed(page, elementId, attributeName) {
  function noNextAttributeMutationNamed (line 169) | async function noNextAttributeMutationNamed(page, elementId, attributeNa...
  function noNextEventNamed (line 174) | async function noNextEventNamed(page, eventName, expectedDetail = {}) {
  function noNextEventOnTarget (line 181) | async function noNextEventOnTarget(page, elementId, eventName) {
  function outerHTMLForSelector (line 186) | async function outerHTMLForSelector(page, selector) {
  function pathname (line 191) | function pathname(url) {
  function withHash (line 197) | function withHash(value) {
  function withPathname (line 201) | function withPathname(value) {
  function withSearch (line 205) | function withSearch(value) {
  function withSearchParam (line 209) | function withSearchParam(name, value) {
  function pathnameForIFrame (line 215) | async function pathnameForIFrame(page, name) {
  function propertyForSelector (line 226) | function propertyForSelector(page, selector, propertyName) {
  function resetMutationLogs (line 230) | function resetMutationLogs(page) {
  function readArray (line 236) | async function readArray(page, identifier, length) {
  function readBodyMutationLogs (line 250) | function readBodyMutationLogs(page, length) {
  function readEventLogs (line 254) | function readEventLogs(page, length) {
  function readMutationLogs (line 258) | function readMutationLogs(page, length) {
  function refreshWithStream (line 262) | function refreshWithStream(page) {
  function searchParams (line 266) | function searchParams(url) {
  function setLocalStorageFromEvent (line 272) | function setLocalStorageFromEvent(page, eventName, storageKey, storageVa...
  function scrollPosition (line 281) | function scrollPosition(page) {
  function isScrolledToTop (line 285) | async function isScrolledToTop(page) {
  function scrollToSelector (line 290) | function scrollToSelector(page, selector) {
  function sleep (line 294) | function sleep(timeout = 0) {
  function strictElementEquals (line 298) | async function strictElementEquals(left, right) {
  function textContent (line 302) | function textContent(page, html) {
  function visitAction (line 311) | function visitAction(page) {
  function willChangeBody (line 323) | async function willChangeBody(page, callback) {

FILE: src/tests/integration/ujs_tests.js
  function assertRequestLimit (line 30) | async function assertRequestLimit(page, count, callback) {

FILE: src/tests/server.mjs
  function receiveMessage (line 192) | function receiveMessage(content, id, target) {
  function renderMessage (line 200) | function renderMessage(content, id, target = "messages") {
  function renderMessageForTargets (line 208) | function renderMessageForTargets(content, id, targets) {
  function renderPageRefresh (line 216) | function renderPageRefresh(requestId) {
  function acceptsStreams (line 222) | function acceptsStreams(request) {
  function renderSSEData (line 226) | function renderSSEData(data) {
  function escapeHTML (line 235) | function escapeHTML(html) {

FILE: src/tests/unit/deprecated_adapter_support_tests.js
  class DeprecatedAdapterSupportTest (line 4) | class DeprecatedAdapterSupportTest {
    method visitProposedToLocation (line 7) | visitProposedToLocation(location, _options) {
    method visitStarted (line 11) | visitStarted(visit) {
    method visitCompleted (line 16) | visitCompleted(_visit) {}
    method visitFailed (line 18) | visitFailed(_visit) {}
    method visitRequestStarted (line 20) | visitRequestStarted(_visit) {}
    method visitRequestCompleted (line 22) | visitRequestCompleted(_visit) {}
    method visitRequestFailedWithStatusCode (line 24) | visitRequestFailedWithStatusCode(_visit, _statusCode) {}
    method visitRequestFinished (line 26) | visitRequestFinished(_visit) {}
    method visitRendered (line 28) | visitRendered(_visit) {}
    method formSubmissionStarted (line 30) | formSubmissionStarted(_formSubmission) {}
    method formSubmissionFinished (line 32) | formSubmissionFinished(_formSubmission) {}
    method pageInvalidated (line 34) | pageInvalidated() {}

FILE: src/tests/unit/native_adapter_support_tests.js
  class NativeAdapterSupportTest (line 4) | class NativeAdapterSupportTest {
    method visitProposedToLocation (line 18) | visitProposedToLocation(location, options) {
    method visitStarted (line 22) | visitStarted(visit) {
    method visitCompleted (line 26) | visitCompleted(visit) {
    method visitRequestStarted (line 30) | visitRequestStarted(visit) {
    method visitRequestCompleted (line 34) | visitRequestCompleted(visit) {
    method visitRequestFailedWithStatusCode (line 38) | visitRequestFailedWithStatusCode(visit, _statusCode) {
    method visitRequestFinished (line 42) | visitRequestFinished(visit) {
    method visitRendered (line 46) | visitRendered(_visit) {}
    method formSubmissionStarted (line 48) | formSubmissionStarted(formSubmission) {
    method formSubmissionFinished (line 52) | formSubmissionFinished(formSubmission) {
    method pageInvalidated (line 56) | pageInvalidated() {}
    method linkPrefetchingIsEnabledForLocation (line 58) | linkPrefetchingIsEnabledForLocation(location) {

FILE: src/tests/unit/stream_element_tests.js
  function createStreamElement (line 8) | function createStreamElement(action, target, templateElement, attributes...
  function createTemplateElement (line 17) | function createTemplateElement(html) {
  class StreamElementTests (line 23) | class StreamElementTests extends DOMTestCase {}

FILE: src/util.js
  function activateScriptElement (line 1) | function activateScriptElement(element) {
  function copyElementAttributes (line 17) | function copyElementAttributes(destinationElement, sourceElement) {
  function createDocumentFragment (line 23) | function createDocumentFragment(html) {
  function dispatch (line 29) | function dispatch(eventName, { target, cancelable, detail } = {}) {
  function cancelEvent (line 46) | function cancelEvent(event) {
  function nextRepaint (line 51) | function nextRepaint() {
  function nextAnimationFrame (line 59) | function nextAnimationFrame() {
  function nextEventLoopTick (line 63) | function nextEventLoopTick() {
  function nextMicrotask (line 67) | function nextMicrotask() {
  function parseHTMLDocument (line 71) | function parseHTMLDocument(html = "") {
  function unindent (line 75) | function unindent(strings, ...values) {
  function interpolate (line 82) | function interpolate(strings, values) {
  function uuid (line 89) | function uuid() {
  function getAttribute (line 105) | function getAttribute(attributeName, ...elements) {
  function hasAttribute (line 113) | function hasAttribute(attributeName, ...elements) {
  function markAsBusy (line 117) | function markAsBusy(...elements) {
  function clearBusyState (line 126) | function clearBusyState(...elements) {
  function waitForLoad (line 136) | function waitForLoad(element, timeoutInMilliseconds = 2000) {
  function getHistoryMethodForAction (line 150) | function getHistoryMethodForAction(action) {
  function isAction (line 160) | function isAction(action) {
  function getVisitAction (line 164) | function getVisitAction(...elements) {
  function getMetaElement (line 170) | function getMetaElement(name) {
  function getMetaContent (line 174) | function getMetaContent(name) {
  function getCspNonce (line 179) | function getCspNonce() {
  function setMetaContent (line 188) | function setMetaContent(name, content) {
  function findClosestRecursively (line 203) | function findClosestRecursively(element, selector) {
  function elementIsStylesheet (line 211) | function elementIsStylesheet(element) {
  function elementIsFocusable (line 216) | function elementIsFocusable(element) {
  function queryAutofocusableElement (line 222) | function queryAutofocusableElement(elementOrDocumentFragment) {
  function around (line 226) | async function around(callback, reader) {
  function doesNotTargetIFrame (line 238) | function doesNotTargetIFrame(name) {
  function findLinkFromClickTarget (line 252) | function findLinkFromClickTarget(target) {
  function debounce (line 265) | function debounce(fn, delay) {
Condensed preview — 223 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (606K chars).
[
  {
    "path": ".devcontainer/Dockerfile",
    "chars": 180,
    "preview": "# See comments in devcontainer.json for details of setting the playwright tag.\nARG PLAYWRIGHT_TAG=\"v1.51.1-jammy\"\nFROM m"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 1167,
    "preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:\n// https://github.co"
  },
  {
    "path": ".eslintignore",
    "chars": 20,
    "preview": "dist/\nnode_modules/\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 986,
    "preview": "module.exports = {\n  env: {\n    browser: true,\n    es2021: true\n  },\n  extends: [\"eslint:recommended\"],\n  overrides: [\n "
  },
  {
    "path": ".github/scripts/publish-dev-build",
    "chars": 1139,
    "preview": "#!/usr/bin/env bash\nset -eux\n\nDEV_BUILD_REPO_NAME=\"hotwired/dev-builds\"\nDEV_BUILD_ORIGIN_URL=\"https://${1}@github.com/${"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 872,
    "preview": "name: CI\n\non: [push, pull_request]\n\njobs:\n  build:\n\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v"
  },
  {
    "path": ".github/workflows/dev-builds.yml",
    "chars": 478,
    "preview": "name: dev-builds\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n      - 'builds/**'\n\njobs:\n  build:\n    ru"
  },
  {
    "path": ".gitignore",
    "chars": 58,
    "preview": "/dist\n/node_modules\n/test-results\n*.log\npackage-lock.json\n"
  },
  {
    "path": ".prettierignore",
    "chars": 20,
    "preview": "dist/\nnode_modules/\n"
  },
  {
    "path": ".prettierrc.json",
    "chars": 93,
    "preview": "{\n  \"singleQuote\": false,\n  \"printWidth\": 120,\n  \"semi\": false,\n  \"trailingComma\" : \"none\"\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 556,
    "preview": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  //"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 754,
    "preview": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=733558\n  // for the documentation about the tasks.json format\n  \"vers"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 98,
    "preview": "# Changelog\n\nPlease see [our GitHub \"Releases\" page](https://github.com/hotwired/turbo/releases).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3325,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3443,
    "preview": "[![Version](https://img.shields.io/npm/v/@hotwired/turbo)](https://www.npmjs.com/package/@hotwired/turbo)\n[![License](ht"
  },
  {
    "path": "MIT-LICENSE",
    "chars": 1048,
    "preview": "Copyright (c) 37signals\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software a"
  },
  {
    "path": "README.md",
    "chars": 1010,
    "preview": "# Turbo\n\nTurbo uses complementary techniques to dramatically reduce the amount of custom JavaScript that most web applic"
  },
  {
    "path": "package.json",
    "chars": 1848,
    "preview": "{\n  \"name\": \"@hotwired/turbo\",\n  \"version\": \"8.0.23\",\n  \"description\": \"The speed of a single-page web application witho"
  },
  {
    "path": "playwright.config.js",
    "chars": 869,
    "preview": "import { devices } from \"@playwright/test\"\n\nconst config = {\n  projects: [\n    {\n      name: \"chrome\",\n      use: {\n    "
  },
  {
    "path": "rollup.config.js",
    "chars": 570,
    "preview": "import resolve from \"@rollup/plugin-node-resolve\"\n\nimport { version } from \"./package.json\"\nconst year = new Date().getF"
  },
  {
    "path": "src/core/bardo.js",
    "chars": 2035,
    "preview": "export class Bardo {\n  static async preservingPermanentElements(delegate, permanentElementMap, callback) {\n    const bar"
  },
  {
    "path": "src/core/cache.js",
    "chars": 451,
    "preview": "import { setMetaContent } from \"../util\"\n\nexport class Cache {\n  constructor(session) {\n    this.session = session\n  }\n\n"
  },
  {
    "path": "src/core/config/drive.js",
    "chars": 592,
    "preview": "export const drive = {\n  enabled: true,\n  progressBarDelay: 500,\n  unvisitableExtensions: new Set(\n    [\n      \".7z\", \"."
  },
  {
    "path": "src/core/config/forms.js",
    "chars": 826,
    "preview": "import { cancelEvent } from \"../../util\"\n\nconst submitter = {\n  \"aria-disabled\": {\n    beforeSubmit: submitter => {\n    "
  },
  {
    "path": "src/core/config/index.js",
    "chars": 108,
    "preview": "import { drive } from \"./drive\"\nimport { forms } from \"./forms\"\n\nexport const config = {\n  drive,\n  forms\n}\n"
  },
  {
    "path": "src/core/drive/error_renderer.js",
    "chars": 1053,
    "preview": "import { activateScriptElement } from \"../../util\"\nimport { Renderer } from \"../renderer\"\n\nexport class ErrorRenderer ex"
  },
  {
    "path": "src/core/drive/form_submission.js",
    "chars": 7804,
    "preview": "import { FetchRequest, FetchMethod, fetchMethodFromString, fetchEnctypeFromString, isSafe } from \"../../http/fetch_reque"
  },
  {
    "path": "src/core/drive/head_snapshot.js",
    "chars": 3162,
    "preview": "import { elementIsStylesheet } from \"../../util\"\nimport { Snapshot } from \"../snapshot\"\n\nexport class HeadSnapshot exten"
  },
  {
    "path": "src/core/drive/history.js",
    "chars": 2705,
    "preview": "import { uuid } from \"../../util\"\n\nexport class History {\n  location\n  restorationIdentifier = uuid()\n  restorationData "
  },
  {
    "path": "src/core/drive/limited_set.js",
    "chars": 308,
    "preview": "export class LimitedSet extends Set {\n  constructor(maxSize) {\n    super()\n    this.maxSize = maxSize\n  }\n\n  add(value) "
  },
  {
    "path": "src/core/drive/morphing_page_renderer.js",
    "chars": 935,
    "preview": "import { PageRenderer } from \"./page_renderer\"\nimport { dispatch } from \"../../util\"\nimport { morphElements, shouldRefre"
  },
  {
    "path": "src/core/drive/navigator.js",
    "chars": 4526,
    "preview": "import { getVisitAction } from \"../../util\"\nimport { FormSubmission } from \"./form_submission\"\nimport { expandURL } from"
  },
  {
    "path": "src/core/drive/page_renderer.js",
    "chars": 5862,
    "preview": "import { activateScriptElement, elementIsStylesheet, waitForLoad } from \"../../util\"\nimport { Renderer } from \"../render"
  },
  {
    "path": "src/core/drive/page_snapshot.js",
    "chars": 2834,
    "preview": "import { elementIsStylesheet, parseHTMLDocument } from \"../../util\"\nimport { Snapshot } from \"../snapshot\"\nimport { expa"
  },
  {
    "path": "src/core/drive/page_view.js",
    "chars": 2180,
    "preview": "import { nextEventLoopTick } from \"../../util\"\nimport { View } from \"../view\"\nimport { ErrorRenderer } from \"./error_ren"
  },
  {
    "path": "src/core/drive/prefetch_cache.js",
    "chars": 1102,
    "preview": "import { LRUCache } from \"../lru_cache\"\nimport { toCacheKey } from \"../url\"\n\nconst PREFETCH_DELAY = 100\n\nclass PrefetchC"
  },
  {
    "path": "src/core/drive/preloader.js",
    "chars": 1832,
    "preview": "import { PageSnapshot } from \"./page_snapshot\"\nimport { FetchMethod, FetchRequest } from \"../../http/fetch_request\"\n\nexp"
  },
  {
    "path": "src/core/drive/progress_bar.js",
    "chars": 2903,
    "preview": "import { unindent, getCspNonce } from \"../../util\"\n\nexport const ProgressBarID = \"turbo-progress-bar\"\n\nexport class Prog"
  },
  {
    "path": "src/core/drive/snapshot_cache.js",
    "chars": 228,
    "preview": "import { toCacheKey } from \"../url\"\nimport { LRUCache } from \"../lru_cache\"\n\nexport class SnapshotCache extends LRUCache"
  },
  {
    "path": "src/core/drive/view_transitioner.js",
    "chars": 616,
    "preview": "export class ViewTransitioner {\n  #viewTransitionStarted = false\n  #lastOperation = Promise.resolve()\n\n  renderChange(us"
  },
  {
    "path": "src/core/drive/visit.js",
    "chars": 10715,
    "preview": "import { FetchMethod, FetchRequest } from \"../../http/fetch_request\"\nimport { getAnchor } from \"../url\"\nimport { PageSna"
  },
  {
    "path": "src/core/errors.js",
    "chars": 53,
    "preview": "export class TurboFrameMissingError extends Error {}\n"
  },
  {
    "path": "src/core/frames/frame_controller.js",
    "chars": 17046,
    "preview": "import { FrameElement, FrameLoadingStyle } from \"../../elements/frame_element\"\nimport { FetchMethod, FetchRequest } from"
  },
  {
    "path": "src/core/frames/frame_redirector.js",
    "chars": 2507,
    "preview": "import { FormSubmitObserver } from \"../../observers/form_submit_observer\"\nimport { FrameElement } from \"../../elements/f"
  },
  {
    "path": "src/core/frames/frame_renderer.js",
    "chars": 2511,
    "preview": "import { activateScriptElement, nextRepaint } from \"../../util\"\nimport { Renderer } from \"../renderer\"\n\nexport class Fra"
  },
  {
    "path": "src/core/frames/frame_view.js",
    "chars": 281,
    "preview": "import { Snapshot } from \"../snapshot\"\nimport { View } from \"../view\"\n\nexport class FrameView extends View {\n  missing()"
  },
  {
    "path": "src/core/frames/link_interceptor.js",
    "chars": 1534,
    "preview": "import { findLinkFromClickTarget } from \"../../util\"\n\nexport class LinkInterceptor {\n  constructor(delegate, element) {\n"
  },
  {
    "path": "src/core/frames/morphing_frame_renderer.js",
    "chars": 915,
    "preview": "import { FrameRenderer } from \"./frame_renderer\"\nimport { morphChildren, shouldRefreshFrameWithMorphing, closestFrameRel"
  },
  {
    "path": "src/core/index.js",
    "chars": 4670,
    "preview": "import { Session } from \"./session\"\nimport { PageRenderer } from \"./drive/page_renderer\"\nimport { PageSnapshot } from \"."
  },
  {
    "path": "src/core/lru_cache.js",
    "chars": 1062,
    "preview": "const identity = key => key\n\nexport class LRUCache {\n  keys = []\n  entries = {}\n  #toCacheKey\n\n  constructor(size, toCac"
  },
  {
    "path": "src/core/morphing.js",
    "chars": 3579,
    "preview": "import { Idiomorph } from \"idiomorph\"\nimport { FrameElement } from \"../elements/frame_element\"\nimport { dispatch } from "
  },
  {
    "path": "src/core/native/browser_adapter.js",
    "chars": 3320,
    "preview": "import { ProgressBar } from \"../drive/progress_bar\"\nimport { SystemStatusCode } from \"../drive/visit\"\nimport { uuid, dis"
  },
  {
    "path": "src/core/renderer.js",
    "chars": 2143,
    "preview": "import { Bardo } from \"./bardo\"\n\nexport class Renderer {\n  #activeElement = null\n\n  static renderElement(currentElement,"
  },
  {
    "path": "src/core/session.js",
    "chars": 14543,
    "preview": "import { BrowserAdapter } from \"./native/browser_adapter\"\nimport { CacheObserver } from \"../observers/cache_observer\"\nim"
  },
  {
    "path": "src/core/snapshot.js",
    "chars": 1480,
    "preview": "import { queryAutofocusableElement } from \"../util\"\n\nexport class Snapshot {\n  constructor(element) {\n    this.element ="
  },
  {
    "path": "src/core/streams/stream_actions.js",
    "chars": 1620,
    "preview": "import { session } from \"../\"\nimport { morphElements, morphChildren } from \"../morphing\"\n\nexport const StreamActions = {"
  },
  {
    "path": "src/core/streams/stream_message.js",
    "chars": 834,
    "preview": "import { activateScriptElement, createDocumentFragment } from \"../../util\"\n\nexport class StreamMessage {\n  static conten"
  },
  {
    "path": "src/core/streams/stream_message_renderer.js",
    "chars": 3150,
    "preview": "import { Bardo } from \"../bardo\"\nimport { getPermanentElementById, queryPermanentElementsAll } from \"../snapshot\"\nimport"
  },
  {
    "path": "src/core/url.js",
    "chars": 1694,
    "preview": "import { config } from \"./config\"\n\nexport function expandURL(locatable) {\n  return new URL(locatable.toString(), documen"
  },
  {
    "path": "src/core/view.js",
    "chars": 3239,
    "preview": "import { getAnchor } from \"./url\"\n\nexport class View {\n  #resolveRenderPromise = (_value) => {}\n  #resolveInterceptionPr"
  },
  {
    "path": "src/elements/frame_element.js",
    "chars": 4005,
    "preview": "export const FrameLoadingStyle = {\n  eager: \"eager\",\n  lazy: \"lazy\"\n}\n\n/**\n * Contains a fragment of HTML which is updat"
  },
  {
    "path": "src/elements/index.js",
    "chars": 741,
    "preview": "import { FrameController } from \"../core/frames/frame_controller\"\nimport { FrameElement } from \"./frame_element\"\nimport "
  },
  {
    "path": "src/elements/stream_element.js",
    "chars": 5305,
    "preview": "import { StreamActions } from \"../core/streams/stream_actions\"\nimport { nextRepaint } from \"../util\"\n\n// <turbo-stream a"
  },
  {
    "path": "src/elements/stream_source_element.js",
    "chars": 544,
    "preview": "import { connectStreamSource, disconnectStreamSource } from \"../core/index\"\n\nexport class StreamSourceElement extends HT"
  },
  {
    "path": "src/http/fetch.js",
    "chars": 495,
    "preview": "import { uuid } from \"../util\"\nimport { LimitedSet } from \"../core/drive/limited_set\"\n\nexport const recentRequests = new"
  },
  {
    "path": "src/http/fetch_request.js",
    "chars": 6116,
    "preview": "import { FetchResponse } from \"./fetch_response\"\nimport { expandURL } from \"../core/url\"\nimport { dispatch } from \"../ut"
  },
  {
    "path": "src/http/fetch_response.js",
    "chars": 1076,
    "preview": "import { expandURL } from \"../core/url\"\n\nexport class FetchResponse {\n  constructor(response) {\n    this.response = resp"
  },
  {
    "path": "src/http/index.js",
    "chars": 65,
    "preview": "export * from \"./fetch_request\"\nexport * from \"./fetch_response\"\n"
  },
  {
    "path": "src/index.js",
    "chars": 319,
    "preview": "import \"./polyfills\"\nimport \"./elements\"\nimport \"./script_warning\"\nimport { StreamActions } from \"./core/streams/stream_"
  },
  {
    "path": "src/observers/appearance_observer.js",
    "chars": 660,
    "preview": "export class AppearanceObserver {\n  started = false\n\n  constructor(delegate, element) {\n    this.delegate = delegate\n   "
  },
  {
    "path": "src/observers/cache_observer.js",
    "chars": 620,
    "preview": "export class CacheObserver {\n  selector = \"[data-turbo-temporary]\"\n\n  started = false\n\n  start() {\n    if (!this.started"
  },
  {
    "path": "src/observers/form_link_click_observer.js",
    "chars": 2171,
    "preview": "import { LinkClickObserver } from \"./link_click_observer\"\nimport { getVisitAction } from \"../util\"\n\nexport class FormLin"
  },
  {
    "path": "src/observers/form_submit_observer.js",
    "chars": 1618,
    "preview": "import { doesNotTargetIFrame } from \"../util\"\n\nexport class FormSubmitObserver {\n  started = false\n\n  constructor(delega"
  },
  {
    "path": "src/observers/link_click_observer.js",
    "chars": 1576,
    "preview": "import { getLocationForLink } from \"../core/url\"\nimport { doesNotTargetIFrame, findLinkFromClickTarget } from \"../util\"\n"
  },
  {
    "path": "src/observers/link_prefetch_observer.js",
    "chars": 5444,
    "preview": "import { getLocationForLink } from \"../core/url\"\nimport {\n  dispatch,\n  getMetaContent,\n  findClosestRecursively\n} from "
  },
  {
    "path": "src/observers/page_observer.js",
    "chars": 1449,
    "preview": "export const PageStage = {\n  initial: 0,\n  loading: 1,\n  interactive: 2,\n  complete: 3\n}\n\nexport class PageObserver {\n  "
  },
  {
    "path": "src/observers/scroll_observer.js",
    "chars": 590,
    "preview": "export class ScrollObserver {\n  started = false\n\n  constructor(delegate) {\n    this.delegate = delegate\n  }\n\n  start() {"
  },
  {
    "path": "src/observers/stream_observer.js",
    "chars": 2019,
    "preview": "import { FetchResponse } from \"../http/fetch_response\"\nimport { StreamMessage } from \"../core/streams/stream_message\"\n\ne"
  },
  {
    "path": "src/polyfills/index.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/script_warning.js",
    "chars": 925,
    "preview": "import { unindent } from \"./util\"\n;(() => {\n  const scriptElement = document.currentScript\n  if (!scriptElement) return\n"
  },
  {
    "path": "src/tests/fixtures/422.html",
    "chars": 302,
    "preview": "<html>\n  <head>\n    <title>Unprocessable Content</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"r"
  },
  {
    "path": "src/tests/fixtures/422_morph.html",
    "chars": 416,
    "preview": "<html>\n  <head>\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <meta name=\"turbo-refresh-scroll\" content=\"pr"
  },
  {
    "path": "src/tests/fixtures/422_tall.html",
    "chars": 355,
    "preview": "<html>\n  <head>\n    <title>Unprocessable Content</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"r"
  },
  {
    "path": "src/tests/fixtures/500.html",
    "chars": 302,
    "preview": "<html>\n  <head>\n    <title>Internal Server Error</title>\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"r"
  },
  {
    "path": "src/tests/fixtures/additional_assets.html",
    "chars": 497,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Additional assets</title>\n    <link rel=\"styleshee"
  },
  {
    "path": "src/tests/fixtures/additional_script.html",
    "chars": 452,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Additional assets</title>\n    <link rel=\"styleshee"
  },
  {
    "path": "src/tests/fixtures/async_script.html",
    "chars": 644,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/src/tests/fixtures"
  },
  {
    "path": "src/tests/fixtures/async_script_2.html",
    "chars": 296,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/src/tests/fixtures"
  },
  {
    "path": "src/tests/fixtures/autofocus-inert.html",
    "chars": 963,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Autofocus</title>\n    <script src=\"/dist/turbo.es2"
  },
  {
    "path": "src/tests/fixtures/autofocus.html",
    "chars": 1568,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Autofocus</title>\n    <script src=\"/dist/turbo.es2"
  },
  {
    "path": "src/tests/fixtures/bare.html",
    "chars": 120,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Bare</title>\n  </head>\n  <body>\n  </body>\n</html>\n"
  },
  {
    "path": "src/tests/fixtures/body_noscript.html",
    "chars": 502,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Body noscript test</title>\n    <script src=\"/dist/"
  },
  {
    "path": "src/tests/fixtures/body_noscript_with_content.html",
    "chars": 973,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Body noscript with content</title>\n    <script src"
  },
  {
    "path": "src/tests/fixtures/body_script.html",
    "chars": 536,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Body script</title>\n    <script src=\"/dist/turbo.e"
  },
  {
    "path": "src/tests/fixtures/cache_observer.html",
    "chars": 632,
    "preview": "<!DOCTYPE html>\n <html>\n   <head>\n     <meta charset=\"utf-8\">\n     <title>Turbo</title>\n     <script src=\"/dist/turbo.es"
  },
  {
    "path": "src/tests/fixtures/dir_rtl.html",
    "chars": 298,
    "preview": "<!doctype html>\n<html dir=\"rtl\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Turbo</title>\n    <script src=\"/dist/t"
  },
  {
    "path": "src/tests/fixtures/drive.html",
    "chars": 615,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Drive</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/drive_disabled.html",
    "chars": 1698,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Drive (Disabled by Default)</title>\n    <script sr"
  },
  {
    "path": "src/tests/fixtures/es_locale.html",
    "chars": 296,
    "preview": "<!DOCTYPE html>\n<html lang=\"es\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/tur"
  },
  {
    "path": "src/tests/fixtures/esm.html",
    "chars": 337,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>ESM</title>\n    <script type=\"module\" data-turbo-t"
  },
  {
    "path": "src/tests/fixtures/eval_false_script.html",
    "chars": 535,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>data-turbo-eval=false script</title>\n    <script s"
  },
  {
    "path": "src/tests/fixtures/form.html",
    "chars": 19447,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">"
  },
  {
    "path": "src/tests/fixtures/form_mode.html",
    "chars": 1702,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Form</title>\n    <script src=\"/dist/turbo.es2017-u"
  },
  {
    "path": "src/tests/fixtures/frame_navigation.html",
    "chars": 1808,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <link rel=\"icon\" href=\"data:imag"
  },
  {
    "path": "src/tests/fixtures/frame_preloading.html",
    "chars": 635,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame Preloading</title>\n    <script src=\"/dist/tu"
  },
  {
    "path": "src/tests/fixtures/frame_refresh_after_navigation.html",
    "chars": 133,
    "preview": "<turbo-frame id=\"refresh-after-navigation\">\n  <h2 id=\"refresh-after-navigation-content\">Frame has been navigated</h2>\n</"
  },
  {
    "path": "src/tests/fixtures/frame_refresh_morph.html",
    "chars": 80,
    "preview": "<turbo-frame id=\"refresh-morph\">\n  <h2>Loaded morphed frame</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/frame_refresh_reload.html",
    "chars": 84,
    "preview": "<turbo-frame id=\"refresh-reload\">\n  <h2>Loaded reloadable frame</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/frames/body_script.html",
    "chars": 700,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Body Script</title>\n    <script src=\"/dist"
  },
  {
    "path": "src/tests/fixtures/frames/body_script_2.html",
    "chars": 702,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Body Script 2</title>\n    <script src=\"/di"
  },
  {
    "path": "src/tests/fixtures/frames/empty_head.html",
    "chars": 137,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body>\n  <turbo-frame id=\"empty-head\">\n    <h2>Frame updated</h2>\n  </turbo-frame>"
  },
  {
    "path": "src/tests/fixtures/frames/eval_false_script.html",
    "chars": 767,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Eval False Script</title>\n    <script src="
  },
  {
    "path": "src/tests/fixtures/frames/form-redirect.html",
    "chars": 655,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Form Redirect</title>\n    <script src=\"/di"
  },
  {
    "path": "src/tests/fixtures/frames/form-redirected.html",
    "chars": 419,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Form Redirected</title>\n    <script src=\"/"
  },
  {
    "path": "src/tests/fixtures/frames/form.html",
    "chars": 761,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Form</title>\n    <script src=\"/dist/turbo."
  },
  {
    "path": "src/tests/fixtures/frames/frame.html",
    "chars": 513,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Frame</title>\n    <script src=\"/dist/turbo"
  },
  {
    "path": "src/tests/fixtures/frames/frame_for_eager.html",
    "chars": 379,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Frame for Eager</title>\n    <script src=\"/"
  },
  {
    "path": "src/tests/fixtures/frames/hello.html",
    "chars": 804,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Hello</title>\n    <script src=\"/dist/turbo"
  },
  {
    "path": "src/tests/fixtures/frames/parent.html",
    "chars": 389,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Nested Root: Parent</title>\n    <script src=\"/dist"
  },
  {
    "path": "src/tests/fixtures/frames/part.html",
    "chars": 346,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Part</title>\n    <script src=\"/dist/turbo."
  },
  {
    "path": "src/tests/fixtures/frames/preloading.html",
    "chars": 462,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Preloading</title>\n    <script src=\"/dist/"
  },
  {
    "path": "src/tests/fixtures/frames/recursive.html",
    "chars": 734,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Recursive</title>\n    <script src=\"/dist/t"
  },
  {
    "path": "src/tests/fixtures/frames/self.html",
    "chars": 383,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frames: Self</title>\n    <script src=\"/dist/turbo."
  },
  {
    "path": "src/tests/fixtures/frames/unvisitable.html",
    "chars": 431,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/frames.html",
    "chars": 9926,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:click turbo:before-render\">\n  <head>\n    <meta charset=\"u"
  },
  {
    "path": "src/tests/fixtures/greetings.ejs",
    "chars": 233,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Greeting</title>\n    <script src=\"/dist/turbo.es20"
  },
  {
    "path": "src/tests/fixtures/head_script.html",
    "chars": 477,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Head script</title>\n    <script src=\"/dist/turbo.e"
  },
  {
    "path": "src/tests/fixtures/headers.html",
    "chars": 201,
    "preview": "<html>\n\n<head>\n  <title>Headers</title>\n  <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track=\"reload\"></script>\n</"
  },
  {
    "path": "src/tests/fixtures/hot_preloading.html",
    "chars": 374,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Page That Links to Preloading Page</title>\n  <script sr"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch.html",
    "chars": 3536,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_custom_cache_time.html",
    "chars": 424,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_disabled.html",
    "chars": 367,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_iframe.html",
    "chars": 367,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch</title>\n    <script src=\"/dist"
  },
  {
    "path": "src/tests/fixtures/hover_to_prefetch_without_meta_tag_with_link_to_with_meta_tag.html",
    "chars": 523,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Hover to Prefetch Not Enabled</title>\n    <scrip"
  },
  {
    "path": "src/tests/fixtures/link_redirect.html",
    "chars": 287,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <script src=\"/dist/turbo.es2017-umd.js\"></script>\n    <meta data-turbo-track=\"reload"
  },
  {
    "path": "src/tests/fixtures/link_redirect_target.html",
    "chars": 153,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <script src=\"/dist/turbo.es2017-umd.js\"></script>\n    <meta data-turbo-track=\"reload"
  },
  {
    "path": "src/tests/fixtures/loading.html",
    "chars": 865,
    "preview": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turb"
  },
  {
    "path": "src/tests/fixtures/navigation.html",
    "chars": 8608,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end\">\n  <head>\n    <meta charse"
  },
  {
    "path": "src/tests/fixtures/noscript.css",
    "chars": 50,
    "preview": ":root {\n  --black-if-noscript-evaluated: black;\n}\n"
  },
  {
    "path": "src/tests/fixtures/one.html",
    "chars": 885,
    "preview": "<!DOCTYPE html>\n<html id=\"one\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>One</title>\n    <script src=\"/dist/turbo."
  },
  {
    "path": "src/tests/fixtures/page_refresh.html",
    "chars": 5260,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">"
  },
  {
    "path": "src/tests/fixtures/page_refresh_replace.html",
    "chars": 1447,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">"
  },
  {
    "path": "src/tests/fixtures/page_refresh_scroll_reset.html",
    "chars": 1336,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:submit-start turbo:submit-end turbo:fetch-request-error\">"
  },
  {
    "path": "src/tests/fixtures/page_refresh_stream_action.html",
    "chars": 614,
    "preview": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:submit-start turbo:submit-end\">\n<head>\n  <meta charset=\"utf-8\">\n  <"
  },
  {
    "path": "src/tests/fixtures/page_refreshed.html",
    "chars": 331,
    "preview": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turb"
  },
  {
    "path": "src/tests/fixtures/page_with_eager_frame.html",
    "chars": 615,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:before-render\">\n  <head>\n    <meta charset=\"utf-8\">\n    <"
  },
  {
    "path": "src/tests/fixtures/pausable_rendering.html",
    "chars": 900,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/pausable_requests.html",
    "chars": 680,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/permanent_children.html",
    "chars": 1104,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"turbo-refresh-method\" content=\"morph\">\n    <m"
  },
  {
    "path": "src/tests/fixtures/permanent_element.html",
    "chars": 791,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/prefetched.html",
    "chars": 242,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Prefetched Page</title>\n    <script src=\"/dist/t"
  },
  {
    "path": "src/tests/fixtures/preloaded.html",
    "chars": 308,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Preloaded Page</title>\n  <script src=\"/dist/turbo.es201"
  },
  {
    "path": "src/tests/fixtures/preloading.html",
    "chars": 784,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Preloading Page</title>\n  <script src=\"/dist/turbo.es20"
  },
  {
    "path": "src/tests/fixtures/remote_permanent_frame.html",
    "chars": 91,
    "preview": "<turbo-frame id=\"remote-permanent-frame\">\n  <h2>Loaded permanent frame</h2>\n</turbo-frame>\n"
  },
  {
    "path": "src/tests/fixtures/rendering.html",
    "chars": 5594,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Turbo</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/response.js",
    "chars": 73,
    "preview": "document.getElementById(\"frame\").innerHTML = \"Content from UJS response\"\n"
  },
  {
    "path": "src/tests/fixtures/root/index.html",
    "chars": 695,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track="
  },
  {
    "path": "src/tests/fixtures/root/page.html",
    "chars": 405,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <script src=\"/dist/turbo.es2017-umd.js\" data-turbo-track="
  },
  {
    "path": "src/tests/fixtures/scroll/one.html",
    "chars": 578,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Scroll: One</title>\n    <script src=\"/dist/turbo.e"
  },
  {
    "path": "src/tests/fixtures/scroll/two.html",
    "chars": 578,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Scroll: Two</title>\n    <script src=\"/dist/turbo.e"
  },
  {
    "path": "src/tests/fixtures/scroll_restoration.html",
    "chars": 507,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Scroll Restoration</title>\n    <script src=\"/dist/"
  },
  {
    "path": "src/tests/fixtures/stream.html",
    "chars": 1492,
    "preview": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:submit-start turbo:submit-end\">\n  <head>\n    <meta charset=\"utf-8\">"
  },
  {
    "path": "src/tests/fixtures/stylesheets/common.css",
    "chars": 83,
    "preview": "body {\n  background-color: rgb(0, 0, 128);\n  color: rgb(0, 128, 0);\n  margin: 0;\n}\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/left.css",
    "chars": 70,
    "preview": "body {\n  background-color: rgb(128, 0, 0);\n  color: rgb(128, 0, 0);\n}\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/left.html",
    "chars": 987,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Left</title>\n    <script src=\"/dist/turbo.es2017"
  },
  {
    "path": "src/tests/fixtures/stylesheets/right.css",
    "chars": 45,
    "preview": "body {\n  background-color: rgb(0, 128, 0);\n}\n"
  },
  {
    "path": "src/tests/fixtures/stylesheets/right.html",
    "chars": 684,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Right</title>\n    <script src=\"/dist/turbo.es201"
  },
  {
    "path": "src/tests/fixtures/tabs/three.html",
    "chars": 620,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/tabs/two.html",
    "chars": 618,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/tabs.html",
    "chars": 688,
    "preview": "<!DOCTYPE html>\n<html data-skip-event-details=\"turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">\n    <titl"
  },
  {
    "path": "src/tests/fixtures/target.html",
    "chars": 335,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Target</title>\n    <script src=\"/dist/turbo.es2017"
  },
  {
    "path": "src/tests/fixtures/test.css",
    "chars": 41,
    "preview": ":root {\n  --black-if-evaluated: black;\n}\n"
  },
  {
    "path": "src/tests/fixtures/test.js",
    "chars": 3596,
    "preview": "(function (eventNames) {\n  function serializeToChannel(object, visited = new Set()) {\n    const returned = {}\n\n    for ("
  },
  {
    "path": "src/tests/fixtures/tracked_asset_change.html",
    "chars": 310,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Tracked asset change</title>\n    <script src=\"/dis"
  },
  {
    "path": "src/tests/fixtures/tracked_nonce_change.html",
    "chars": 428,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Tracked nonce tag</title>\n    <script src=\"/dist/t"
  },
  {
    "path": "src/tests/fixtures/transitions/left.html",
    "chars": 799,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Left</title>\n    <meta name=\"turbo-view-transiti"
  },
  {
    "path": "src/tests/fixtures/transitions/left_legacy.html",
    "chars": 759,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\" />\n  <title>Left</title>\n  <meta name=\"view-transition\" content=\""
  },
  {
    "path": "src/tests/fixtures/transitions/other.html",
    "chars": 342,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Other</title>\n    <script src=\"/dist/turbo.es2017-"
  },
  {
    "path": "src/tests/fixtures/transitions/right.html",
    "chars": 708,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Right</title>\n    <meta name=\"turbo-view-transitio"
  },
  {
    "path": "src/tests/fixtures/transitions/right_legacy.html",
    "chars": 670,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <title>Right</title>\n  <meta name=\"view-transition\" content=\"s"
  },
  {
    "path": "src/tests/fixtures/two.html",
    "chars": 540,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Two</title>\n    <script src=\"/dist/turbo.es2017-um"
  },
  {
    "path": "src/tests/fixtures/ujs.html",
    "chars": 800,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Frame</title>\n    <script src=\"/src/tests/fixtures"
  },
  {
    "path": "src/tests/fixtures/umd.html",
    "chars": 337,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>UMD</title>\n    <script type=\"module\" data-turbo-t"
  },
  {
    "path": "src/tests/fixtures/visit.html",
    "chars": 1451,
    "preview": "<!DOCTYPE html>\n<html id=\"html\" data-skip-event-details=\"turbo:fetch-request-error\">\n  <head>\n    <meta charset=\"utf-8\">"
  },
  {
    "path": "src/tests/fixtures/visit_control_reload.html",
    "chars": 574,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Visit control: reload</title>\n    <script src=\"/di"
  },
  {
    "path": "src/tests/functional/async_script_tests.js",
    "chars": 609,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { readEventLogs, visitAction } from \"../helpers/page\"\n\ntest.befor"
  },
  {
    "path": "src/tests/functional/autofocus_tests.js",
    "chars": 3643,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { nextEventNamed, nextPageRefresh } from \"../helpers/page\"\n\ntest."
  },
  {
    "path": "src/tests/functional/cache_observer_tests.js",
    "chars": 761,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { nextEventNamed } from \"../helpers/page\"\n\ntest(\"removes temporar"
  },
  {
    "path": "src/tests/functional/drive_disabled_tests.js",
    "chars": 1952,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport {\n  getFromLocalStorage,\n  nextEventOnTarget,\n  setLocalStorageFr"
  },
  {
    "path": "src/tests/functional/drive_stylesheet_merging_tests.js",
    "chars": 1462,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { cssClassIsDefined, getComputedStyle } from \"../helpers/page\"\n\nt"
  },
  {
    "path": "src/tests/functional/drive_tests.js",
    "chars": 997,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { visitAction, withPathname } from \"../helpers/page\"\n\nconst path "
  },
  {
    "path": "src/tests/functional/drive_view_transition_legacy_tests.js",
    "chars": 830,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { nextBody } from \"../helpers/page\"\n\ntest.beforeEach(async ({ pag"
  },
  {
    "path": "src/tests/functional/drive_view_transition_tests.js",
    "chars": 1219,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { nextBody } from \"../helpers/page\"\n\ntest.beforeEach(async ({ pag"
  },
  {
    "path": "src/tests/functional/form_mode_tests.js",
    "chars": 2741,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { getFromLocalStorage, setLocalStorageFromEvent } from \"../helper"
  },
  {
    "path": "src/tests/functional/form_submission_tests.js",
    "chars": 51117,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport {\n  getFromLocalStorage,\n  getSearchParam,\n  hasSelector,\n  isScr"
  },
  {
    "path": "src/tests/functional/frame_navigation_tests.js",
    "chars": 4998,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { getFromLocalStorage, nextBeat, nextEventNamed, nextEventOnTarge"
  },
  {
    "path": "src/tests/functional/frame_tests.js",
    "chars": 45616,
    "preview": "import { test, expect } from \"@playwright/test\"\nimport {\n  attributeForSelector,\n  listenForEventOnTarget,\n  nextAttribu"
  },
  {
    "path": "src/tests/functional/import_tests.js",
    "chars": 1676,
    "preview": "import { expect, test } from \"@playwright/test\"\n\ntest(\"window variable with ESM\", async ({ page }) => {\n  await page.got"
  },
  {
    "path": "src/tests/functional/link_prefetch_observer_tests.js",
    "chars": 12312,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport { nextBeat, nextEventOnTarget, noNextEventNamed, noNextEventOnTar"
  },
  {
    "path": "src/tests/functional/loading_tests.js",
    "chars": 6789,
    "preview": "import { expect, test } from \"@playwright/test\"\nimport {\n  attributeForSelector,\n  nextAttributeMutationNamed,\n  nextBea"
  }
]

// ... and 23 more files (download for full content)

About this extraction

This page contains the full source code of the hotwired/turbo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 223 files (547.0 KB), approximately 145.0k tokens, and a symbol index with 862 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!