[
  {
    "path": ".eslintignore",
    "content": "dist/\nscripts/\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at conduct@quilljs.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\nThe best way to contribute is to help others in the Quill community. This includes:\n\n- Reporting new [bugs](https://github.com/slab/quill/labels/bug) or adding details to existing ones\n- Reproducing [unconfirmed bugs](https://github.com/slab/quill/labels/needs%20reproduction)\n- Quick typo fix or documentation improvement [Pull Requests](#pull-requests)\n- Participating in [discussions](https://github.com/slab/quill/discussions)\n\nAfter becoming familiar with Quill and the codebase, likely through using Quill yourself and making some of the above contributions, you may choose to take on a bigger commitment by:\n\n- Helping fix [bugs](https://github.com/slab/quill/labels/bug)\n- Implementing new [features](https://github.com/slab/quill/labels/feature)\n- Publishing guides, tutorials, and examples\n- Supporting Quill in other ecosystems (Angular, React, etc)\n\nNote that if you are going to be making significant contributions, you should first open\na [discussion](https://github.com/slab/quill/discussions) to ensure your work aligns with the project's goals and direction.\n\n## Questions\n\nIf you have a question, it is best to ask on [Discussions](https://github.com/slab/quill/discussions) under the Q&A category.\n\n## Bug Reports\n\nSearch through [Github Issues](https://github.com/slab/quill/issues) to see if the bug has already been reported. If so, please comment with any additional information.\n\nNew bug reports must include:\n\n1. Detailed description of faulty behavior\n2. Steps for reproduction or failing test case\n3. Expected and actual behaviors\n4. Platforms (OS **and** browser combination) affected\n5. Version of Quill\n\nLacking reports it may be autoclosed with a link to these instructions.\n\n## Feature Requests\n\nSearch through [Github Issues](https://github.com/slab/quill/labels/feature) to see if someone has already suggested the feature. If so, please provide support with a [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) and add your own use case.\n\nTo open a new feature request, please include:\n\n1. A detailed description of the feature\n2. Why this feature belongs in Quill core, instead of your own application logic\n3. Background of where and how you are using Quill\n4. The use case that would be enabled or improved for your product, if the feature was implemented\n\nFeatures are prioritized based on real world users and use cases, not theoretically useful additions for other unknown users. Lacking feature requests may be autoclosed with a link to this section.\n\nThe more complete and compelling the request, the more likely it will ultimately be implemented. Garnering community support will help as well!\n\n## Pull Requests\n\nPlease check to make sure your plans fall within Quill's scope. This often means opening up a [discussion](https://github.com/slab/quill/labels/discussion).\n\nNon-code Pull Requests such as typo fixes or documentation improvements are highly encouraged and are often accepted immediately.\n\nPull Requests modifying public facing interfaces or APIs, including backwards compatible additions, will undergo the most scrutiny, and will almost certainly require a proper discussion of the motivation and merits beforehand. Simply increasing code complexity is a cost not to be taken lightly.\n\nPull requests must:\n\n1. Be forked off the [main](https://github.com/slab/quill/tree/main) branch.\n2. Pass the linter and conform to existing coding styles.\n3. Commits are [squashed](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Squashing-Commits) to minimally coherent units of changes.\n4. Are accompanied by tests covering the new feature or demonstrating the bug for fixes.\n5. Serve a single atomic purpose (add one feature or fix one bug).\n6. Introduce only changes that further the PR's singular purpose (ex. do not tweak an unrelated config along with adding your feature).\n7. Not break any existing unit or end to end tests.\n\n**Important:** By issuing a Pull Request you agree to allow the project owners to license your work under the terms of the [License](https://github.com/slab/quill/blob/master/LICENSE).\n"
  },
  {
    "path": ".github/DEVELOPMENT.md",
    "content": "# Development\n\nThis repo is a monorepo powered by npm's official [workspace feature](https://docs.npmjs.com/cli/v10/using-npm/workspaces). It contains the following packages:\n\n### quill\n\nThis is the Quill library. It's written in [TypeScript](https://www.typescriptlang.org/), and use [Webpack](https://webpack.js.org/) as the bundler.\nIt uses [Vitest](https://vitest.dev) for unit testing, and [Playwright](https://playwright.dev/) for E2E testing.\n\n### website\n\nIt's Quill's website (hosted at [quilljs.com](https://quilljs.com/)). It's built with [Next.js](https://nextjs.org/).\n\n## Setup\n\nTo prepare your local environment for development, ensure you have Node.js installed. The repo uses npm, and doesn't support Yarn and pnpm.\n\nInstall the necessary dependencies with the command below:\n\n```shell\nnpm install\n```\n\nStart the development environment using:\n\n```shell\nnpm start\n```\n\nThis command starts two services:\n\n- Quill's webpack dev server\n- Website's Next.js dev server\n\nThese servers dynamically build and serve the latest copy of the source code.\n\nAccess the running website at [localhost:9000](http://localhost:9000/). By default, the website will use your local Quill build, that includes all the examples in the website. This convenient setup allows for seamless development and ensures changes to Quill do not disrupt the website's content.\n\nIf you need to modify only the website's code, start the website with `npm start -w website``. This makes the website use the latest CDN version.\n\n## Testing\n\nTo run the unit tests in watch mode, run:\n\n    npm run test:unit -w quill\n\nTo execute the E2E tests, run:\n\n    npm run test:e2e -w quill\n\n## Workflow\n\nA standard development workflow involves:\n\n1. `npm start` - to run development services\n2. [localhost:9000/standalone/snow](http://localhost:9000/standalone/snow) - to interactively develop and test an isolated example\n3. `npm run test:unit -w quill` - to run unit tests\n4. If everything is working, run the E2E tests\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "Please describe the a concise description and fill out the details below. It will help others efficiently understand your request and get to an answer instead of repeated back and forth. Providing a [minimal, complete and verifiable example](https://stackoverflow.com/help/mcve) will further increase your chances that someone can help.\n\n**Steps for Reproduction**\n\n1. Visit [quilljs.com, jsfiddle.net, codepen.io]\n2. Step Two\n3. Step Three\n\n**Expected behavior**:\n\n**Actual behavior**:\n\n**Platforms**:\n\nInclude browser, operating system and respective versions\n\n**Version**:\n\nRun `Quill.version` to find out\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  exclude:\n    authors:\n      - quill-bot\n  categories:\n    - title: Bug Fixes 🛠\n      labels:\n        - change:bugfix\n    - title: New Features 🎉\n      labels:\n        - change:feature\n    - title: Documentation 📚\n      labels:\n        - change:documentation\n    - title: Other Changes\n      labels:\n        - \"*\"\n"
  },
  {
    "path": ".github/workflows/_test.yml",
    "content": "name: Tests\non:\n  workflow_call:\njobs:\n  e2e:\n    name: E2E Tests\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 20\n      - name: Install dependencies\n        run: npm ci\n      - name: Install Playwright Browsers\n        run: npx playwright install --with-deps\n        working-directory: packages/quill\n      - name: Run Playwright tests\n        uses: coactions/setup-xvfb@v1\n        with:\n          run: npm run test:e2e -- --headed\n          working-directory: packages/quill\n  fuzz:\n    name: Fuzz Tests\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Git checkout\n        uses: actions/checkout@v4\n\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - run: npm ci\n        env:\n          PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1\n      - run: npm run test:fuzz -w quill\n  unit:\n    name: Unit Tests\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        browser: [chromium, webkit, firefox]\n\n    steps:\n      - name: Git checkout\n        uses: actions/checkout@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - run: npm ci\n      - run: npx playwright install --with-deps\n      - run: npm run lint\n      - run: npm run test:unit -w quill || npm run test:unit -w quill || npm run test:unit -w quill\n        env:\n          BROWSER: ${{ matrix.browser }}\n"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: Generate Changelog\n\non:\n  release:\n    types: [published, created]\n  workflow_dispatch: {}\n\njobs:\n  changelog:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Git checkout\n        uses: actions/checkout@v4\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - run: npm ci\n      - run: node ./scripts/changelog.mjs\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/label.yml",
    "content": "name: Pull Requests\n\non:\n  pull_request:\n    types: [opened, labeled, unlabeled, synchronize]\n\njobs:\n  label:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n    steps:\n      - uses: mheap/github-action-required-labels@v5\n        with:\n          mode: exactly\n          count: 1\n          labels: |\n            change:bugfix\n            change:feature\n            change:documentation\n            change:chore\n            change:refactor\n          add_comment: false\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Main\n\non:\n  push:\n    branches: [main]\n\nconcurrency:\n  group: main-build\n  cancel-in-progress: true\n\njobs:\n  test:\n    uses: ./.github/workflows/_test.yml\n"
  },
  {
    "path": ".github/workflows/pull-request.yml",
    "content": "name: Pull Requests\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    uses: ./.github/workflows/_test.yml\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'npm version. Examples: \"2.0.0\", \"2.0.0-beta.0\". To deploy an experimental version, type \"experimental\".'\n        default: \"experimental\"\n        required: true\n      dry-run:\n        description: \"Only create a tarball, do not publish to npm or create a release on GitHub.\"\n        type: boolean\n        default: true\n        required: true\n\npermissions:\n  contents: write\n\njobs:\n  test:\n    uses: ./.github/workflows/_test.yml\n\n  release:\n    runs-on: ubuntu-latest\n    needs: test\n\n    steps:\n      - name: Git checkout\n        uses: actions/checkout@v4\n\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - run: npm ci\n      - run: ./scripts/release.js --version ${{ github.event.inputs.version }} ${{ github.event.inputs.dry-run == 'true' && '--dry-run' || '' }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      - name: Archive npm package tarball\n        uses: actions/upload-artifact@v4\n        with:\n          name: npm\n          path: |\n            packages/quill/dist/*.tgz\n"
  },
  {
    "path": ".gitignore",
    "content": ".*\n!.eslintrc.json\n!.eslintignore\n!.npmignore\n!.gitignore\n!.github\n\nnode_modules\n\ntest-results/\nplaywright-report/\n"
  },
  {
    "path": ".npmignore",
    "content": "*.ts\n!*.d.ts\n.*\n.github\n.vscode\ndocs\ntest\ntsconfig.json\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# v2.0.2 (2024-05-13)\n\n<!-- Release notes generated using configuration in .github/release.yml at v2.0.2 -->\n\n## What's Changed\n\n### Bug Fixes 🛠\n\n- Fix typing errors for Quill.register by [@hzgotb](https://github.com/hzgotb) in https://github.com/slab/quill/pull/4127\n- Fix event source when deleting a link with shortcuts by [@luin](https://github.com/luin) in https://github.com/slab/quill/pull/4200\n- Avoid side effects for Enter/Backspace when composing in Safari by [@luin](https://github.com/luin) in https://github.com/slab/quill/pull/4201\n- Ignore pasting images when image format is disallowed by [@luin](https://github.com/luin) in https://github.com/slab/quill/pull/4202\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.2)\n\n# v2.0.1 (2024-05-01)\n\n## Bug Fixes\n\n- Prevent overriding of theme's default toolbar settings mistakenly [#4120](https://github.com/slab/quill/pull/4120)\n- Improve typings for methods that return a Delta [#4136](https://github.com/slab/quill/pull/4136)\n- Fix toolbar icons for h3-h6 [#4131](https://github.com/slab/quill/pull/4131)\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.1)\n\n# v2.0.0 (2024-04-17)\n\nWe are thrilled to announce the release of Quill 2.0! Please check out the [announcement post](https://slab.com/blog/announcing-quill-2-0/).\n\n## Major Improvements\n\n- Quill is now a valid ESM package for better ecosystem (e.g. bundlers) and tree-shaking support\n- Nested Quill support [#3590](https://github.com/slab/quill/pull/3590)\n- Improved IME and spell corrector support [#3807](https://github.com/slab/quill/pull/3807)\n- Semantic cleanups for TEXT_CHANGE event [#3778](https://github.com/slab/quill/pull/3778)\n- **History**: Record selection in history module [#3823](https://github.com/slab/quill/pull/3823)\n- Auto detect scrolling container [#3840](https://github.com/slab/quill/pull/3840)\n- **Clipboard**: Improve support for pasting from Google Docs and Microsoft Word\n\n## Performance Improvements\n\nQuill 2.0 includes many performance optimizations, the most important of which is the improved rendering speed for large content.\n\n- Improve inserting performance [#3815](https://github.com/slab/quill/pull/3815)\n- Avoid fetching selections when possible [#3538](https://github.com/slab/quill/pull/3538)\n- No need to setContents when container is empty [#3539](https://github.com/slab/quill/pull/3539)\n\n## Code Modernization\n\n- Migrated to TypeScript\n- Provided official TypeScript declarations\n- Migrated to Vitest for unit testing\n- Migrated to Playwright for E2E testing\n- Migrated website to Gatsby\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0)\n\n# v2.0.0-rc.5 (2024-04-04)\n\n- **Clipboard** Add support for Quill v1 list attributes\n- Fix overload declarations for `quill.formatText()` and other methods\n- Expose Bounds type for getBounds()\n- Expose Range type\n- Allow ref for insertBefore to be null\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.5)\n\n# v2.0.0-rc.4 (2024-03-24)\n\n- Include source maps for Parchment\n- **Clipboard** Support pasting links copied from iOS share sheets\n- Fix config parsing where undefined values were kept\n- Expose types for Quill options\n- Remove empty .css.js files generated by bundlers\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.4)\n\n# v2.0.0-rc.3 (2024-03-16)\n\n- Fix `Quill#getSemanticHTML()` for list items\n- Remove unnecessary Firefox workaround\n- **Clipboard** Fix redundant newlines when pasting from external sources\n- Add `formats` option for specifying allowed formats\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.3)\n\n# v2.0.0-rc.2 (2024-02-15)\n\n- Fix toolbar button state not updated in some cases\n- Narrower `BubbleTheme.tooltip` type\n- Fix `Selection#getBounds()` when starting range at end of text node\n- Improve compatibility with esbuild\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.2)\n\n# v2.0.0-rc.1 (2024-02-12)\n\n- Remove unnecessary lodash usages.\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.1)\n\n# v2.0.0-rc.0 (2024-02-03)\n\n- **Clipboard** Convert newlines between inline elements to a space.\n- **Clipboard** Avoid generating unsupported formats on paste.\n- **Clipboard** Improve support for pasting from Google Docs and Microsoft Word.\n- **Clipboard** Ignore whitespace between pasted empty paragraphs.\n- **Syntax** Support highlight.js v10 and v11.\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.0)\n\n# v2.0.0-beta.2 (2024-01-30)\n\n- Fix IME not working correctly in Safari.\n- **Clipboard** Support paste as plain text.\n- Fix `Quill.getText()` not respecting `length` parameter.\n- **History** Fix redo shortcut not working on Linux and Windows.\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-beta.2)\n\n# v2.0.0-beta.1 (2024-01-21)\n\n- Fix syntax label from \"Javascript\" to \"JavaScript\".\n- Fix typing errors for emitter.\n- Inline SVG images for easier bundler setup.\n- Improve typing for Registry.\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-beta.1)\n\n# v2.0.0-beta.0 (2023-12-08)\n\nIn the upcoming 2.0 release, Quill has been significantly modernized. Leveraging the latest browser-supported APIs, Quill now delivers a more efficient and reliable editing experience.\n\n## Major Improvements\n\n- Nested Quill support [#3590](https://github.com/slab/quill/pull/3590)\n- Improved IME and spell corrector support [#3807](https://github.com/slab/quill/pull/3807)\n- Semantic cleanups for TEXT_CHANGE event [#3778](https://github.com/slab/quill/pull/3778)\n- **History**: Record selection in history module [#3823](https://github.com/slab/quill/pull/3823)\n- Auto detect scrolling container [#3840](https://github.com/slab/quill/pull/3840)\n\n## Performance Improvements\n\nQuill 2.0 includes many performance optimizations, the most important of which is the improved rendering speed for large content.\n\n- Improve inserting performance [#3815](https://github.com/slab/quill/pull/3815)\n- Avoid fetching selections when possible [#3538](https://github.com/slab/quill/pull/3538)\n- No need to setContents when container is empty [#3539](https://github.com/slab/quill/pull/3539)\n\n## Code Modernization\n\n- Migrated to TypeScript\n- Provided official TypeScript declarations\n- Migrated to Vitest for unit testing\n- Migrated to Playwright for E2E testing\n- Migrated website to Gatsby\n\n[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-beta.0)\n\n# v1.3.7 (2019-09-09)\n\nSecurity related bug fixes.\n\n- https://app.snyk.io/vuln/npm:extend:20180424\n- https://www.npmjs.com/advisories/1039\n\nThank you [@danfuzz](https://github.com/danfuzz), [@danielw93](https://github.com/danielw93), [@jonathanlloyd](https://github.com/jonathanlloyd), and [@k-sai-kiranmayee](https://github.com/k-sai-kiranmayee) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.7)\n\n# v1.3.6 (2018-03-12)\n\n- Make picker accessible [#1999](https://github.com/slab/quill/pull/1999)\n- Fix Japanese composition in Chrome 65 [#2009](https://github.com/slab/quill/issues/2009)\n\nThanks to [@berylw](https://github.com/berylw) and [@erinsinger93](https://github.com/erinsinger93) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.6)\n\n# v1.3.5 (2018-01-22)\n\n- Fix indent preservation of a checked checklist item [#1818](https://github.com/slab/quill/issues/1818)\n  - added as a shortcut to trigger bullet list formatting [#1819](https://github.com/slab/quill/pull/1819)\n- Fix pasting text-align styles [#1873](https://github.com/slab/quill/issues/1873)\n- Fix cursor position after dangerouslyPasteHTML [#1886](https://github.com/slab/quill/issues/1886)\n- Fix value of history stack in text-change handler [#1906](https://github.com/slab/quill/pull/1906)\n- Workaround for Webkit locking up when navigating around images using hotkeys [#1910](https://github.com/slab/quill/issues/1910)\n\nThank you [@araruna](https://github.com/araruna), [@bryanrsmith](https://github.com/bryanrsmith), [@haugstrup](https://github.com/haugstrup), [@icylace](https://github.com/icylace), [@leimig](https://github.com/leimig), [@LFDM](https://github.com/LFDM), [@nikparo](https://github.com/nikparo), [@rafpaf](https://github.com/rafpaf) and [@vk2sky](https://github.com/vk2sky) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.5)\n\n# v1.3.4 (2017-11-06)\n\n- Loosen dependency specification [#1748](https://github.com/slab/quill/issues/1748)\n- Loosen list autofill constraint [#1749](https://github.com/slab/quill/issues/1749)\n\nThanks to [@danfuzz](https://github.com/danfuzz) and [@SoftVision-CarmenFat](https://github.com/SoftVision-CarmenFat) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.4)\n\n# v1.3.3 (2017-10-09)\n\n- Fix `getFormat` with no parameters while editor is not focused [#1548](https://github.com/slab/quill/issues/1548)\n- Remove automatic highlighting across embeds [#1691](https://github.com/slab/quill/issues/1691)\n- Support checking checklist on mobile [#1693](https://github.com/slab/quill/pull/1711)\n- Fix list creation keyboard shortcuts [#1723](https://github.com/slab/quill/issues/1723)\n- Show KaTex rendering errors [#1738](https://github.com/slab/quill/pull/1738)\n\nThank you [@altschuler](https://github.com/altschuler), [@arrocke](https://github.com/arrocke), [@guillaumepotier](https://github.com/guillaumepotier), [@sferoze](https://github.com/sferoze) and [@volser](https://github.com/volser) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.3)\n\n# v1.3.2 (2017-09-04)\n\n- Pasting into code block should always paste as code [#1624](https://github.com/slab/quill/issues/1624)\n- Fix removing embed selection when arrow keys change selection [#1633](https://github.com/slab/quill/issues/1633)\n- Fix selection restoration after image insertion [#1649](https://github.com/slab/quill/issues/1649)\n- Fix selection-change firing after dragging off screen [#1654](https://github.com/slab/quill/issues/1654)\n- Fix placeholder text spacing [#1677](https://github.com/slab/quill/issues/1677)\n\nThanks to [@abramz](https://github.com/abramz), [@amitm02](https://github.com/amitm02), [@eamodio](https://github.com/eamodio), [@HWliao](https://github.com/HWliao), [@mmitis](https://github.com/mmitis), [@nelsonpecora](https://github.com/nelsonpecora), [@nipunjain87](https://github.com/nipunjain87), and [@ValueBerry](https://github.com/ValueBerry) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.2)\n\n# v1.3.1 (2017-08-07)\n\n- Fix placeholder when emptying text [#1594](https://github.com/slab/quill/issues/1594)\n- Fix inserting newline after header [#1616](https://github.com/slab/quill/issues/1616)\n\nThank you [@Natim](https://github.com/Natim) and [@stephenLYao](https://github.com/stephenLYao) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.1)\n\n# v1.3.0 (2017-07-17)\n\nAdd `matchVisual` [configuration](https://quilljs.com/docs/modules/clipboard/#configuration) to Clipboard.\n\n- Use DOM API to determine selected `<select>` option [#1576](https://github.com/slab/quill/pull/1576)\n- Add `:focus` styles to toolbar [#1540](https://github.com/slab/quill/issues/1540)\n- Allow users to undo automatic keyboard completions [#1538](https://github.com/slab/quill/issues/1538)\n- Use github-pages gem to make development environment consistent [#1536](https://github.com/slab/quill/issues/1536) [#1544](https://github.com/slab/quill/pull/1544)\n- Fix composing Chinese with preformatting [#1514](https://github.com/slab/quill/issues/1514)\n- Fix example clipboard module in docs [#1502](https://github.com/slab/quill/issues/1502)\n- Fix list layout in RTL mode [#1498](https://github.com/slab/quill/issues/1498)\n- Clarify documentation for scrollingContainer [#1496](https://github.com/slab/quill/issues/1496)\n- Add `tel` to default link whitelist [#1436](https://github.com/slab/quill/pull/1436)\n- Fix cursor interaction with custom contenteditable=false embeds [#1172](https://github.com/slab/quill/issues/1172) [#1181](https://github.com/slab/quill/issues/1181)\n- Fix rendered cursor in Chrome when interacting with scrollbar [#1114](https://github.com/slab/quill/issues/1114)\n\nThanks to [@alexkrolick](https://github.com/alexkrolick), [@amitm02](https://github.com/amitm02), [@Christilut](https://github.com/Christilut), [@danielschwartz](https://github.com/danielschwartz), [@emanuelbsilva](https://github.com/emanuelbsilva), [@ersommer](https://github.com/ersommer), [@fiurrr](https://github.com/fiurrr), [@jackmu95](https://github.com/jackmu95), [@jmzhang](https://github.com/jmzhang), [@mdpye](https://github.com/mdpye), [@ralrom](https://github.com/ralrom), [@sferoze](https://github.com/sferoze), [@simon-at-fugu](https://github.com/simon-at-fugu), and [@yogadzx](https://github.com/yogadzx) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.3.0)\n\n# v1.2.6 (2017-06-05)\n\n- Disable Grammarly by default [#574](https://github.com/slab/quill/issues/574)\n- Fix RTL list spacing [#1485](https://github.com/slab/quill/pull/1485)\n- Add support for mobile Youtube links [#1489](https://github.com/slab/quill/pull/1489)\n\nThank you [@amitm02](https://github.com/amitm02), [@benbro](https://github.com/benbro)\n[@nickbaum](https://github.com/nickbaum), [@stalniy](https://github.com/stalniy) and [@ygrishajev](https://github.com/ygrishajev) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.6)\n\n# v1.2.5 (2017-05-29)\n\n- Fix cursor shifting to be exclusive of user cursor [#1367](https://github.com/slab/quill/issues/1367)\n- Fix iOS hover state on toolbar [#1388](https://github.com/slab/quill/issues/1388)\n- Fix `getText()` for Bangla [#1427](https://github.com/slab/quill/issues/1427)\n- Fix Korean character composition in Safari [#1437](https://github.com/slab/quill/issues/1437)\n- Fix pasting HTML handling special class names [#1445](https://github.com/slab/quill/issues/1445)\n- Fix paste or initializing with font-weight [#1456](https://github.com/slab/quill/issues/1456)\n- Fix updating active picker logic [#1468](https://github.com/slab/quill/issues/1468)\n\nThanks to [@aliciawood](https://github.com/aliciawood), [@benbro](https://github.com/benbro), [@denis-aes](https://github.com/denis-aes), [@despreju](https://github.com/despreju), [@GlenKPeterson](https://github.com/GlenKPeterson), [@haugstrup](https://github.com/haugstrup), [@jziggas](https://github.com/jziggas), [@RobAley](https://github.com/RobAley), [@sheley1998](https://github.com/sheley1998), [@silverprize](https://github.com/silverprize), and [@yairy](https://github.com/yairy) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.5)\n\n# v1.2.4 (2017-04-18)\n\n- Fix pasting nested list [#906](https://github.com/slab/quill/issues/906)\n- Fix delete key interaction at end of list [#1277](https://github.com/slab/quill/issues/1277)\n- Fix pasting whitespace prefix [#1244](https://github.com/slab/quill/issues/1244)\n- Fix file dialog open speed [#1265](https://github.com/slab/quill/issues/1265)\n- Fix backspace with at beginning of list interaction with meta keys [#1307](https://github.com/slab/quill/issues/1307)\n- Fix pasting nested styles [#1333](https://github.com/slab/quill/issues/1333)\n- Fix backspacing into an empty line should keep own formats [#1339](https://github.com/slab/quill/issues/1339)\n- Fix IE11 autolinking interaction [#1390](https://github.com/slab/quill/issues/1390)\n- Fix persistent focus interaction with tabbing away [#1404](https://github.com/slab/quill/issues/1404)\n\nThanks to [@bigggge](https://github.com/bigggge), [@CoenWarmer](https://github.com/CoenWarmer), [@cutteroid](https://github.com/cutteroid), [@jay-cox](https://github.com/jay-cox), [@kiewic](https://github.com/kiewic), [@kloots](https://github.com/kloots), [@MichaelTontchev](https://github.com/MichaelTontchev), [@montlebalm](https://github.com/montlebalm), [@RichardNeill](https://github.com/RichardNeill), and [@vasconita](https://github.com/vasconita) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.4)\n\n# v1.2.3 (2017-03-29)\n\n- Fix scrolling when appending new lines [#1276](https://github.com/slab/quill/issues/1276) [#1361](https://github.com/slab/quill/issues/1361)\n- Fix binding to explicit shortcut key [#1365](https://github.com/slab/quill/issues/1365)\n- Merge clone update [#1359](https://github.com/slab/quill/pull/1359)\n\nThank you [@artaommahe](https://github.com/artaommahe), [@c-w](https://github.com/c-w), [@EladBet](https://github.com/EladBet), [@emenoh](https://github.com/emenoh), and [@montlebalm](https://github.com/montlebalm) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.3)\n\n# v1.2.2 (2017-02-27)\n\n- Fix backspace/delete on Windows/Ubuntu [#1334](https://github.com/slab/quill/issues/1334)\n\nThanks to [@dinusuresh](https://github.com/dinusuresh) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.2)\n\n# v1.2.1 (2017-02-27)\n\n- Fix link removal on Snow theme [#1259](https://github.com/slab/quill/issues/1259)\n- Fix CMD+backspace on empty editor [#1319](https://github.com/slab/quill/issues/1319)\n- Fix disabled checklist behavior [#1312](https://github.com/slab/quill/issues/1312)\n\nThank you [@danielschwartz](https://github.com/@danielschwartz), [@JedWatson](https://github.com/@JedWatson), [@montlebalm](https://github.com/@montlebalm), and [@simi](https://github.com/@simi) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.1)\n\n# v1.2.0 (2017-01-21)\n\nAdd concept of experimental APIs: they are APIs meant to try out support for use cases we would like to address, but gives flexibility to find the right API interface. As such they are not covered by Semantic Versioning. Several are added to start things off: `find`, `getIndex`, `getLeaf`, `getLine`, `getLines`.\n\n- Merge disabling list keyboard shortcut when list format is disabled [#1257](https://github.com/slab/quill/pull/1257)\n\nThanks to [@haugstrup](https://github.com/haugstrup) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.2.0)\n\n# v1.1.10 (2017-01-16)\n\n- Preserve user selection on API changes [#1152](https://github.com/slab/quill/issues/1152)\n- Fix backspacing into emojis [#1230](https://github.com/slab/quill/issues/1230)\n- Fix ability to type after emptying line in IE/Firefox [#1254](https://github.com/slab/quill/issues/1254)\n- Fix whitelisting block formats [#1256](https://github.com/slab/quill/issues/1256)\n\nThank you [@benbro](https://github.com/benbro), [@haugstrup](https://github.com/haugstrup), [@peterweck](https://github.com/peterweck) and [@sbevels](https://github.com/sbevels) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.10)\n\n# v1.1.9 (2017-01-02)\n\n- Support pasting italics from Google Docs [#1185](https://github.com/slab/quill/issues/1185)\n- Fix setting dropdown picker back to default [#1191](https://github.com/slab/quill/issues/1191)\n- Fix code-block formatting on empty first line in Firefox [#1195](https://github.com/slab/quill/issues/1195)\n- Prevent formatting via keyboard shortcuts when not whitelisted [#1197](https://github.com/slab/quill/issues/1197)\n- Fix select-all copy and overwrite paste in Firefox [#1202](https://github.com/slab/quill/issues/1202)\n\nThank you [@adfaure](https://github.com/adfaure), [@berndschimmer](https://github.com/berndschimmer), [@CoenWarmer](https://github.com/CoenWarmer), [@montlebalm](https://github.com/montlebalm), and [@TraceyYau](https://github.com/TraceyYau) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.9)\n\n# v1.1.8 (2016-12-23)\n\n- Support pasting italics from Google Docs [#1185](https://github.com/slab/quill/issues/1185)\n- Fix setting dropdown picker back to default [#1191](https://github.com/slab/quill/issues/1191)\n- Fix code-block formatting on empty first line in Firefox [#1195](https://github.com/slab/quill/issues/1195)\n- Prevent formatting via keyboard shortcuts when not whitelisted [#1197](https://github.com/slab/quill/issues/1197)\n- Fix select-all copy and overwrite paste in Firefox [#1202](https://github.com/slab/quill/issues/1202)\n\nThank you [@adfaure](https://github.com/adfaure), [@berndschimmer](https://github.com/berndschimmer), [@CoenWarmer](https://github.com/CoenWarmer), [@montlebalm](https://github.com/montlebalm), and [@TraceyYau](https://github.com/TraceyYau) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.8)\n\n# v1.1.7 (2016-12-13)\n\n- Fix dropdown values reverting to default [#901](https://github.com/slab/quill/issues/901)\n- Add config to prevent scroll jumping on paste [#1082](https://github.com/slab/quill/issues/1082)\n- Prevent scrolling on API source calls [#1152](https://github.com/slab/quill/issues/1152)\n- Fix tsconfig build error [#1165](https://github.com/slab/quill/issues/1165)\n- Fix delete and formatting interaction in Firefox [#1171](https://github.com/slab/quill/issues/1171)\n- Fix cursor jump on formatting in middle of text [#1176](https://github.com/slab/quill/issues/1176)\n\nThanks to [@cutteroid](https://github.com/cutteroid), [@houxg](https://github.com/houxg), [@jasongisstl](https://github.com/jasongisstl), [@nikparo](https://github.com/nikparo), [@sbevels](https://github.com/sbevels), and [sferoze](https://github.com/sferoze) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.7)\n\n# v1.1.6 (2016-12-06)\n\n### Features\n\nChecklists [#759](https://github.com/slab/quill/issues/759) support has been added to the API. UI and relevant interactions are still forthcoming.\n\n### Bug Fixes\n\n- Fix bug that allowed edits in readOnly mode [#1151](https://github.com/slab/quill/issues/1151)\n- Fix max call stack bug on large paste [#1123](https://github.com/slab/quill/issues/1123)\n\nThank you [@jgmediadesign](https://github.com/jgmediadesign) and [@julienbmobile](https://github.com/julienbmobile) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.6)\n\n# v1.1.5 (2016-11-07)\n\n- Remove unnecessary type attribute in documentation [#1087](https://github.com/slab/quill/pull/1087)\n- Fix chrome 52+ input file label open slow [#1090](https://github.com/slab/quill/pull/1090)\n- Only query the last op's insertion string if it's actually an insert [#1095](https://github.com/slab/quill/pull/1095)\n\nThank you [@jleen](https://github.com/jleen), [@kaelig](https://github.com/kaelig), and [@YouHan26](https://github.com/YouHan26) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.5)\n\n# v1.1.3 (2016-10-24)\n\n- Update quill-delta [delta#2](https://github.com/quilljs/delta/issues/2)\n- Fix link creation [#1073](https://github.com/slab/quill/issues/1073)\n\nThanks to [@eamodio](https://github.com/eamodio) and [@metsavir](https://github.com/metsavir) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.3)\n\n# v1.1.2 (2016-10-24)\n\n- Fix setContents on already formatted text [#1065](https://github.com/slab/quill/issues/1065)\n- Fix regression [#1067](https://github.com/slab/quill/issues/1067)\n- Improve documentation [#1069](https://github.com/slab/quill/pull/1069) [#1070](https://github.com/slab/quill/pull/1070)\n\nThank you [benbro](https://github.com/benbro), [derickruiz](https://github.com/derickruiz), [eamodio](https://github.com/eamodio), [hallaathrad](https://github.com/hallaathrad), and [philly385](https://github.com/philly385) for your contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.2)\n\n# v1.1.1 (2016-10-21)\n\n### Bug fixes\n\n- TEXT_CHANGE event now use cursor position to inform change location [#746](https://github.com/slab/quill/issues/746)\n- Fix inconsistent cursor reporting between browsers [#1007](https://github.com/slab/quill/issues/1007)\n- Fix tooltip overflow in docs [#1060](https://github.com/slab/quill/issues/1060)\n- Fix naming [#1063](https://github.com/slab/quill/pull/1063)\n- Fix Medium example [#1064](https://github.com/slab/quill/issues/1064)\n\nThanks to [@artaommahe](https://github.com/artaommahe), [@benbro](https://github.com/benbro), [@fuffalist](https://github.com/fuffalist), [@sachinrekhi](https://github.com/sachinrekhi), [@sergop321](https://github.com/sergop321), and [@tlg](https://github.com/tlg) for contributions to this release!\n\nSpecial thanks to [@DadaMonad](https://github.com/DadaMonad) for contributions on [fast-diff](https://github.com/jhchen/fast-diff) that enabled the [#746](https://github.com/slab/quill/issues/746) fix.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.1)\n\n# v1.1.0 (2016-10-17)\n\n### Additions\n\nQuill has always allowed API calls, even when the editor is in readOnly mode. All API calls also took a `source` parameter to indicate the origin of the change. For example, a click handler in the toolbar would call `formatText` with `source` set to `\"user\"`. When the editor is in readOnly mode, it would make sense for user initiated actions to be ignored. For example the user cannot focus or type into the editor. However because API calls are allowed, the user could still modify the editor contents [#909](https://github.com/slab/quill/issues/909). The natural fix is to ignore user initiated actions, even if it came through an API call, when the editor is in readOnly mode.\n\nHowever, the documentation never stated API calls with `source` set to `\"user\"` would be ignored sometimes, so this would be a breaking change under semver. Some could argue this is a bug fix and would only warrant a patch version bump, but this seems disingenuous for this particular case. The fact that almost no one took advantage of the `source` beyond default values is irrelevant under the eyes of semver.\n\nSo a `strict` configuration option has been added. It is true by default so the above behavior is unchanged, and [#909](https://github.com/slab/quill/issues/909) is unfixed. Changing this to `false`, will use new behavior of ignoring user initiated changes on a disabled editor, even if through an API call.\n\n### Fixes\n\n- Fix undo when preformatted text inserted before plain text [#1019](https://github.com/slab/quill/issues/1019)\n- Add focus indicator on toolbar buttons [#1020](https://github.com/slab/quill/issues/1020)\n- Do not steal focus on API calls [#1029](https://github.com/slab/quill/issues/1029)\n- Disable paste when Quill is disabled [#1038](https://github.com/slab/quill/issues/1038)\n- Fix blank detection [#1043](https://github.com/slab/quill/issues/1043)\n- Enable yarn [#1041](https://github.com/slab/quill/issues/1041)\n- Documentation fixes [#1026](https://github.com/slab/quill/pull/1026), [#1027](https://github.com/slab/quill/pull/1027), [#1032](https://github.com/slab/quill/pull/1032)\n\nThank you [@benbro](https://github.com/benbro), [@cutteroid](https://github.com/cutteroid), [@evansolomon](https://github.com/evansolomon), [@felipeochoa](https://github.com/felipeochoa), [jackmu95](https://github.com/jackmu95), [@joedynamite](https://github.com/joedynamite), [@lance13c](https://github.com/lance13c), [@leebenson](https://github.com/leebenson), [@maartenvanvliet](https://github.com/maartenvanvliet), [@sarbbottam](https://github.com/sarbbottam), [@viljark](https://github.com/viljark), [@w00fz](https://github.com/w00fz) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.1.0)\n\n# v1.0.6 (2016-09-30)\n\nDocumentation clarifications and bug fixes.\n\n- Fix attaching toolbar to `<select>` without themes [#997](https://github.com/slab/quill/issues/997)\n- Link `code` icon to `code-block` [#998](https://github.com/slab/quill/issues/998)\n- Fix undo stack when at size limit [#1001](https://github.com/slab/quill/pull/1001)\n- Fix bug where `formatLine` did not ignore inline formats [8a7190](https://github.com/quilljs/parchment/commit/8a71905b2dd02d003edb02a15fdc727b26914e49)\n\nThanks to [@dropfen](https://github.com/dropfen), [@evansolomon](https://github.com/evansolomon), [@hallaathrad](https://github.com/hallaathrad), [@janyksteenbeek](https://github.com/janyksteenbeek), [@jackmu95](https://github.com/jackmu95), [@marktron](https://github.com/marktron), [@mcat-ee](https://github.com/mcat-ee), [@unhammer](https://github.com/unhammer), and [@zeke](https://github.com/zeke) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.6)\n\n# v1.0.4 (2016-09-19)\n\n- Fix bubble theme defaults [#963](https://github.com/slab/quill/issues/963)\n- Fix browsers modifying inline nesting order [#971](https://github.com/slab/quill/issues/971)\n- Do not fire selection-change event on paste [#974](https://github.com/slab/quill/issues/974)\n- Support alt attribute in images [#975](https://github.com/slab/quill/issues/975)\n- Deprecate `pasteHTML` for removal in Quill 2.0 [#981](https://github.com/slab/quill/issues/981)\n\nThank you [jackmu95](https://github.com/jackmu95), [kristeehan](https://github.com/kristeehan), [ruffle1986](https://github.com/ruffle1986), [sergop321](https://github.com/sergop321), [sferoze](https://github.com/sferoze), and [sijad](https://github.com/sijad) for contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.4)\n\n# v1.0.3 (2016-09-07)\n\n- Fix [#928](https://github.com/slab/quill/issues/928)\n\nThank you [@scottmessinger](https://github.com/scottmessinger) for the bug report.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.3)\n\n# v1.0.2 (2016-09-07)\n\n- Fix building quill.core.js [docs #11](https://github.com/quilljs/quilljs.github.io/issues/11)\n- Fix regression of [#793](https://github.com/slab/quill/issues/793)\n\nThanks to [@eamodio](https://github.com/eamodio) and [@neandrake](https://github.com/neandrake) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.2)\n\n# v1.0.0 (2016-09-06)\n\nQuill 1.0 is released! Read the [official announcement](https://quilljs.com/blog/announcing-quill-1-0/).\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0)\n\n# v1.0.0-rc.4 (2016-08-31)\n\nFix one important bug [fdd920](https://github.com/slab/quill/commit/fdd920250c05403ed9e5d6d86826a00167ba0b09)\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.4)\n\n# v1.0.0-rc.3 (2016-08-29)\n\nA few bug fixes, one with with possibly significant implications. See the [issue #889](https://github.com/slab/quill/issues/889) and [commit fix](https://github.com/slab/quill/commit/be24c62a6234818548658fcb5e1935a0c07b4eb7) for more details.\n\n### Bug Fixes\n\n- Fix indenting beyond first level with toolbar [#882](https://github.com/slab/quill/issues/882)\n- Fix toolbar font/size display on Safari [#884](https://github.com/slab/quill/issues/884)\n- Fix pasting from Gmail from on different browser [#886](https://github.com/slab/quill/issues/886)\n- Fix undo/redo consistency [#889](https://github.com/slab/quill/issues/889)\n- Fix null error when selecting all on Firefox [#891](https://github.com/slab/quill/issues/891)\n- Fix merging keyboard options twice [#897](https://github.com/slab/quill/issues/897)\n\nThank you [@benbro](https://github.com/benbro), [@cgilboy](https://github.com/cgilboy), [@cutteroid](https://github.com/cutteroid), and [@routman](https://github.com/routman) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.3)\n\n# v1.0.0-rc.2 (2016-08-23)\n\nA few bug fixes, including one significant [one](https://github.com/slab/quill/issues/883)\n\n### Bug Fixes\n\n- Fix icon picker rendering in MS Edge [#877](https://github.com/slab/quill/issues/877)\n- Add back minified build to release [#881](https://github.com/slab/quill/issues/881)\n- Fix optimized change calculation with preformatted text [#883](https://github.com/slab/quill/issues/883)\n\nThanks to [benbro](https://github.com/benbro), [cutteroid](https://github.com/cutteroid), and [CapTec](https://github.com/CapTec) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.2)\n\n# v1.0.0-rc.1 (2016-08-23)\n\nA few bug fixes and performance improvements.\n\n### Features\n\n- Source maps now available from CDN for minified build\n\n### Bug Fixes\n\n- Fix scroll interaction between two Quill editors [#855](https://github.com/slab/quill/issues/855)\n- Fix scroll on paste [#856](https://github.com/slab/quill/issues/856)\n- Fix native iOS tooltip formatting [#862](https://github.com/slab/quill/issues/862)\n- Remove comments from pasting from Word [#872](https://github.com/slab/quill/issues/872)\n- Fix indent at all supported indent levels [#873](https://github.com/slab/quill/issues/873)\n- Fix indent interaction with text direction [#874](https://github.com/slab/quill/issues/874)\n\nThank you [@benbro](https://github.com/benbro), [@fernandogmar](https://github.com/fernandogmar), [@sachinrekhi](https://github.com/sachinrekhi), [@sferoze](https://github.com/sferoze), and [@stalniy](https://github.com/stalniy) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.1)\n\n# v1.0.0-rc.0 (2016-08-18)\n\nTake a look at [Quill 1.0 Release Candidate](https://quilljs.com/blog/quill-1-0-release-candidate-released/) for more details.\n\n### Updates\n\n- Going forward the minimal stylesheet build will be named quill.core.css, instead of quill.css\n\n### Bug Fixes\n\n- Fix identifying ordered and bulletd lists [#846](https://github.com/slab/quill/issues/846) [#847](https://github.com/slab/quill/issues/847)\n- Fix bullet interaction with text direction [#848](https://github.com/slab/quill/issues/848)\n\nA huge thank you to all contributors to through the beta! Special thanks goes to [@benbro](https://github.com/benbro) and [@sachinrekhi](https://github.com/sachinrekhi) who together submitted submitted almost 50 Issues and Pull Requests!\n\n- [@abejdaniels](https://github.com/abejdaniels)\n- [@anovi](https://github.com/anovi)\n- [@benbro](https://github.com/benbro)\n- [@bram2w](https://github.com/bram2w)\n- [@brynjagr](https://github.com/brynjagr)\n- [@CapTec](https://github.com/CapTec)\n- [@Cinamonas](https://github.com/Cinamonas)\n- [@clemmy](https://github.com/clemmy)\n- [@crisbeto](https://github.com/crisbeto)\n- [@cutteroid](https://github.com/cutteroid)\n- [@DadaMonad](https://github.com/DadaMonad)\n- [@davelozier](https://github.com/davelozier)\n- [@emanuelbsilva](https://github.com/emanuelbsilva)\n- [@ersommer](https://github.com/ersommer)\n- [@fernandogmar](https://github.com/fernandogmar)\n- [@george-norris-salesforce](https://github.com/george-norris-salesforce)\n- [@jackmu95](https://github.com/jackmu95)\n- [@jasonmng](https://github.com/jasonmng)\n- [@jbrowning](https://github.com/jbrowning)\n- [@jonnolen](https://github.com/jonnolen)\n- [@KameSama](https://github.com/KameSama)\n- [@kei-ito](https://github.com/kei-ito)\n- [@kylebragger](https://github.com/kylebragger)\n- [@LucVanPelt](https://github.com/LucVanPelt)\n- [@lukechapman](https://github.com/lukechapman)\n- [@micimize](https://github.com/micimize)\n- [@mmorearty](https://github.com/mmorearty)\n- [@mshamaiev-intel471](https://github.com/mshamaiev-intel471)\n- [@quentez](https://github.com/quentez)\n- [@sachinrekhi](https://github.com/sachinrekhi)\n- [@sagacitysite](https://github.com/sagacitysite)\n- [@saw](https://github.com/saw)\n- [@stalniy](https://github.com/stalniy)\n- [@tOgg1](https://github.com/tOgg1)\n- [@u9520107](https://github.com/u9520107)\n- [@WriterStat](https://github.com/WriterStat)\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.0)\n\n# v1.0.0-beta.11 (2016-08-03)\n\nFixed some regressive bugs from previous release.\n\n### Bug Fixes\n\n- Fix activating more than one format before typing [#841](https://github.com/slab/quill/issues/841)\n- Run default matchers before before user defined ones [#843](https://github.com/slab/quill/issues/843)\n- Fix merging theme configurations [#844](https://github.com/slab/quill/issues/844), [#845](https://github.com/slab/quill/issues/845)\n\nThanks [benbro](https://github.com/benbro), [jackmu95](https://github.com/jackmu95), and [george-norris-salesforce](https://github.com/george-norris-salesforce) for the bug reports.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.11)\n\n# v1.0.0-beta.10 (2016-08-03)\n\nLots of bug fixes and performance improvements.\n\n### Breaking Changes\n\n- Keyboard handler format in initial [configuration](beta.quilljs.com/docs/modules/keyboard/) has changed. `addBinding` is overloaded to be backwards compatible.\n\n### Bug Fixes\n\n- Preserve last bullet on paste [#696](https://github.com/slab/quill/issues/696)\n- Fix getBounds calculation for lists [#765](https://github.com/slab/quill/issues/765)\n- Escape quotes in font value [#769](https://github.com/slab/quill/issues/769)\n- Fix spacing calculation on paste [#797](https://github.com/slab/quill/issues/797)\n- Fix Snow tooltip label [#798](https://github.com/slab/quill/issues/798)\n- Fix link tooltip showing up on long click [#799](https://github.com/slab/quill/issues/799)\n- Fix entering code block in IE and Firefox [#803](https://github.com/slab/quill/issues/803)\n- Fix opening image dialog on Firefox [#805](https://github.com/slab/quill/issues/805)\n- Fix focus loss on updateContents [#809](https://github.com/slab/quill/issues/809)\n- Reset toolbar of blur [#810](https://github.com/slab/quill/issues/810)\n- Fix cursor position calculation on delete [#811](https://github.com/slab/quill/issues/811)\n- Fix highlighting across different alignment values [#815](https://github.com/slab/quill/issues/815)\n- Allow default active button [#816](https://github.com/slab/quill/issues/816)\n- Fix deleting last character of formatted text on Firefox [#824](https://github.com/slab/quill/issues/824)\n- Fix Youtube regex [#826](https://github.com/slab/quill/pull/826)\n- Fix missing imports when Quill not global [#836](https://github.com/slab/quill/pull/836)\n\nThanks to [benbro](https://github.com/benbro), [clemmy](https://github.com/clemmy), [crisbeto](https://github.com/crisbeto), [cutteroid](https://github.com/cutteroid), [jackmu95](https://github.com/jackmu95), [kylebragger](https://github.com/kylebragger), [sachinrekhi](https://github.com/sachinrekhi), [stalniy](https://github.com/stalniy), and [tOgg1](https://github.com/tOgg1) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.10)\n\n# v1.0.0-beta.9 (2016-07-19)\n\nPotentially the final beta before a release candidate, if no major issues are discovered.\n\n### Breaking Changes\n\n- No longer expose `ui/link-tooltip` through `import` as implementation is now Snow specific\n- Significant refactoring of `ui/tooltip`\n- Syntax module now autodetects language, instead of defaulting to Javascript\n\n### Features\n\n- Formula and video insertion UI added to Snow and Bubble themes\n\n### Bug Fixes\n\n- Fix toolbar active state after backspacing to previous line [#730](https://github.com/slab/quill/issues/730)\n- User selection is now preserved various API calls [#731](https://github.com/slab/quill/issues/731)\n- Fix long click on link-tooltip [#747](https://github.com/slab/quill/issues/747)\n- Fix ordered list and text-align right interaction [#784](https://github.com/slab/quill/issues/784)\n- Fix toggling code block off [#789](https://github.com/slab/quill/issues/789)\n- Scroll position is now automatically preserved between editor blur and focus\n\nThank you [@benbro](https://github.com/benbro), [@KameSama](https://github.com/KameSama), and [@sachinrekhi](https://github.com/sachinrekhi) for contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.9)\n\n# v1.0.0-beta.8 (2016-07-08)\n\nWeekly beta preview release. The editor is almost ready for release candidacy but a couple cycles will be spent on the Snow and Bubble interfaces.\n\n### Work in Progress\n\nImage insertion is being reworked in the provided Snow and Bubble themes. The old image-tooltip has been removed in favor of a simpler and native interaction. By default clicking the image icon on the toolbar will open the OS file picker to convert and that into a base64 image. This will allow for a more natural hook to upload to a remote server instead. Some changes to the link tooltip is also being made to accommodate formula and video insertion, currently only available through the API.\n\n### Breaking Changes\n\n- Image tooltip UI has been removed, see above\n- Code blocks now use a single `<pre>` tag, instead of one per line [#723](https://github.com/slab/quill/issues/723)\n\n### Bug Fixes\n\n- Fix multiline syntax highlighting [#723](https://github.com/slab/quill/issues/723)\n- Keep pickers open on api text-change [#734](https://github.com/slab/quill/issues/734)\n- Emit correct source for text-change [#760](https://github.com/slab/quill/issues/760)\n- Emit correct parameters in selection-change [#762](https://github.com/slab/quill/issues/762)\n- Fix error redoing line insertion [#767](https://github.com/slab/quill/issues/767)\n- Better emitted Deltas for text-change [#768](https://github.com/slab/quill/issues/768)\n- Better Array.prototype.find polyfill for IE11 [#776](https://github.com/slab/quill/issues/776)\n- Fix Parchment errors in replacing text [#779](https://github.com/slab/quill/issues/779) [#783](https://github.com/slab/quill/issues/783)\n- Fix align button active state [#780](https://github.com/slab/quill/issues/780)\n- Fix format text on falsy value [#782](https://github.com/slab/quill/issues/782)\n- Use native cut [#785](https://github.com/slab/quill/issues/785)\n- Fix inializing document where last line is formatted [#786](https://github.com/slab/quill/issues/786)\n\nThanks to [benbro](https://github.com/benbro), [bram2w](https://github.com/bram2w), [clemmy](https://github.com/clemmy), [DadaMonad](https://github.com/DadaMonad), [ersommer](https://github.com/ersommer), [michaeljosephrosenthal](https://github.com/michaeljosephrosenthal), [mmorearty](https://github.com/mmorearty), [mshamaiev-intel471](https://github.com/mshamaiev-intel471), and [sachinrekhi](https://github.com/sachinrekhi) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.8)\n\n# v1.0.0-beta.6 (2016-06-21)\n\nWeekly beta preview release.\n\n### Features\n\n- Pickers can now be used and is styled in Bubble theme\n\n### Bug Fixes\n\n- Fix editing within formula [#702](https://github.com/slab/quill/issues/702)\n- Fix adding new line when deleting across lists [#741](https://github.com/slab/quill/issues/741)\n- Fix placeholder when default block tag is changed [#743](https://github.com/slab/quill/issues/743)\n- Keep Bubble tooltip open on format [#744](https://github.com/slab/quill/issues/744)\n- Fix format loss when copying from Quill [#748](https://github.com/slab/quill/issues/748) [#750](https://github.com/slab/quill/issues/750)\n- Break long lines in Firefox [#751](https://github.com/slab/quill/issues/751)\n- Fix cursor position being off after formatting and typing quickly [#752](https://github.com/slab/quill/issues/752)\n- Remove image resizing handles on Firefox [#753](https://github.com/slab/quill/issues/753)\n- Fix removing blockquote on initialization [#754](https://github.com/slab/quill/issues/754)\n- Fix adding blank lines on initialization [#756](https://github.com/slab/quill/issues/756)\n\nThank you [abejdaniels](https://github.com/abejdaniels), [benbro](https://github.com/benbro), [davelozier](https://github.com/davelozier), [fernandogmar](https://github.com/fernandogmar), [KameSama](https://github.com/KameSama), and [WriterStat](https://github.com/WriterStat) for contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.6)\n\n# v1.0.0-beta.5 (2016-06-14)\n\nWeekly beta preview release.\n\n### Features\n\n- Add blur() [#726](https://github.com/slab/quill/pull/726)\n\n### Bug Fixes\n\n- Fix null error [#728](https://github.com/slab/quill/issues/728)\n- Fix building with Node v6 [#732](https://github.com/slab/quill/issues/732)\n- Ensure button type for supplied buttons [#733](https://github.com/slab/quill/issues/733)\n- Fix line break pasting on Firefox [#735](https://github.com/slab/quill/issues/735)\n- Fix 'user' source on API calls [#739](https://github.com/slab/quill/issues/739)\n\nThanks to [benbro](https://github.com/benbro), [lukechapman](https://github.com/lukechapman), [sachinrekhi](https://github.com/sachinrekhi), and [saw](https://github.com/saw) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.5)\n\n# v1.0.0-beta.4 (2016-06-03)\n\nWeekly beta preview release.\n\n### Breaking Changes\n\n- Headers no longer generates id attribute [#700](https://github.com/slab/quill/issues/700)\n- Add Control+Y hotkey on Windows [#705](https://github.com/slab/quill/issues/705)\n- BlockEmbed Blots are now length 1 and represented in a Delta the same as an inline embed\n  - value() used to return object and newline, newline is now removed\n  - formats used to be attributed on the newline character, it is now attributed on the object\n\n### Features\n\n- Enter on empty and indented list removes indent [#707](https://github.com/slab/quill/issues/707)\n- Allow base64 images to be inserted via APIs [#721](https://github.com/slab/quill/issues/721)\n\n### Bug Fixes\n\n- Fix typing after clearing inline format [#703](https://github.com/slab/quill/issues/703)\n- Correctly position Bubble tooltip when selecting multiple lines [#706](https://github.com/slab/quill/issues/706)\n- Fix typing after link format [#708](https://github.com/slab/quill/issues/708)\n- Fix loss of selection on using link tooltip [#709](https://github.com/slab/quill/issues/709)\n- Fix `setSelection(null)` [#722](https://github.com/slab/quill/issues/722)\n\nThank you [@benbro](https://github.com/benbro), [@brynjagr](https://github.com/brynjagr), and [@sachinrekhi](https://github.com/sachinrekhi) for contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.4)\n\n# v1.0.0-beta.3 (2016-05-25)\n\n# 1.0.0-beta.3\n\nWeekly beta preview release.\n\n### Breaking Changes\n\n- Keyboard was incorrectly using `metaKey` to refer to the control key on Windows. It now correctly refers to the Window key and `shortKey` has been added to refer the common platform specific modifier for hotkeys (metaKey for Mac, ctrlKey for Windows/Linux)\n- Formula is now a module, since it uses KaTeX\n\n### Features\n\n- Picker now uses text from original `<option>` if available\n- Tabbing inside code blocks inserts tab to each line\n\n### Bug Fixes\n\n- Enter preserves inline formats [#666](https://github.com/slab/quill/issues/666)\n- Fix resetting format button with no selection [#667](https://github.com/slab/quill/issues/667)\n- Fix paste interpretation from Word [#668](https://github.com/slab/quill/issues/668)\n- Focus scrolls to correct cursor position [#669](https://github.com/slab/quill/issues/669)\n- Fix deleting image on otherwise empty document [#670](https://github.com/slab/quill/issues/670)\n- Fix bubble toolbar formatting [#679](https://github.com/slab/quill/issues/679)\n- Fix pasting ql-indent lines [#681](https://github.com/slab/quill/issues/681)\n- Fix getting into state with double underline tag [#695](https://github.com/slab/quill/issues/695)\n- Fix source type on delete [#697](https://github.com/slab/quill/issues/697)\n- Fix indent becoming NaN [#698](https://github.com/slab/quill/issues/698)\n\nThanks to [@benbro](https://github.com/benbro), [@Cinamonas](https://github.com/Cinamonas), [@emanuelbsilva](https://github.com/emanuelbsilva), [@jasonmng](https://github.com/jasonmng), [@jonnolen](https://github.com/jonnolen), [@LucVanPelt](https://github.com/LucVanPelt), [@sachinrekhi](https://github.com/sachinrekhi), [@sagacitysite](https://github.com/sagacitysite), [@WriterStat](https://github.com/WriterStat) for their contributions to this release.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.3)\n\n# v1.0.0-beta.2 (2016-05-15)\n\nWeekly beta preview release. Major emphasis on keyboard API and customization.\n\n### Breaking Changes\n\n- Rename code highlighter module to syntax\n- Clipboard matchers specified in configuration appends to instead of replaces default matchers\n- Change video embed to use `<iframe>` instead of `<video>` enabling Youtube/Vimeo links\n\n### Features\n\n- Add contextual keyboard listeners\n- Allow indent format to take +1/-1 in addition to target indent level\n- Shortcuts for creating ordered or bulleted lists\n- Autofill mailto for email links [#278](https://github.com/slab/quill/issues/278)\n- Enter does not continue header format [#540](https://github.com/slab/quill/issues/540)\n\n### Bug Fixes\n\n- Allow native handling of backspace [#473](https://github.com/slab/quill/issues/473) [#548](https://github.com/slab/quill/issues/548) [#565](https://github.com/slab/quill/issues/565)\n- removeFormat() removes last line block formats [#649](https://github.com/slab/quill/issues/649)\n- Fix text direction icon directon [#654](https://github.com/slab/quill/issues/654)\n- Fix text insertion into root scroll [#655](https://github.com/slab/quill/issues/655)\n- Fix focusing on placeholder text in FF [#656](https://github.com/slab/quill/issues/656)\n- Hide placeholder on formatted line [#657](https://github.com/slab/quill/issues/657)\n- Fix selection handling on focus and blur [#664](https://github.com/slab/quill/issues/664)\n\nThanks to [@anovi](https://github.com/anovi), [@benbro](https://github.com/benbro), [@jbrowning](https://github.com/jbrowning), [@kei-ito](https://github.com/kei-ito), [@quentez](https://github.com/quentez), [@u9520107](https://github.com/u9520107) for their contributions to this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.2)\n\n# v1.0.0-beta.1 (2016-05-10)\n\nWeekly beta preview release.\n\n### Breaking Changes\n\n- Toolbar only attaches to `<button>` and `<select>` elements\n- Toolbar uses button `value` attribute, instead of `data-value`\n- Toolbar handlers overwrite default handlers instead of possibly cascading\n- Deprecate keyboard `removeBinding` and `removeAllBindings`\n\n### Features\n\n- Expose default keyboard bindings in configuration\n- Add context listener to keyboard bindings\n\n### Bug Fixes\n\n- Error when cursor places next to video embed [#644](https://github.com/slab/quill/issues/644)\n- Selection removed when clicking on a menu button in the toolbar [#645](https://github.com/slab/quill/issues/645)\n- Editor looses focus in FF after typing two bold characters [#646](https://github.com/slab/quill/issues/646)\n- Get rid of resize boxes in code in IE11 [0ad636](https://github.com/slab/quill/commit/0ad6363c9fcd70c52ca667d39a393760eeb646b5)\n- Text direction icon should flip the arrow when pressed [#651](https://github.com/slab/quill/issues/651)\n- Not possible to combine direction:rtl with text-align:left [#652](https://github.com/slab/quill/issues/652)\n\nThanks to [@benbro](https://github.com/benbro) for the bug reports for this release!\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.1)\n\n# v1.0.0-beta.0 (2016-05-04)\n\nPlease see the [Upgrading to 1.0](http://beta.quilljs.com/guides/upgrading-to-1-0/) guide.\n\n[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.0)\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017-2024, Slab\nCopyright (c) 2014, Jason Chen\nCopyright (c) 2013, salesforce.com\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n  <a href=\"https://quilljs.com/\" title=\"Quill\">Quill Rich Text Editor</a>\n</h1>\n<p align=\"center\">\n  <a href=\"https://quilljs.com/\" title=\"Quill\"><img alt=\"Quill Logo\" src=\"https://quilljs.com/assets/images/logo.svg\" width=\"180\"></a>\n</p>\n<p align=\"center\">\n  <a title=\"Documentation\" href=\"https://quilljs.com/docs/quickstart\"><strong>Documentation</strong></a>\n  &#x2022;\n  <a title=\"Development\" href=\"https://github.com/slab/quill/blob/main/.github/DEVELOPMENT.md\"><strong>Development</strong></a>\n  &#x2022;\n  <a title=\"Contributing\" href=\"https://github.com/slab/quill/blob/main/.github/CONTRIBUTING.md\"><strong>Contributing</strong></a>\n  &#x2022;\n  <a title=\"Interactive Playground\" href=\"https://quilljs.com/playground/\"><strong>Interactive Playground</strong></a>\n</p>\n<p align=\"center\">\n  <a href=\"https://github.com/slab/quill/actions\" title=\"Build Status\"><img src=\"https://github.com/slab/quill/actions/workflows/main.yml/badge.svg\" alt=\"Build Status\"></a>\n  <a href=\"https://npmjs.com/package/quill\" title=\"Version\"><img src=\"https://img.shields.io/npm/v/quill.svg\" alt=\"Version\"></a>\n  <a href=\"https://npmjs.com/package/quill\" title=\"Downloads\"><img src=\"https://img.shields.io/npm/dm/quill.svg\" alt=\"Downloads\"></a>\n</p>\n\n<hr/>\n\n[Quill](https://quilljs.com/) is a modern rich text editor built for compatibility and extensibility. It was created by [Jason Chen](https://twitter.com/jhchen) and [Byron Milligan](https://twitter.com/byronmilligan) and actively maintained by [Slab](https://slab.com).\n\nTo get started, check out [https://quilljs.com/](https://quilljs.com/) for documentation, guides, and live demos!\n\n## Quickstart\n\nInstantiate a new Quill object with a css selector for the div that should become the editor.\n\n```html\n<!-- Include Quill stylesheet -->\n<link\n  href=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css\"\n  rel=\"stylesheet\"\n/>\n\n<!-- Create the toolbar container -->\n<div id=\"toolbar\">\n  <button class=\"ql-bold\">Bold</button>\n  <button class=\"ql-italic\">Italic</button>\n</div>\n\n<!-- Create the editor container -->\n<div id=\"editor\">\n  <p>Hello World!</p>\n  <p>Some initial <strong>bold</strong> text</p>\n  <p><br /></p>\n</div>\n\n<!-- Include the Quill library -->\n<script src=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js\"></script>\n\n<!-- Initialize Quill editor -->\n<script>\n  const quill = new Quill(\"#editor\", {\n    theme: \"snow\",\n  });\n</script>\n```\n\nTake a look at the [Quill](https://quilljs.com/) website for more documentation, guides and [live playground](https://quilljs.com/playground/)!\n\n## Download\n\n```shell\nnpm install quill\n```\n\n### CDN\n\n```html\n<!-- Main Quill library -->\n<script src=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js\"></script>\n\n<!-- Theme included stylesheets -->\n<link\n  href=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css\"\n  rel=\"stylesheet\"\n/>\n<link\n  href=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.bubble.css\"\n  rel=\"stylesheet\"\n/>\n\n<!-- Core build with no theme, formatting, non-essential modules -->\n<link\n  href=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.core.css\"\n  rel=\"stylesheet\"\n/>\n<script src=\"https://cdn.jsdelivr.net/npm/quill@2/dist/quill.core.js\"></script>\n```\n\n## Community\n\nGet help or stay up to date.\n\n- [Contribute](https://github.com/slab/quill/blob/main/.github/CONTRIBUTING.md) on [Issues](https://github.com/slab/quill/issues)\n- Ask questions on [Discussions](https://github.com/slab/quill/discussions)\n\n## License\n\nBSD 3-clause\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"quill-monorepo\",\n  \"version\": \"2.0.3\",\n  \"description\": \"Quill development environment\",\n  \"private\": true,\n  \"author\": \"Jason Chen <jhchen7@gmail.com>\",\n  \"homepage\": \"https://quilljs.com\",\n  \"config\": {\n    \"ports\": {\n      \"webpack\": \"9080\",\n      \"website\": \"9000\"\n    }\n  },\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"license\": \"BSD-3-Clause\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/slab/quill.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/slab/quill/issues\"\n  },\n  \"scripts\": {\n    \"build\": \"run-p build:*\",\n    \"build:quill\": \"npm run build -w quill\",\n    \"build:website\": \"npm run build -w website\",\n    \"start\": \"run-p start:*\",\n    \"start:quill\": \"npm start -w quill\",\n    \"start:website\": \"NEXT_PUBLIC_LOCAL_QUILL=true npm start -w website\",\n    \"lint\": \"npm run lint -ws\"\n  },\n  \"keywords\": [\n    \"quill\",\n    \"editor\",\n    \"rich text\",\n    \"wysiwyg\",\n    \"operational transformation\",\n    \"ot\",\n    \"framework\"\n  ],\n  \"engines\": {\n    \"npm\": \">=8.2.3\"\n  },\n  \"engineStrict\": true,\n  \"devDependencies\": {\n    \"execa\": \"^9.0.2\",\n    \"npm-run-all\": \"^4.1.5\"\n  }\n}\n"
  },
  {
    "path": "packages/quill/.eslintrc.json",
    "content": "{\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:prettier/recommended\",\n    \"plugin:import/recommended\",\n    \"plugin:require-extensions/recommended\"\n  ],\n  \"env\": {\n    \"browser\": true,\n    \"commonjs\": true,\n    \"es6\": true\n  },\n  \"parser\": \"@typescript-eslint/parser\",\n  \"settings\": {\n    \"import/resolver\": {\n      \"webpack\": {\n        \"env\": \"development\"\n      },\n      \"typescript\": true\n    }\n  },\n  \"ignorePatterns\": [\"*.js\", \"*.d.ts\"],\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.ts\"],\n      \"extends\": [\n        \"plugin:@typescript-eslint/recommended\",\n        \"plugin:import/typescript\"\n      ],\n      \"excludedFiles\": \"*.d.ts\",\n      \"plugins\": [\"@typescript-eslint\", \"require-extensions\"],\n      \"rules\": {\n        \"@typescript-eslint/consistent-type-imports\": \"error\",\n        \"@typescript-eslint/ban-ts-comment\": \"off\",\n        \"@typescript-eslint/no-empty-function\": \"off\",\n        \"@typescript-eslint/ban-types\": \"off\",\n        \"@typescript-eslint/no-explicit-any\": \"off\",\n        \"import/no-named-as-default-member\": \"off\",\n        \"prefer-arrow-callback\": \"error\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/quill/.gitignore",
    "content": "dist\n"
  },
  {
    "path": "packages/quill/LICENSE",
    "content": "Copyright (c) 2017-2024, Slab\nCopyright (c) 2014, Jason Chen\nCopyright (c) 2013, salesforce.com\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "packages/quill/README.md",
    "content": "# Quill\n\nThis is the main package of Quill.\n"
  },
  {
    "path": "packages/quill/babel.config.cjs",
    "content": "const pkg = require('./package.json');\n\nmodule.exports = {\n  presets: [\n    ['@babel/preset-env', { modules: false }],\n    '@babel/preset-typescript',\n  ],\n  plugins: [\n    ['transform-define', { QUILL_VERSION: pkg.version }],\n    './scripts/babel-svg-inline-import.cjs',\n  ],\n};\n"
  },
  {
    "path": "packages/quill/package.json",
    "content": "{\n  \"name\": \"quill\",\n  \"version\": \"2.0.3\",\n  \"description\": \"Your powerful, rich text editor\",\n  \"author\": \"Jason Chen <jhchen7@gmail.com>\",\n  \"homepage\": \"https://quilljs.com\",\n  \"main\": \"quill.js\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"eventemitter3\": \"^5.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"parchment\": \"^3.0.0\",\n    \"quill-delta\": \"^5.1.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"^7.23.9\",\n    \"@babel/core\": \"^7.24.0\",\n    \"@babel/preset-env\": \"^7.24.0\",\n    \"@babel/preset-typescript\": \"^7.23.3\",\n    \"@playwright/test\": \"^1.54.1\",\n    \"@types/highlight.js\": \"^9.12.4\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^20.10.0\",\n    \"@types/webpack\": \"^5.28.5\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.2.0\",\n    \"@typescript-eslint/parser\": \"^7.2.0\",\n    \"@vitest/browser\": \"^3.2.4\",\n    \"babel-loader\": \"^9.1.3\",\n    \"babel-plugin-transform-define\": \"^2.1.4\",\n    \"css-loader\": \"^6.10.0\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-import-resolver-typescript\": \"^3.6.1\",\n    \"eslint-import-resolver-webpack\": \"^0.13.8\",\n    \"eslint-plugin-import\": \"^2.29.1\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-prettier\": \"^5.1.3\",\n    \"eslint-plugin-require-extensions\": \"^0.1.3\",\n    \"glob\": \"10.4.2\",\n    \"highlight.js\": \"^9.18.1\",\n    \"html-loader\": \"^4.2.0\",\n    \"html-webpack-plugin\": \"^5.5.3\",\n    \"jsdom\": \"^22.1.0\",\n    \"mini-css-extract-plugin\": \"^2.7.6\",\n    \"prettier\": \"^3.0.3\",\n    \"source-map-loader\": \"^5.0.0\",\n    \"style-loader\": \"^3.3.3\",\n    \"stylus\": \"^0.62.0\",\n    \"stylus-loader\": \"^7.1.3\",\n    \"svgo\": \"^3.2.0\",\n    \"terser-webpack-plugin\": \"^5.3.9\",\n    \"transpile-webpack-plugin\": \"^1.1.3\",\n    \"ts-loader\": \"^9.5.1\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.4.2\",\n    \"vitest\": \"^3.2.4\",\n    \"webpack\": \"^5.89.0\",\n    \"webpack-cli\": \"^5.1.4\",\n    \"webpack-dev-server\": \"^4.15.1\",\n    \"webpack-merge\": \"^5.10.0\"\n  },\n  \"license\": \"BSD-3-Clause\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/slab/quill.git\",\n    \"directory\": \"packages/quill\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/slab/quill/issues\"\n  },\n  \"prettier\": {\n    \"singleQuote\": true\n  },\n  \"browserslist\": [\n    \"defaults\"\n  ],\n  \"scripts\": {\n    \"build\": \"./scripts/build production\",\n    \"lint\": \"run-s lint:*\",\n    \"lint:eslint\": \"eslint .\",\n    \"lint:tsc\": \"tsc --noEmit --skipLibCheck\",\n    \"start\": \"[[ -z \\\"$npm_package_config_ports_webpack\\\" ]] && webpack-dev-server || webpack-dev-server --port $npm_package_config_ports_webpack\",\n    \"test\": \"run-s test:*\",\n    \"test:unit\": \"vitest --config test/unit/vitest.config.ts\",\n    \"test:fuzz\": \"vitest --config test/fuzz/vitest.config.ts\",\n    \"test:e2e\": \"playwright test\"\n  },\n  \"keywords\": [\n    \"quill\",\n    \"editor\",\n    \"rich text\",\n    \"wysiwyg\",\n    \"operational transformation\",\n    \"ot\",\n    \"framework\"\n  ],\n  \"engines\": {\n    \"npm\": \">=8.2.3\"\n  },\n  \"engineStrict\": true\n}\n"
  },
  {
    "path": "packages/quill/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\nconst port = 9001;\n\nexport default defineConfig({\n  testDir: './test/e2e',\n  testMatch: '*.spec.ts',\n  timeout: 30 * 1000,\n  expect: {\n    timeout: 5000,\n  },\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,\n  reporter: 'list',\n  use: {\n    actionTimeout: 0,\n    trace: 'on-first-retry',\n    baseURL: `https://127.0.0.1:${port}`,\n    ignoreHTTPSErrors: true,\n  },\n  projects: [\n    {\n      name: 'Chrome',\n      use: {\n        ...devices['Desktop Chrome'],\n        contextOptions: {\n          permissions: ['clipboard-read', 'clipboard-write'],\n        },\n      },\n    },\n    { name: 'Firefox', use: { ...devices['Desktop Firefox'] } },\n    { name: 'Safari', use: { ...devices['Desktop Safari'] } },\n  ],\n  webServer: {\n    command: `npx webpack serve --config test/e2e/__dev_server__/webpack.config.cjs --env port=${port}`,\n    port,\n    ignoreHTTPSErrors: true,\n    reuseExistingServer: !process.env.CI,\n    stdout: 'ignore',\n    stderr: 'pipe',\n  },\n});\n"
  },
  {
    "path": "packages/quill/scripts/babel-svg-inline-import.cjs",
    "content": "const fs = require('fs');\nconst { dirname, resolve } = require('path');\nconst { optimize } = require('svgo');\n\nmodule.exports = ({ types: t }) => {\n  class BabelSVGInlineImport {\n    constructor() {\n      return {\n        visitor: {\n          ImportDeclaration: {\n            exit(path, state) {\n              const givenPath = path.node.source.value;\n              if (!givenPath.endsWith('.svg')) {\n                return;\n              }\n              const specifier = path.node.specifiers[0];\n              const id = specifier.local.name;\n              const reference = state && state.file && state.file.opts.filename;\n              const absolutePath = resolve(dirname(reference), givenPath);\n              const content = optimize(\n                fs.readFileSync(absolutePath).toString(),\n                { plugins: [] },\n              ).data;\n\n              const variableValue = t.stringLiteral(content);\n              const variable = t.variableDeclarator(\n                t.identifier(id),\n                variableValue,\n              );\n\n              path.replaceWith({\n                type: 'VariableDeclaration',\n                kind: 'const',\n                declarations: [variable],\n              });\n            },\n          },\n        },\n      };\n    }\n  }\n\n  return new BabelSVGInlineImport();\n};\n"
  },
  {
    "path": "packages/quill/scripts/build",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nDIST=dist\n\nTMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')\nnpx tsc --declaration --emitDeclarationOnly --outDir $TMPDIR\n\nrm -rf $DIST\nmkdir $DIST\nmv $TMPDIR/src/* $DIST\nrm -rf $TMPDIR\nnpx babel src --out-dir $DIST --copy-files --no-copy-ignored --extensions .ts --source-maps\nnpx webpack -- --mode $1\n# https://github.com/webpack-contrib/mini-css-extract-plugin/issues/151\nrm -rf $DIST/dist/*.css.js $DIST/dist/*.css.js.*\ncp package.json $DIST\ncp README.md $DIST\ncp LICENSE $DIST\n"
  },
  {
    "path": "packages/quill/src/assets/base.styl",
    "content": "// Styles shared between snow and bubble\n\ncontrolHeight = 24px\ninputPaddingWidth = 5px\ninputPaddingHeight = 3px\n\ncolorItemMargin = 2px\ncolorItemSize = 16px\ncolorItemsPerRow = 7\n\n\n.ql-{themeName}.ql-toolbar, .ql-{themeName} .ql-toolbar\n  &:after\n    clear: both\n    content: ''\n    display: table\n\n  button\n    background: none\n    border: none\n    cursor: pointer\n    display: inline-block\n    float: left\n    height: controlHeight\n    padding: inputPaddingHeight inputPaddingWidth\n    width: controlHeight + (inputPaddingWidth - inputPaddingHeight)*2\n\n    svg\n      float: left\n      height: 100%\n\n    &:active:hover\n      outline: none\n\n  input.ql-image[type=file]\n    display: none\n\n  button:hover, button:focus, button.ql-active,\n  .ql-picker-label:hover, .ql-picker-label.ql-active,\n  .ql-picker-item:hover, .ql-picker-item.ql-selected\n    color: activeColor\n    .ql-fill, .ql-stroke.ql-fill\n      fill: activeColor\n    .ql-stroke, .ql-stroke-miter\n      stroke: activeColor\n\n// Fix for iOS not losing hover on touch\n@media (pointer: coarse)\n  .ql-{themeName}.ql-toolbar, .ql-{themeName} .ql-toolbar\n    button:hover:not(.ql-active)\n      color: inactiveColor\n      .ql-fill, .ql-stroke.ql-fill\n        fill: inactiveColor\n      .ql-stroke, .ql-stroke-miter\n        stroke: inactiveColor\n\n.ql-{themeName}\n  box-sizing: border-box\n  *\n    box-sizing: border-box\n\n  .ql-hidden\n    display: none\n  .ql-out-bottom, .ql-out-top\n    visibility: hidden\n\n  .ql-tooltip\n    position: absolute\n    transform: translateY(10px)\n    a\n      cursor: pointer\n      text-decoration: none\n  .ql-tooltip.ql-flip\n    transform: translateY(-10px)\n\n  .ql-formats\n    &:after\n      clear: both\n      content: ''\n      display: table\n    display: inline-block\n    vertical-align: middle\n\n  .ql-stroke\n    fill: none\n    stroke: inactiveColor\n    stroke-linecap: round\n    stroke-linejoin: round\n    stroke-width: 2\n  .ql-stroke-miter\n    fill: none\n    stroke: inactiveColor\n    stroke-miterlimit: 10\n    stroke-width: 2\n\n  .ql-fill, .ql-stroke.ql-fill\n    fill: inactiveColor\n\n  .ql-empty\n    fill: none\n  .ql-even\n    fill-rule: evenodd\n  .ql-thin, .ql-stroke.ql-thin\n    stroke-width: 1\n  .ql-transparent\n    opacity: 0.4\n\n  .ql-direction\n    svg:last-child\n      display: none\n  .ql-direction.ql-active\n    svg:last-child\n      display: inline\n    svg:first-child\n      display: none\n\n  .ql-editor\n    h1\n      font-size: 2em\n    h2\n      font-size: 1.5em\n    h3\n      font-size: 1.17em\n    h4\n      font-size: 1em\n    h5\n      font-size: 0.83em\n    h6\n      font-size: 0.67em\n    a\n      text-decoration: underline\n    blockquote\n      border-left: 4px solid #ccc\n      margin-bottom: 5px\n      margin-top: 5px\n      padding-left: 16px\n    code, .ql-code-block-container\n      background-color: #f0f0f0\n      border-radius: 3px\n    .ql-code-block-container\n      margin-bottom: 5px\n      margin-top: 5px\n      padding: 5px 10px\n    code\n      font-size: 85%\n      padding: 2px 4px\n    .ql-code-block-container\n      background-color: #23241f\n      color: #f8f8f2\n      overflow: visible\n    img\n      max-width: 100%\n\n  .ql-picker\n    color: inactiveColor\n    display: inline-block\n    float: left\n    font-size: 14px\n    font-weight: 500\n    height: controlHeight\n    position: relative\n    vertical-align: middle\n  .ql-picker-label\n    cursor: pointer\n    display: inline-block\n    height: 100%\n    padding-left: 8px\n    padding-right: 2px\n    position: relative\n    width: 100%\n    &::before\n      display: inline-block\n      line-height: 22px\n  .ql-picker-options\n    background-color: backgroundColor\n    display: none\n    min-width: 100%\n    padding: 4px 8px\n    position: absolute\n    white-space: nowrap\n    .ql-picker-item\n      cursor: pointer\n      display: block\n      padding-bottom: 5px\n      padding-top: 5px\n  .ql-picker.ql-expanded\n    .ql-picker-label\n      color: borderColor\n      z-index: 2\n      .ql-fill\n        fill: borderColor\n      .ql-stroke\n        stroke: borderColor\n    .ql-picker-options\n      display: block\n      margin-top: -1px\n      top: 100%\n      z-index: 1\n\n  .ql-color-picker, .ql-icon-picker\n    width: controlHeight + 4\n    .ql-picker-label\n      padding: 2px 4px\n      svg\n        right: 4px\n  .ql-icon-picker\n    .ql-picker-options\n      padding: 4px 0px\n    .ql-picker-item\n      height: controlHeight\n      width: controlHeight\n      padding: 2px 4px\n  .ql-color-picker\n    .ql-picker-options\n      padding: inputPaddingHeight inputPaddingWidth\n      width: (colorItemSize + 2*colorItemMargin) * colorItemsPerRow + 2*inputPaddingWidth + 2  // +2 for the border\n    .ql-picker-item\n      border: 1px solid transparent\n      float: left\n      height: colorItemSize\n      margin: colorItemMargin\n      padding: 0px\n      width: colorItemSize\n\n  .ql-picker:not(.ql-color-picker):not(.ql-icon-picker)\n    svg\n      position: absolute\n      margin-top: -9px\n      right: 0\n      top: 50%\n      width: 18px\n\n  .ql-picker.ql-header, .ql-picker.ql-font, .ql-picker.ql-size\n    .ql-picker-label[data-label]:not([data-label='']),\n    .ql-picker-item[data-label]:not([data-label=''])\n      &::before\n        content: attr(data-label)\n\n  .ql-picker.ql-header\n    width: 98px\n    .ql-picker-label::before,\n    .ql-picker-item::before\n      content: 'Normal'\n    for num in (1..6)\n      .ql-picker-label[data-value=\\\"{num}\\\"]::before,\n      .ql-picker-item[data-value=\\\"{num}\\\"]::before\n        content: 'Heading ' + num\n    .ql-picker-item[data-value=\"1\"]::before\n      font-size: 2em\n    .ql-picker-item[data-value=\"2\"]::before\n      font-size: 1.5em\n    .ql-picker-item[data-value=\"3\"]::before\n      font-size: 1.17em\n    .ql-picker-item[data-value=\"4\"]::before\n      font-size: 1em\n    .ql-picker-item[data-value=\"5\"]::before\n      font-size: 0.83em\n    .ql-picker-item[data-value=\"6\"]::before\n      font-size: 0.67em\n\n  .ql-picker.ql-font\n    width: 108px\n    .ql-picker-label::before,\n    .ql-picker-item::before\n      content: 'Sans Serif'\n    .ql-picker-label[data-value=serif]::before,\n    .ql-picker-item[data-value=serif]::before\n      content: 'Serif'\n    .ql-picker-label[data-value=monospace]::before,\n    .ql-picker-item[data-value=monospace]::before\n      content: 'Monospace'\n    .ql-picker-item[data-value=serif]::before\n      font-family: Georgia, Times New Roman, serif\n    .ql-picker-item[data-value=monospace]::before\n      font-family: Monaco, Courier New, monospace\n\n  .ql-picker.ql-size\n    width: 98px\n    .ql-picker-label::before,\n    .ql-picker-item::before\n      content: 'Normal'\n    .ql-picker-label[data-value=small]::before,\n    .ql-picker-item[data-value=small]::before\n      content: 'Small'\n    .ql-picker-label[data-value=large]::before,\n    .ql-picker-item[data-value=large]::before\n      content: 'Large'\n    .ql-picker-label[data-value=huge]::before,\n    .ql-picker-item[data-value=huge]::before\n      content: 'Huge'\n    .ql-picker-item[data-value=small]::before\n      font-size: 10px\n    .ql-picker-item[data-value=large]::before\n      font-size: 18px\n    .ql-picker-item[data-value=huge]::before\n      font-size: 32px\n\n  .ql-color-picker.ql-background\n    .ql-picker-item\n      background-color: #fff\n  .ql-color-picker.ql-color\n    .ql-picker-item\n      background-color: #000\n\n.ql-code-block-container\n  position: relative\n  .ql-ui\n    right: 5px\n    top: 5px\n"
  },
  {
    "path": "packages/quill/src/assets/bubble/toolbar.styl",
    "content": "arrowWidth = 6px\n\n.ql-bubble\n  .ql-toolbar\n    .ql-formats\n      margin: 8px 12px 8px 0px\n    .ql-formats:first-child\n      margin-left: 12px\n\n  .ql-color-picker\n    svg\n      margin: 1px\n    .ql-picker-item.ql-selected, .ql-picker-item:hover\n      border-color: activeColor\n"
  },
  {
    "path": "packages/quill/src/assets/bubble/tooltip.styl",
    "content": "arrowWidth = 6px\n\n.ql-bubble\n  .ql-tooltip\n    background-color: backgroundColor\n    border-radius: 25px\n    color: textColor\n  .ql-tooltip-arrow\n    border-left: arrowWidth solid transparent\n    border-right: arrowWidth solid transparent\n    content: \" \"\n    display: block\n    left: 50%\n    margin-left: -1 * arrowWidth\n    position: absolute\n  .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow\n    border-bottom: arrowWidth solid backgroundColor\n    top: -1 * arrowWidth\n  .ql-tooltip.ql-flip .ql-tooltip-arrow\n    border-top: arrowWidth solid backgroundColor\n    bottom: -1 * arrowWidth\n\n  .ql-tooltip.ql-editing\n    .ql-tooltip-editor\n      display: block\n    .ql-formats\n      visibility: hidden\n\n  .ql-tooltip-editor\n    display: none\n    input[type=text]\n      background: transparent\n      border: none\n      color: textColor\n      font-size: 13px\n      height: 100%\n      outline: none\n      padding: 10px 20px\n      position: absolute\n      width: 100%\n    a\n      &:before\n        color: inactiveColor\n        content: \"\\00D7\"\n        font-size: 16px\n        font-weight: bold\n      top: 10px\n      position: absolute\n      right: 20px\n"
  },
  {
    "path": "packages/quill/src/assets/bubble.styl",
    "content": "themeName = 'bubble'\nactiveColor = #fff\nborderColor = #777\nbackgroundColor = #444\ninactiveColor = #ccc\nshadowColor = #ddd\ntextColor = #fff\n\n@import './core'\n@import './base'\n@import './bubble/*'\n\n.ql-container.ql-bubble:not(.ql-disabled)\n  a:not(.ql-close)\n    position: relative\n    white-space: nowrap\n  a:not(.ql-close)::before\n    background-color: #444\n    border-radius: 15px\n    top: -5px\n    font-size: 12px\n    color: #fff\n    content: attr(href)\n    font-weight: normal\n    overflow: hidden\n    padding: 5px 15px\n    text-decoration: none\n    z-index: 1\n  a:not(.ql-close)::after\n    border-top: 6px solid #444\n    border-left: 6px solid transparent\n    border-right: 6px solid transparent\n    top: 0\n    content: \" \"\n    height: 0\n    width: 0\n  a:not(.ql-close)::before, a:not(.ql-close)::after\n    left: 0\n    margin-left: 50%\n    position: absolute\n    transform: translate(-50%, -100%)\n    transition: visibility 0s ease 200ms\n    visibility: hidden\n  a:not(.ql-close):hover::before, a:not(.ql-close):hover::after\n    visibility: visible\n"
  },
  {
    "path": "packages/quill/src/assets/core.styl",
    "content": "// Styles necessary for Quill\n\nLIST_STYLE = decimal lower-alpha lower-roman\nLIST_STYLE_WIDTH = 1.2em\nLIST_STYLE_MARGIN = 0.3em\nLIST_STYLE_OUTER_WIDTH = LIST_STYLE_MARGIN + LIST_STYLE_WIDTH\nMAX_INDENT = 9\n\nresets(arr)\n  unquote('list-' + join(' list-', arr))\n\n.ql-container\n  box-sizing: border-box\n  font-family: Helvetica, Arial, sans-serif\n  font-size: 13px\n  height: 100%\n  margin: 0px\n  position: relative\n\n.ql-container.ql-disabled\n  .ql-tooltip\n    visibility: hidden\n\n.ql-container:not(.ql-disabled)\n  li[data-list=checked],\n  li[data-list=unchecked]\n    > .ql-ui\n      cursor: pointer\n\n.ql-clipboard\n  left: -100000px\n  height: 1px\n  overflow-y: hidden\n  position: absolute\n  top: 50%\n  p\n    margin: 0\n    padding: 0\n\n.ql-editor\n  box-sizing: border-box\n  counter-reset: resets(0..MAX_INDENT)\n  line-height: 1.42\n  height: 100%\n  outline: none\n  overflow-y: auto\n  padding: 12px 15px\n  tab-size: 4\n  -moz-tab-size: 4\n  text-align: left\n  white-space: pre-wrap\n  word-wrap: break-word\n  > *\n    cursor: text\n\n  p, ol, pre, blockquote, h1, h2, h3, h4, h5, h6\n    margin: 0\n    padding: 0\n  p, h1, h2, h3, h4, h5, h6\n    @supports (counter-set: none)\n      counter-set: resets(0..MAX_INDENT)\n    @supports not (counter-set: none)\n      counter-reset: resets(0..MAX_INDENT)\n  table\n    border-collapse: collapse\n  td\n    border: 1px solid #000\n    padding: 2px 5px\n  ol\n    padding-left: 1.5em\n  li\n    list-style-type: none\n    padding-left: LIST_STYLE_OUTER_WIDTH\n    position: relative\n\n    > .ql-ui:before\n      display: inline-block\n      margin-left: -1*LIST_STYLE_OUTER_WIDTH\n      margin-right: LIST_STYLE_MARGIN\n      text-align: right\n      white-space: nowrap\n      width: LIST_STYLE_WIDTH\n\n  li[data-list=checked],\n  li[data-list=unchecked]\n    > .ql-ui\n      color: #777\n\n  li[data-list=bullet] > .ql-ui:before\n    content: '\\2022'\n  li[data-list=checked] > .ql-ui:before\n    content: '\\2611'\n  li[data-list=unchecked] > .ql-ui:before\n    content: '\\2610'\n\n  li[data-list]\n    @supports (counter-set: none)\n      counter-set: resets(1..MAX_INDENT)\n    @supports not (counter-set: none)\n      counter-reset: resets(1..MAX_INDENT)\n\n  li[data-list=ordered]\n    counter-increment: list-0\n    > .ql-ui:before\n      content: unquote('counter(list-0, ' + LIST_STYLE[0] + ')') '. '\n  for num in (1..MAX_INDENT)\n    li[data-list=ordered].ql-indent-{num}\n      counter-increment: unquote('list-' + num)\n      > .ql-ui:before\n        content: unquote('counter(list-' + num + ', ' + LIST_STYLE[num%3] + ')') '. '\n    if (num < MAX_INDENT)\n      li[data-list].ql-indent-{num}\n        @supports (counter-set: none)\n          counter-set: resets((num+1)..MAX_INDENT)\n        @supports not (counter-set: none)\n          counter-reset: resets((num+1)..MAX_INDENT)\n\n  for num in (1..MAX_INDENT)\n    .ql-indent-{num}:not(.ql-direction-rtl)\n      padding-left: (3*num)em\n    li.ql-indent-{num}:not(.ql-direction-rtl)\n      padding-left: (3*num + LIST_STYLE_OUTER_WIDTH)em\n    .ql-indent-{num}.ql-direction-rtl.ql-align-right\n      padding-right: (3*num)em\n    li.ql-indent-{num}.ql-direction-rtl.ql-align-right\n      padding-right: (3*num + LIST_STYLE_OUTER_WIDTH)em\n\n  li.ql-direction-rtl\n    padding-right: LIST_STYLE_OUTER_WIDTH\n    > .ql-ui:before\n      margin-left: LIST_STYLE_MARGIN\n      margin-right: -1*LIST_STYLE_OUTER_WIDTH\n      text-align: left\n\n  table\n    table-layout: fixed\n    width: 100%\n    td\n      outline: none\n\n  .ql-code-block-container\n    font-family: monospace\n\n  .ql-video\n    display: block\n    max-width: 100%\n  .ql-video.ql-align-center\n    margin: 0 auto\n  .ql-video.ql-align-right\n    margin: 0 0 0 auto\n\n  .ql-bg-black\n    background-color: rgb(0,0,0)\n  .ql-bg-red\n    background-color: rgb(230,0,0)\n  .ql-bg-orange\n    background-color: rgb(255,153,0)\n  .ql-bg-yellow\n    background-color: rgb(255,255,0)\n  .ql-bg-green\n    background-color: rgb(0,138,0)\n  .ql-bg-blue\n    background-color: rgb(0,102,204)\n  .ql-bg-purple\n    background-color: rgb(153,51,255)\n\n  .ql-color-white\n    color: rgb(255,255,255)\n  .ql-color-red\n    color: rgb(230,0,0)\n  .ql-color-orange\n    color: rgb(255,153,0)\n  .ql-color-yellow\n    color: rgb(255,255,0)\n  .ql-color-green\n    color: rgb(0,138,0)\n  .ql-color-blue\n    color: rgb(0,102,204)\n  .ql-color-purple\n    color: rgb(153,51,255)\n\n  .ql-font-serif\n    font-family: Georgia, Times New Roman, serif\n  .ql-font-monospace\n    font-family: Monaco, Courier New, monospace\n\n  .ql-size-small\n    font-size: 0.75em\n  .ql-size-large\n    font-size: 1.5em\n  .ql-size-huge\n    font-size: 2.5em\n\n  .ql-direction-rtl\n    direction: rtl\n    text-align: inherit\n\n  .ql-align-center\n    text-align: center\n  .ql-align-justify\n    text-align: justify\n  .ql-align-right\n    text-align: right\n\n  .ql-ui\n    position: absolute\n\n.ql-editor.ql-blank::before\n  color: rgba(0,0,0,0.6)\n  content: attr(data-placeholder)\n  font-style: italic\n  left: 15px\n  pointer-events: none\n  position: absolute\n  right: 15px\n"
  },
  {
    "path": "packages/quill/src/assets/snow/toolbar.styl",
    "content": ".ql-toolbar.ql-snow\n  border: 1px solid borderColor\n  box-sizing: border-box\n  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif\n  padding: 8px\n\n  .ql-formats\n    margin-right: 15px\n\n  .ql-picker-label\n    border: 1px solid transparent\n  .ql-picker-options\n    border: 1px solid transparent\n    box-shadow: rgba(0,0,0,0.2) 0 2px 8px\n  .ql-picker.ql-expanded\n    .ql-picker-label\n      border-color: borderColor\n    .ql-picker-options\n      border-color: borderColor\n\n  .ql-color-picker\n    .ql-picker-item.ql-selected, .ql-picker-item:hover\n      border-color: #000\n\n.ql-toolbar.ql-snow + .ql-container.ql-snow\n  border-top: 0px;\n"
  },
  {
    "path": "packages/quill/src/assets/snow/tooltip.styl",
    "content": "tooltipMargin = 8px\n\n.ql-snow\n  .ql-tooltip\n    background-color: #fff\n    border: 1px solid borderColor\n    box-shadow: 0px 0px 5px shadowColor\n    color: textColor\n    padding: 5px 12px\n    white-space: nowrap\n    &::before\n      content: \"Visit URL:\"\n      line-height: 26px\n      margin-right: tooltipMargin\n    input[type=text]\n      display: none\n      border: 1px solid borderColor\n      font-size: 13px\n      height: 26px\n      margin: 0px\n      padding: 3px 5px\n      width: 170px\n    a.ql-preview\n      display: inline-block\n      max-width: 200px\n      overflow-x: hidden\n      text-overflow: ellipsis\n      vertical-align: top\n    a.ql-action::after\n      border-right: 1px solid borderColor\n      content: 'Edit'\n      margin-left: tooltipMargin*2\n      padding-right: tooltipMargin\n    a.ql-remove::before\n      content: 'Remove'\n      margin-left: tooltipMargin\n    a\n      line-height: 26px\n  .ql-tooltip.ql-editing\n    a.ql-preview, a.ql-remove\n      display: none\n    input[type=text]\n      display: inline-block\n    a.ql-action::after\n      border-right: 0px\n      content: 'Save'\n      padding-right: 0px\n  .ql-tooltip[data-mode=link]::before\n    content: \"Enter link:\"\n  .ql-tooltip[data-mode=formula]::before\n    content: \"Enter formula:\"\n  .ql-tooltip[data-mode=video]::before\n    content: \"Enter video:\"\n"
  },
  {
    "path": "packages/quill/src/assets/snow.styl",
    "content": "themeName = 'snow'\nactiveColor = #06c\nborderColor = #ccc\nbackgroundColor = #fff\ninactiveColor = #444\nshadowColor = #ddd\ntextColor = #444\n\n@import './core'\n@import './base'\n@import './snow/*'\n\n.ql-snow\n  a\n    color: activeColor\n\n.ql-container.ql-snow\n  border: 1px solid borderColor\n"
  },
  {
    "path": "packages/quill/src/blots/block.ts",
    "content": "import {\n  AttributorStore,\n  BlockBlot,\n  EmbedBlot,\n  LeafBlot,\n  Scope,\n} from 'parchment';\nimport type { Blot, Parent } from 'parchment';\nimport Delta from 'quill-delta';\nimport Break from './break.js';\nimport Inline from './inline.js';\nimport TextBlot from './text.js';\n\nconst NEWLINE_LENGTH = 1;\n\nclass Block extends BlockBlot {\n  cache: { delta?: Delta | null; length?: number } = {};\n\n  delta(): Delta {\n    if (this.cache.delta == null) {\n      this.cache.delta = blockDelta(this);\n    }\n    return this.cache.delta;\n  }\n\n  deleteAt(index: number, length: number) {\n    super.deleteAt(index, length);\n    this.cache = {};\n  }\n\n  formatAt(index: number, length: number, name: string, value: unknown) {\n    if (length <= 0) return;\n    if (this.scroll.query(name, Scope.BLOCK)) {\n      if (index + length === this.length()) {\n        this.format(name, value);\n      }\n    } else {\n      super.formatAt(\n        index,\n        Math.min(length, this.length() - index - 1),\n        name,\n        value,\n      );\n    }\n    this.cache = {};\n  }\n\n  insertAt(index: number, value: string, def?: unknown) {\n    if (def != null) {\n      super.insertAt(index, value, def);\n      this.cache = {};\n      return;\n    }\n    if (value.length === 0) return;\n    const lines = value.split('\\n');\n    const text = lines.shift() as string;\n    if (text.length > 0) {\n      if (index < this.length() - 1 || this.children.tail == null) {\n        super.insertAt(Math.min(index, this.length() - 1), text);\n      } else {\n        this.children.tail.insertAt(this.children.tail.length(), text);\n      }\n      this.cache = {};\n    }\n    // TODO: Fix this next time the file is edited.\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    let block: Blot | this = this;\n    lines.reduce((lineIndex, line) => {\n      // @ts-expect-error Fix me later\n      block = block.split(lineIndex, true);\n      block.insertAt(0, line);\n      return line.length;\n    }, index + text.length);\n  }\n\n  insertBefore(blot: Blot, ref?: Blot | null) {\n    const { head } = this.children;\n    super.insertBefore(blot, ref);\n    if (head instanceof Break) {\n      head.remove();\n    }\n    this.cache = {};\n  }\n\n  length() {\n    if (this.cache.length == null) {\n      this.cache.length = super.length() + NEWLINE_LENGTH;\n    }\n    return this.cache.length;\n  }\n\n  moveChildren(target: Parent, ref?: Blot | null) {\n    super.moveChildren(target, ref);\n    this.cache = {};\n  }\n\n  optimize(context: { [key: string]: any }) {\n    super.optimize(context);\n    this.cache = {};\n  }\n\n  path(index: number) {\n    return super.path(index, true);\n  }\n\n  removeChild(child: Blot) {\n    super.removeChild(child);\n    this.cache = {};\n  }\n\n  split(index: number, force: boolean | undefined = false): Blot | null {\n    if (force && (index === 0 || index >= this.length() - NEWLINE_LENGTH)) {\n      const clone = this.clone();\n      if (index === 0) {\n        this.parent.insertBefore(clone, this);\n        return this;\n      }\n      this.parent.insertBefore(clone, this.next);\n      return clone;\n    }\n    const next = super.split(index, force);\n    this.cache = {};\n    return next;\n  }\n}\nBlock.blotName = 'block';\nBlock.tagName = 'P';\nBlock.defaultChild = Break;\nBlock.allowedChildren = [Break, Inline, EmbedBlot, TextBlot];\n\nclass BlockEmbed extends EmbedBlot {\n  attributes: AttributorStore;\n  domNode: HTMLElement;\n\n  attach() {\n    super.attach();\n    this.attributes = new AttributorStore(this.domNode);\n  }\n\n  delta() {\n    return new Delta().insert(this.value(), {\n      ...this.formats(),\n      ...this.attributes.values(),\n    });\n  }\n\n  format(name: string, value: unknown) {\n    const attribute = this.scroll.query(name, Scope.BLOCK_ATTRIBUTE);\n    if (attribute != null) {\n      // @ts-expect-error TODO: Scroll#query() should return Attributor when scope is attribute\n      this.attributes.attribute(attribute, value);\n    }\n  }\n\n  formatAt(index: number, length: number, name: string, value: unknown) {\n    this.format(name, value);\n  }\n\n  insertAt(index: number, value: string, def?: unknown) {\n    if (def != null) {\n      super.insertAt(index, value, def);\n      return;\n    }\n    const lines = value.split('\\n');\n    const text = lines.pop();\n    const blocks = lines.map((line) => {\n      const block = this.scroll.create(Block.blotName);\n      block.insertAt(0, line);\n      return block;\n    });\n    const ref = this.split(index);\n    blocks.forEach((block) => {\n      this.parent.insertBefore(block, ref);\n    });\n    if (text) {\n      this.parent.insertBefore(this.scroll.create('text', text), ref);\n    }\n  }\n}\nBlockEmbed.scope = Scope.BLOCK_BLOT;\n// It is important for cursor behavior BlockEmbeds use tags that are block level elements\n\nfunction blockDelta(blot: BlockBlot, filter = true) {\n  return blot\n    .descendants(LeafBlot)\n    .reduce((delta, leaf) => {\n      if (leaf.length() === 0) {\n        return delta;\n      }\n      return delta.insert(leaf.value(), bubbleFormats(leaf, {}, filter));\n    }, new Delta())\n    .insert('\\n', bubbleFormats(blot));\n}\n\nfunction bubbleFormats(\n  blot: Blot | null,\n  formats: Record<string, unknown> = {},\n  filter = true,\n): Record<string, unknown> {\n  if (blot == null) return formats;\n  if ('formats' in blot && typeof blot.formats === 'function') {\n    formats = {\n      ...formats,\n      ...blot.formats(),\n    };\n    if (filter) {\n      // exclude syntax highlighting from deltas and getFormat()\n      delete formats['code-token'];\n    }\n  }\n  if (\n    blot.parent == null ||\n    blot.parent.statics.blotName === 'scroll' ||\n    blot.parent.statics.scope !== blot.statics.scope\n  ) {\n    return formats;\n  }\n  return bubbleFormats(blot.parent, formats, filter);\n}\n\nexport { blockDelta, bubbleFormats, BlockEmbed, Block as default };\n"
  },
  {
    "path": "packages/quill/src/blots/break.ts",
    "content": "import { EmbedBlot } from 'parchment';\n\nclass Break extends EmbedBlot {\n  static value() {\n    return undefined;\n  }\n\n  optimize() {\n    if (this.prev || this.next) {\n      this.remove();\n    }\n  }\n\n  length() {\n    return 0;\n  }\n\n  value() {\n    return '';\n  }\n}\nBreak.blotName = 'break';\nBreak.tagName = 'BR';\n\nexport default Break;\n"
  },
  {
    "path": "packages/quill/src/blots/container.ts",
    "content": "import { ContainerBlot } from 'parchment';\n\nclass Container extends ContainerBlot {}\n\nexport default Container;\n"
  },
  {
    "path": "packages/quill/src/blots/cursor.ts",
    "content": "import { EmbedBlot, Scope } from 'parchment';\nimport type { Parent, ScrollBlot } from 'parchment';\nimport type Selection from '../core/selection.js';\nimport TextBlot from './text.js';\nimport type { EmbedContextRange } from './embed.js';\n\nclass Cursor extends EmbedBlot {\n  static blotName = 'cursor';\n  static className = 'ql-cursor';\n  static tagName = 'span';\n  static CONTENTS = '\\uFEFF'; // Zero width no break space\n\n  static value() {\n    return undefined;\n  }\n\n  selection: Selection;\n  textNode: Text;\n  savedLength: number;\n\n  constructor(scroll: ScrollBlot, domNode: HTMLElement, selection: Selection) {\n    super(scroll, domNode);\n    this.selection = selection;\n    this.textNode = document.createTextNode(Cursor.CONTENTS);\n    this.domNode.appendChild(this.textNode);\n    this.savedLength = 0;\n  }\n\n  detach() {\n    // super.detach() will also clear domNode.__blot\n    if (this.parent != null) this.parent.removeChild(this);\n  }\n\n  format(name: string, value: unknown) {\n    if (this.savedLength !== 0) {\n      super.format(name, value);\n      return;\n    }\n    // TODO: Fix this next time the file is edited.\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    let target: Parent | this = this;\n    let index = 0;\n    while (target != null && target.statics.scope !== Scope.BLOCK_BLOT) {\n      index += target.offset(target.parent);\n      target = target.parent;\n    }\n    if (target != null) {\n      this.savedLength = Cursor.CONTENTS.length;\n      // @ts-expect-error TODO: allow empty context in Parchment\n      target.optimize();\n      target.formatAt(index, Cursor.CONTENTS.length, name, value);\n      this.savedLength = 0;\n    }\n  }\n\n  index(node: Node, offset: number) {\n    if (node === this.textNode) return 0;\n    return super.index(node, offset);\n  }\n\n  length() {\n    return this.savedLength;\n  }\n\n  position(): [Text, number] {\n    return [this.textNode, this.textNode.data.length];\n  }\n\n  remove() {\n    super.remove();\n    // @ts-expect-error Fix me later\n    this.parent = null;\n  }\n\n  restore(): EmbedContextRange | null {\n    if (this.selection.composing || this.parent == null) return null;\n    const range = this.selection.getNativeRange();\n    // Browser may push down styles/nodes inside the cursor blot.\n    // https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#push-down-values\n    while (\n      this.domNode.lastChild != null &&\n      this.domNode.lastChild !== this.textNode\n    ) {\n      // @ts-expect-error Fix me later\n      this.domNode.parentNode.insertBefore(\n        this.domNode.lastChild,\n        this.domNode,\n      );\n    }\n\n    const prevTextBlot = this.prev instanceof TextBlot ? this.prev : null;\n    const prevTextLength = prevTextBlot ? prevTextBlot.length() : 0;\n    const nextTextBlot = this.next instanceof TextBlot ? this.next : null;\n    // @ts-expect-error TODO: make TextBlot.text public\n    const nextText = nextTextBlot ? nextTextBlot.text : '';\n    const { textNode } = this;\n    // take text from inside this blot and reset it\n    const newText = textNode.data.split(Cursor.CONTENTS).join('');\n    textNode.data = Cursor.CONTENTS;\n\n    // proactively merge TextBlots around cursor so that optimization\n    // doesn't lose the cursor.  the reason we are here in cursor.restore\n    // could be that the user clicked in prevTextBlot or nextTextBlot, or\n    // the user typed something.\n    let mergedTextBlot;\n    if (prevTextBlot) {\n      mergedTextBlot = prevTextBlot;\n      if (newText || nextTextBlot) {\n        prevTextBlot.insertAt(prevTextBlot.length(), newText + nextText);\n        if (nextTextBlot) {\n          nextTextBlot.remove();\n        }\n      }\n    } else if (nextTextBlot) {\n      mergedTextBlot = nextTextBlot;\n      nextTextBlot.insertAt(0, newText);\n    } else {\n      const newTextNode = document.createTextNode(newText);\n      mergedTextBlot = this.scroll.create(newTextNode);\n      this.parent.insertBefore(mergedTextBlot, this);\n    }\n\n    this.remove();\n    if (range) {\n      // calculate selection to restore\n      const remapOffset = (node: Node, offset: number) => {\n        if (prevTextBlot && node === prevTextBlot.domNode) {\n          return offset;\n        }\n        if (node === textNode) {\n          return prevTextLength + offset - 1;\n        }\n        if (nextTextBlot && node === nextTextBlot.domNode) {\n          return prevTextLength + newText.length + offset;\n        }\n        return null;\n      };\n\n      const start = remapOffset(range.start.node, range.start.offset);\n      const end = remapOffset(range.end.node, range.end.offset);\n      if (start !== null && end !== null) {\n        return {\n          startNode: mergedTextBlot.domNode,\n          startOffset: start,\n          endNode: mergedTextBlot.domNode,\n          endOffset: end,\n        };\n      }\n    }\n    return null;\n  }\n\n  update(mutations: MutationRecord[], context: Record<string, unknown>) {\n    if (\n      mutations.some((mutation) => {\n        return (\n          mutation.type === 'characterData' && mutation.target === this.textNode\n        );\n      })\n    ) {\n      const range = this.restore();\n      if (range) context.range = range;\n    }\n  }\n\n  // Avoid .ql-cursor being a descendant of `<a/>`.\n  // The reason is Safari pushes down `<a/>` on text insertion.\n  // That will cause DOM nodes not sync with the model.\n  //\n  // For example ({I} is the caret), given the markup:\n  //    <a><span class=\"ql-cursor\">\\uFEFF{I}</span></a>\n  // When typing a char \"x\", `<a/>` will be pushed down inside the `<span>` first:\n  //    <span class=\"ql-cursor\"><a>\\uFEFF{I}</a></span>\n  // And then \"x\" will be inserted after `<a/>`:\n  //    <span class=\"ql-cursor\"><a>\\uFEFF</a>d{I}</span>\n  optimize(context?: unknown) {\n    // @ts-expect-error Fix me later\n    super.optimize(context);\n\n    let { parent } = this;\n    while (parent) {\n      if (parent.domNode.tagName === 'A') {\n        this.savedLength = Cursor.CONTENTS.length;\n        // @ts-expect-error TODO: make isolate generic\n        parent.isolate(this.offset(parent), this.length()).unwrap();\n        this.savedLength = 0;\n        break;\n      }\n      parent = parent.parent;\n    }\n  }\n\n  value() {\n    return '';\n  }\n}\n\nexport default Cursor;\n"
  },
  {
    "path": "packages/quill/src/blots/embed.ts",
    "content": "import type { ScrollBlot } from 'parchment';\nimport { EmbedBlot } from 'parchment';\nimport TextBlot from './text.js';\n\nconst GUARD_TEXT = '\\uFEFF';\n\nexport interface EmbedContextRange {\n  startNode: Node | Text;\n  startOffset: number;\n  endNode?: Node | Text;\n  endOffset?: number;\n}\n\nclass Embed extends EmbedBlot {\n  contentNode: HTMLSpanElement;\n  leftGuard: Text;\n  rightGuard: Text;\n\n  constructor(scroll: ScrollBlot, node: Node) {\n    super(scroll, node);\n    this.contentNode = document.createElement('span');\n    this.contentNode.setAttribute('contenteditable', 'false');\n    Array.from(this.domNode.childNodes).forEach((childNode) => {\n      this.contentNode.appendChild(childNode);\n    });\n    this.leftGuard = document.createTextNode(GUARD_TEXT);\n    this.rightGuard = document.createTextNode(GUARD_TEXT);\n    this.domNode.appendChild(this.leftGuard);\n    this.domNode.appendChild(this.contentNode);\n    this.domNode.appendChild(this.rightGuard);\n  }\n\n  index(node: Node, offset: number) {\n    if (node === this.leftGuard) return 0;\n    if (node === this.rightGuard) return 1;\n    return super.index(node, offset);\n  }\n\n  restore(node: Text): EmbedContextRange | null {\n    let range: EmbedContextRange | null = null;\n    let textNode: Text;\n    const text = node.data.split(GUARD_TEXT).join('');\n    if (node === this.leftGuard) {\n      if (this.prev instanceof TextBlot) {\n        const prevLength = this.prev.length();\n        this.prev.insertAt(prevLength, text);\n        range = {\n          startNode: this.prev.domNode,\n          startOffset: prevLength + text.length,\n        };\n      } else {\n        textNode = document.createTextNode(text);\n        this.parent.insertBefore(this.scroll.create(textNode), this);\n        range = {\n          startNode: textNode,\n          startOffset: text.length,\n        };\n      }\n    } else if (node === this.rightGuard) {\n      if (this.next instanceof TextBlot) {\n        this.next.insertAt(0, text);\n        range = {\n          startNode: this.next.domNode,\n          startOffset: text.length,\n        };\n      } else {\n        textNode = document.createTextNode(text);\n        this.parent.insertBefore(this.scroll.create(textNode), this.next);\n        range = {\n          startNode: textNode,\n          startOffset: text.length,\n        };\n      }\n    }\n    node.data = GUARD_TEXT;\n    return range;\n  }\n\n  update(mutations: MutationRecord[], context: Record<string, unknown>) {\n    mutations.forEach((mutation) => {\n      if (\n        mutation.type === 'characterData' &&\n        (mutation.target === this.leftGuard ||\n          mutation.target === this.rightGuard)\n      ) {\n        const range = this.restore(mutation.target as Text);\n        if (range) context.range = range;\n      }\n    });\n  }\n}\n\nexport default Embed;\n"
  },
  {
    "path": "packages/quill/src/blots/inline.ts",
    "content": "import { EmbedBlot, InlineBlot, Scope } from 'parchment';\nimport type { BlotConstructor } from 'parchment';\nimport Break from './break.js';\nimport Text from './text.js';\n\nclass Inline extends InlineBlot {\n  static allowedChildren: BlotConstructor[] = [Inline, Break, EmbedBlot, Text];\n  // Lower index means deeper in the DOM tree, since not found (-1) is for embeds\n  static order = [\n    'cursor',\n    'inline', // Must be lower\n    'link', // Chrome wants <a> to be lower\n    'underline',\n    'strike',\n    'italic',\n    'bold',\n    'script',\n    'code', // Must be higher\n  ];\n\n  static compare(self: string, other: string) {\n    const selfIndex = Inline.order.indexOf(self);\n    const otherIndex = Inline.order.indexOf(other);\n    if (selfIndex >= 0 || otherIndex >= 0) {\n      return selfIndex - otherIndex;\n    }\n    if (self === other) {\n      return 0;\n    }\n    if (self < other) {\n      return -1;\n    }\n    return 1;\n  }\n\n  formatAt(index: number, length: number, name: string, value: unknown) {\n    if (\n      Inline.compare(this.statics.blotName, name) < 0 &&\n      this.scroll.query(name, Scope.BLOT)\n    ) {\n      const blot = this.isolate(index, length);\n      if (value) {\n        blot.wrap(name, value);\n      }\n    } else {\n      super.formatAt(index, length, name, value);\n    }\n  }\n\n  optimize(context: { [key: string]: any }) {\n    super.optimize(context);\n    if (\n      this.parent instanceof Inline &&\n      Inline.compare(this.statics.blotName, this.parent.statics.blotName) > 0\n    ) {\n      const parent = this.parent.isolate(this.offset(), this.length());\n      // @ts-expect-error TODO: make isolate generic\n      this.moveChildren(parent);\n      parent.wrap(this);\n    }\n  }\n}\n\nexport default Inline;\n"
  },
  {
    "path": "packages/quill/src/blots/scroll.ts",
    "content": "import { ContainerBlot, LeafBlot, Scope, ScrollBlot } from 'parchment';\nimport type { Blot, Parent, EmbedBlot, ParentBlot, Registry } from 'parchment';\nimport Delta, { AttributeMap, Op } from 'quill-delta';\nimport Emitter from '../core/emitter.js';\nimport type { EmitterSource } from '../core/emitter.js';\nimport Block, { BlockEmbed, bubbleFormats } from './block.js';\nimport Break from './break.js';\nimport Container from './container.js';\n\ntype RenderBlock =\n  | {\n      type: 'blockEmbed';\n      attributes: AttributeMap;\n      key: string;\n      value: unknown;\n    }\n  | { type: 'block'; attributes: AttributeMap; delta: Delta };\n\nfunction isLine(blot: unknown): blot is Block | BlockEmbed {\n  return blot instanceof Block || blot instanceof BlockEmbed;\n}\n\ninterface UpdatableEmbed {\n  updateContent(change: unknown): void;\n}\n\nfunction isUpdatable(blot: Blot): blot is Blot & UpdatableEmbed {\n  return typeof (blot as unknown as any).updateContent === 'function';\n}\n\nclass Scroll extends ScrollBlot {\n  static blotName = 'scroll';\n  static className = 'ql-editor';\n  static tagName = 'DIV';\n  static defaultChild = Block;\n  static allowedChildren = [Block, BlockEmbed, Container];\n\n  emitter: Emitter;\n  batch: false | MutationRecord[];\n\n  constructor(\n    registry: Registry,\n    domNode: HTMLDivElement,\n    { emitter }: { emitter: Emitter },\n  ) {\n    super(registry, domNode);\n    this.emitter = emitter;\n    this.batch = false;\n    this.optimize();\n    this.enable();\n    this.domNode.addEventListener('dragstart', (e) => this.handleDragStart(e));\n  }\n\n  batchStart() {\n    if (!Array.isArray(this.batch)) {\n      this.batch = [];\n    }\n  }\n\n  batchEnd() {\n    if (!this.batch) return;\n    const mutations = this.batch;\n    this.batch = false;\n    this.update(mutations);\n  }\n\n  emitMount(blot: Blot) {\n    this.emitter.emit(Emitter.events.SCROLL_BLOT_MOUNT, blot);\n  }\n\n  emitUnmount(blot: Blot) {\n    this.emitter.emit(Emitter.events.SCROLL_BLOT_UNMOUNT, blot);\n  }\n\n  emitEmbedUpdate(blot: Blot, change: unknown) {\n    this.emitter.emit(Emitter.events.SCROLL_EMBED_UPDATE, blot, change);\n  }\n\n  deleteAt(index: number, length: number) {\n    const [first, offset] = this.line(index);\n    const [last] = this.line(index + length);\n    super.deleteAt(index, length);\n    if (last != null && first !== last && offset > 0) {\n      if (first instanceof BlockEmbed || last instanceof BlockEmbed) {\n        this.optimize();\n        return;\n      }\n      const ref =\n        last.children.head instanceof Break ? null : last.children.head;\n      // @ts-expect-error\n      first.moveChildren(last, ref);\n      // @ts-expect-error\n      first.remove();\n    }\n    this.optimize();\n  }\n\n  enable(enabled = true) {\n    this.domNode.setAttribute('contenteditable', enabled ? 'true' : 'false');\n  }\n\n  formatAt(index: number, length: number, format: string, value: unknown) {\n    super.formatAt(index, length, format, value);\n    this.optimize();\n  }\n\n  insertAt(index: number, value: string, def?: unknown) {\n    if (index >= this.length()) {\n      if (def == null || this.scroll.query(value, Scope.BLOCK) == null) {\n        const blot = this.scroll.create(this.statics.defaultChild.blotName);\n        this.appendChild(blot);\n        if (def == null && value.endsWith('\\n')) {\n          blot.insertAt(0, value.slice(0, -1), def);\n        } else {\n          blot.insertAt(0, value, def);\n        }\n      } else {\n        const embed = this.scroll.create(value, def);\n        this.appendChild(embed);\n      }\n    } else {\n      super.insertAt(index, value, def);\n    }\n    this.optimize();\n  }\n\n  insertBefore(blot: Blot, ref?: Blot | null) {\n    if (blot.statics.scope === Scope.INLINE_BLOT) {\n      const wrapper = this.scroll.create(\n        this.statics.defaultChild.blotName,\n      ) as Parent;\n      wrapper.appendChild(blot);\n      super.insertBefore(wrapper, ref);\n    } else {\n      super.insertBefore(blot, ref);\n    }\n  }\n\n  insertContents(index: number, delta: Delta) {\n    const renderBlocks = this.deltaToRenderBlocks(\n      delta.concat(new Delta().insert('\\n')),\n    );\n    const last = renderBlocks.pop();\n    if (last == null) return;\n\n    this.batchStart();\n\n    const first = renderBlocks.shift();\n    if (first) {\n      const shouldInsertNewlineChar =\n        first.type === 'block' &&\n        (first.delta.length() === 0 ||\n          (!this.descendant(BlockEmbed, index)[0] && index < this.length()));\n      const delta =\n        first.type === 'block'\n          ? first.delta\n          : new Delta().insert({ [first.key]: first.value });\n      insertInlineContents(this, index, delta);\n      const newlineCharLength = first.type === 'block' ? 1 : 0;\n      const lineEndIndex = index + delta.length() + newlineCharLength;\n      if (shouldInsertNewlineChar) {\n        this.insertAt(lineEndIndex - 1, '\\n');\n      }\n\n      const formats = bubbleFormats(this.line(index)[0]);\n      const attributes = AttributeMap.diff(formats, first.attributes) || {};\n      Object.keys(attributes).forEach((name) => {\n        this.formatAt(lineEndIndex - 1, 1, name, attributes[name]);\n      });\n\n      index = lineEndIndex;\n    }\n\n    let [refBlot, refBlotOffset] = this.children.find(index);\n    if (renderBlocks.length) {\n      if (refBlot) {\n        refBlot = refBlot.split(refBlotOffset);\n        refBlotOffset = 0;\n      }\n\n      renderBlocks.forEach((renderBlock) => {\n        if (renderBlock.type === 'block') {\n          const block = this.createBlock(\n            renderBlock.attributes,\n            refBlot || undefined,\n          );\n          insertInlineContents(block, 0, renderBlock.delta);\n        } else {\n          const blockEmbed = this.create(\n            renderBlock.key,\n            renderBlock.value,\n          ) as EmbedBlot;\n          this.insertBefore(blockEmbed, refBlot || undefined);\n          Object.keys(renderBlock.attributes).forEach((name) => {\n            blockEmbed.format(name, renderBlock.attributes[name]);\n          });\n        }\n      });\n    }\n\n    if (last.type === 'block' && last.delta.length()) {\n      const offset = refBlot\n        ? refBlot.offset(refBlot.scroll) + refBlotOffset\n        : this.length();\n      insertInlineContents(this, offset, last.delta);\n    }\n\n    this.batchEnd();\n    this.optimize();\n  }\n\n  isEnabled() {\n    return this.domNode.getAttribute('contenteditable') === 'true';\n  }\n\n  leaf(index: number): [LeafBlot | null, number] {\n    const last = this.path(index).pop();\n    if (!last) {\n      return [null, -1];\n    }\n\n    const [blot, offset] = last;\n    return blot instanceof LeafBlot ? [blot, offset] : [null, -1];\n  }\n\n  line(index: number): [Block | BlockEmbed | null, number] {\n    if (index === this.length()) {\n      return this.line(index - 1);\n    }\n    // @ts-expect-error TODO: make descendant() generic\n    return this.descendant(isLine, index);\n  }\n\n  lines(index = 0, length = Number.MAX_VALUE): (Block | BlockEmbed)[] {\n    const getLines = (\n      blot: ParentBlot,\n      blotIndex: number,\n      blotLength: number,\n    ) => {\n      let lines: (Block | BlockEmbed)[] = [];\n      let lengthLeft = blotLength;\n      blot.children.forEachAt(\n        blotIndex,\n        blotLength,\n        (child, childIndex, childLength) => {\n          if (isLine(child)) {\n            lines.push(child);\n          } else if (child instanceof ContainerBlot) {\n            lines = lines.concat(getLines(child, childIndex, lengthLeft));\n          }\n          lengthLeft -= childLength;\n        },\n      );\n      return lines;\n    };\n    return getLines(this, index, length);\n  }\n\n  optimize(context?: { [key: string]: any }): void;\n  optimize(\n    mutations?: MutationRecord[],\n    context?: { [key: string]: any },\n  ): void;\n  optimize(mutations = [], context = {}) {\n    if (this.batch) return;\n    super.optimize(mutations, context);\n    if (mutations.length > 0) {\n      this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context);\n    }\n  }\n\n  path(index: number) {\n    return super.path(index).slice(1); // Exclude self\n  }\n\n  remove() {\n    // Never remove self\n  }\n\n  update(source?: EmitterSource): void;\n  update(mutations?: MutationRecord[]): void;\n  update(mutations?: MutationRecord[] | EmitterSource): void {\n    if (this.batch) {\n      if (Array.isArray(mutations)) {\n        this.batch = this.batch.concat(mutations);\n      }\n      return;\n    }\n    let source: EmitterSource = Emitter.sources.USER;\n    if (typeof mutations === 'string') {\n      source = mutations;\n    }\n    if (!Array.isArray(mutations)) {\n      mutations = this.observer.takeRecords();\n    }\n    mutations = mutations.filter(({ target }) => {\n      const blot = this.find(target, true);\n      return blot && !isUpdatable(blot);\n    });\n    if (mutations.length > 0) {\n      this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations);\n    }\n    super.update(mutations.concat([])); // pass copy\n    if (mutations.length > 0) {\n      this.emitter.emit(Emitter.events.SCROLL_UPDATE, source, mutations);\n    }\n  }\n\n  updateEmbedAt(index: number, key: string, change: unknown) {\n    // Currently it only supports top-level embeds (BlockEmbed).\n    // We can update `ParentBlot` in parchment to support inline embeds.\n    const [blot] = this.descendant((b: Blot) => b instanceof BlockEmbed, index);\n    if (blot && blot.statics.blotName === key && isUpdatable(blot)) {\n      blot.updateContent(change);\n    }\n  }\n\n  protected handleDragStart(event: DragEvent) {\n    event.preventDefault();\n  }\n\n  private deltaToRenderBlocks(delta: Delta) {\n    const renderBlocks: RenderBlock[] = [];\n\n    let currentBlockDelta = new Delta();\n    delta.forEach((op) => {\n      const insert = op?.insert;\n      if (!insert) return;\n      if (typeof insert === 'string') {\n        const splitted = insert.split('\\n');\n        splitted.slice(0, -1).forEach((text) => {\n          currentBlockDelta.insert(text, op.attributes);\n          renderBlocks.push({\n            type: 'block',\n            delta: currentBlockDelta,\n            attributes: op.attributes ?? {},\n          });\n          currentBlockDelta = new Delta();\n        });\n        const last = splitted[splitted.length - 1];\n        if (last) {\n          currentBlockDelta.insert(last, op.attributes);\n        }\n      } else {\n        const key = Object.keys(insert)[0];\n        if (!key) return;\n        if (this.query(key, Scope.INLINE)) {\n          currentBlockDelta.push(op);\n        } else {\n          if (currentBlockDelta.length()) {\n            renderBlocks.push({\n              type: 'block',\n              delta: currentBlockDelta,\n              attributes: {},\n            });\n          }\n          currentBlockDelta = new Delta();\n          renderBlocks.push({\n            type: 'blockEmbed',\n            key,\n            value: insert[key],\n            attributes: op.attributes ?? {},\n          });\n        }\n      }\n    });\n\n    if (currentBlockDelta.length()) {\n      renderBlocks.push({\n        type: 'block',\n        delta: currentBlockDelta,\n        attributes: {},\n      });\n    }\n\n    return renderBlocks;\n  }\n\n  private createBlock(attributes: AttributeMap, refBlot?: Blot) {\n    let blotName: string | undefined;\n    const formats: AttributeMap = {};\n\n    Object.entries(attributes).forEach(([key, value]) => {\n      const isBlockBlot = this.query(key, Scope.BLOCK & Scope.BLOT) != null;\n      if (isBlockBlot) {\n        blotName = key;\n      } else {\n        formats[key] = value;\n      }\n    });\n\n    const block = this.create(\n      blotName || this.statics.defaultChild.blotName,\n      blotName ? attributes[blotName] : undefined,\n    ) as ParentBlot;\n\n    this.insertBefore(block, refBlot || undefined);\n\n    const length = block.length();\n    Object.entries(formats).forEach(([key, value]) => {\n      block.formatAt(0, length, key, value);\n    });\n\n    return block;\n  }\n}\n\nfunction insertInlineContents(\n  parent: ParentBlot,\n  index: number,\n  inlineContents: Delta,\n) {\n  inlineContents.reduce((index, op) => {\n    const length = Op.length(op);\n    let attributes = op.attributes || {};\n    if (op.insert != null) {\n      if (typeof op.insert === 'string') {\n        const text = op.insert;\n        parent.insertAt(index, text);\n        const [leaf] = parent.descendant(LeafBlot, index);\n        const formats = bubbleFormats(leaf);\n        attributes = AttributeMap.diff(formats, attributes) || {};\n      } else if (typeof op.insert === 'object') {\n        const key = Object.keys(op.insert)[0]; // There should only be one key\n        if (key == null) return index;\n        parent.insertAt(index, key, op.insert[key]);\n        const isInlineEmbed = parent.scroll.query(key, Scope.INLINE) != null;\n        if (isInlineEmbed) {\n          const [leaf] = parent.descendant(LeafBlot, index);\n          const formats = bubbleFormats(leaf);\n          attributes = AttributeMap.diff(formats, attributes) || {};\n        }\n      }\n    }\n    Object.keys(attributes).forEach((key) => {\n      parent.formatAt(index, length, key, attributes[key]);\n    });\n    return index + length;\n  }, index);\n}\n\nexport default Scroll;\n"
  },
  {
    "path": "packages/quill/src/blots/text.ts",
    "content": "import { TextBlot } from 'parchment';\n\nclass Text extends TextBlot {}\n\n// https://lodash.com/docs#escape\nconst entityMap: Record<string, string> = {\n  '&': '&amp;',\n  '<': '&lt;',\n  '>': '&gt;',\n  '\"': '&quot;',\n  \"'\": '&#39;',\n};\n\nfunction escapeText(text: string) {\n  return text.replace(/[&<>\"']/g, (s) => entityMap[s]);\n}\n\nexport { Text as default, escapeText };\n"
  },
  {
    "path": "packages/quill/src/core/composition.ts",
    "content": "import Embed from '../blots/embed.js';\nimport type Scroll from '../blots/scroll.js';\nimport Emitter from './emitter.js';\n\nclass Composition {\n  isComposing = false;\n\n  constructor(\n    private scroll: Scroll,\n    private emitter: Emitter,\n  ) {\n    this.setupListeners();\n  }\n\n  private setupListeners() {\n    this.scroll.domNode.addEventListener('compositionstart', (event) => {\n      if (!this.isComposing) {\n        this.handleCompositionStart(event);\n      }\n    });\n\n    this.scroll.domNode.addEventListener('compositionend', (event) => {\n      if (this.isComposing) {\n        // Webkit makes DOM changes after compositionend, so we use microtask to\n        // ensure the order.\n        // https://bugs.webkit.org/show_bug.cgi?id=31902\n        queueMicrotask(() => {\n          this.handleCompositionEnd(event);\n        });\n      }\n    });\n  }\n\n  private handleCompositionStart(event: CompositionEvent) {\n    const blot =\n      event.target instanceof Node\n        ? this.scroll.find(event.target, true)\n        : null;\n\n    if (blot && !(blot instanceof Embed)) {\n      this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_START, event);\n      this.scroll.batchStart();\n      this.emitter.emit(Emitter.events.COMPOSITION_START, event);\n      this.isComposing = true;\n    }\n  }\n\n  private handleCompositionEnd(event: CompositionEvent) {\n    this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_END, event);\n    this.scroll.batchEnd();\n    this.emitter.emit(Emitter.events.COMPOSITION_END, event);\n    this.isComposing = false;\n  }\n}\n\nexport default Composition;\n"
  },
  {
    "path": "packages/quill/src/core/editor.ts",
    "content": "import { cloneDeep, isEqual, merge } from 'lodash-es';\nimport { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment';\nimport type { Blot } from 'parchment';\nimport Delta, { AttributeMap, Op } from 'quill-delta';\nimport Block, { BlockEmbed, bubbleFormats } from '../blots/block.js';\nimport Break from '../blots/break.js';\nimport CursorBlot from '../blots/cursor.js';\nimport type Scroll from '../blots/scroll.js';\nimport TextBlot, { escapeText } from '../blots/text.js';\nimport { Range } from './selection.js';\n\nconst ASCII = /^[ -~]*$/;\n\ntype SelectionInfo = {\n  newRange: Range;\n  oldRange: Range;\n};\n\nclass Editor {\n  scroll: Scroll;\n  delta: Delta;\n\n  constructor(scroll: Scroll) {\n    this.scroll = scroll;\n    this.delta = this.getDelta();\n  }\n\n  applyDelta(delta: Delta): Delta {\n    this.scroll.update();\n    let scrollLength = this.scroll.length();\n    this.scroll.batchStart();\n    const normalizedDelta = normalizeDelta(delta);\n    const deleteDelta = new Delta();\n    const normalizedOps = splitOpLines(normalizedDelta.ops.slice());\n    normalizedOps.reduce((index, op) => {\n      const length = Op.length(op);\n      let attributes = op.attributes || {};\n      let isImplicitNewlinePrepended = false;\n      let isImplicitNewlineAppended = false;\n      if (op.insert != null) {\n        deleteDelta.retain(length);\n        if (typeof op.insert === 'string') {\n          const text = op.insert;\n          isImplicitNewlineAppended =\n            !text.endsWith('\\n') &&\n            (scrollLength <= index ||\n              !!this.scroll.descendant(BlockEmbed, index)[0]);\n          this.scroll.insertAt(index, text);\n          const [line, offset] = this.scroll.line(index);\n          let formats = merge({}, bubbleFormats(line));\n          if (line instanceof Block) {\n            const [leaf] = line.descendant(LeafBlot, offset);\n            if (leaf) {\n              formats = merge(formats, bubbleFormats(leaf));\n            }\n          }\n          attributes = AttributeMap.diff(formats, attributes) || {};\n        } else if (typeof op.insert === 'object') {\n          const key = Object.keys(op.insert)[0]; // There should only be one key\n          if (key == null) return index;\n          const isInlineEmbed = this.scroll.query(key, Scope.INLINE) != null;\n          if (isInlineEmbed) {\n            if (\n              scrollLength <= index ||\n              !!this.scroll.descendant(BlockEmbed, index)[0]\n            ) {\n              isImplicitNewlineAppended = true;\n            }\n          } else if (index > 0) {\n            const [leaf, offset] = this.scroll.descendant(LeafBlot, index - 1);\n            if (leaf instanceof TextBlot) {\n              const text = leaf.value();\n              if (text[offset] !== '\\n') {\n                isImplicitNewlinePrepended = true;\n              }\n            } else if (\n              leaf instanceof EmbedBlot &&\n              leaf.statics.scope === Scope.INLINE_BLOT\n            ) {\n              isImplicitNewlinePrepended = true;\n            }\n          }\n          this.scroll.insertAt(index, key, op.insert[key]);\n\n          if (isInlineEmbed) {\n            const [leaf] = this.scroll.descendant(LeafBlot, index);\n            if (leaf) {\n              const formats = merge({}, bubbleFormats(leaf));\n              attributes = AttributeMap.diff(formats, attributes) || {};\n            }\n          }\n        }\n        scrollLength += length;\n      } else {\n        deleteDelta.push(op);\n\n        if (op.retain !== null && typeof op.retain === 'object') {\n          const key = Object.keys(op.retain)[0];\n          if (key == null) return index;\n          this.scroll.updateEmbedAt(index, key, op.retain[key]);\n        }\n      }\n      Object.keys(attributes).forEach((name) => {\n        this.scroll.formatAt(index, length, name, attributes[name]);\n      });\n      const prependedLength = isImplicitNewlinePrepended ? 1 : 0;\n      const addedLength = isImplicitNewlineAppended ? 1 : 0;\n      scrollLength += prependedLength + addedLength;\n      deleteDelta.retain(prependedLength);\n      deleteDelta.delete(addedLength);\n      return index + length + prependedLength + addedLength;\n    }, 0);\n    deleteDelta.reduce((index, op) => {\n      if (typeof op.delete === 'number') {\n        this.scroll.deleteAt(index, op.delete);\n        return index;\n      }\n      return index + Op.length(op);\n    }, 0);\n    this.scroll.batchEnd();\n    this.scroll.optimize();\n    return this.update(normalizedDelta);\n  }\n\n  deleteText(index: number, length: number): Delta {\n    this.scroll.deleteAt(index, length);\n    return this.update(new Delta().retain(index).delete(length));\n  }\n\n  formatLine(\n    index: number,\n    length: number,\n    formats: Record<string, unknown> = {},\n  ): Delta {\n    this.scroll.update();\n    Object.keys(formats).forEach((format) => {\n      this.scroll.lines(index, Math.max(length, 1)).forEach((line) => {\n        line.format(format, formats[format]);\n      });\n    });\n    this.scroll.optimize();\n    const delta = new Delta().retain(index).retain(length, cloneDeep(formats));\n    return this.update(delta);\n  }\n\n  formatText(\n    index: number,\n    length: number,\n    formats: Record<string, unknown> = {},\n  ): Delta {\n    Object.keys(formats).forEach((format) => {\n      this.scroll.formatAt(index, length, format, formats[format]);\n    });\n    const delta = new Delta().retain(index).retain(length, cloneDeep(formats));\n    return this.update(delta);\n  }\n\n  getContents(index: number, length: number): Delta {\n    return this.delta.slice(index, index + length);\n  }\n\n  getDelta(): Delta {\n    return this.scroll.lines().reduce((delta, line) => {\n      return delta.concat(line.delta());\n    }, new Delta());\n  }\n\n  getFormat(index: number, length = 0): Record<string, unknown> {\n    let lines: (Block | BlockEmbed)[] = [];\n    let leaves: LeafBlot[] = [];\n    if (length === 0) {\n      this.scroll.path(index).forEach((path) => {\n        const [blot] = path;\n        if (blot instanceof Block) {\n          lines.push(blot);\n        } else if (blot instanceof LeafBlot) {\n          leaves.push(blot);\n        }\n      });\n    } else {\n      lines = this.scroll.lines(index, length);\n      leaves = this.scroll.descendants(LeafBlot, index, length);\n    }\n    const [lineFormats, leafFormats] = [lines, leaves].map((blots) => {\n      const blot = blots.shift();\n      if (blot == null) return {};\n      let formats = bubbleFormats(blot);\n      while (Object.keys(formats).length > 0) {\n        const blot = blots.shift();\n        if (blot == null) return formats;\n        formats = combineFormats(bubbleFormats(blot), formats);\n      }\n      return formats;\n    });\n    return { ...lineFormats, ...leafFormats };\n  }\n\n  getHTML(index: number, length: number): string {\n    const [line, lineOffset] = this.scroll.line(index);\n    if (line) {\n      const lineLength = line.length();\n      const isWithinLine = line.length() >= lineOffset + length;\n      if (isWithinLine && !(lineOffset === 0 && length === lineLength)) {\n        return convertHTML(line, lineOffset, length, true);\n      }\n      return convertHTML(this.scroll, index, length, true);\n    }\n    return '';\n  }\n\n  getText(index: number, length: number): string {\n    return this.getContents(index, length)\n      .filter((op) => typeof op.insert === 'string')\n      .map((op) => op.insert)\n      .join('');\n  }\n\n  insertContents(index: number, contents: Delta): Delta {\n    const normalizedDelta = normalizeDelta(contents);\n    const change = new Delta().retain(index).concat(normalizedDelta);\n    this.scroll.insertContents(index, normalizedDelta);\n    return this.update(change);\n  }\n\n  insertEmbed(index: number, embed: string, value: unknown): Delta {\n    this.scroll.insertAt(index, embed, value);\n    return this.update(new Delta().retain(index).insert({ [embed]: value }));\n  }\n\n  insertText(\n    index: number,\n    text: string,\n    formats: Record<string, unknown> = {},\n  ): Delta {\n    text = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n    this.scroll.insertAt(index, text);\n    Object.keys(formats).forEach((format) => {\n      this.scroll.formatAt(index, text.length, format, formats[format]);\n    });\n    return this.update(\n      new Delta().retain(index).insert(text, cloneDeep(formats)),\n    );\n  }\n\n  isBlank(): boolean {\n    if (this.scroll.children.length === 0) return true;\n    if (this.scroll.children.length > 1) return false;\n    const blot = this.scroll.children.head;\n    if (blot?.statics.blotName !== Block.blotName) return false;\n    const block = blot as Block;\n    if (block.children.length > 1) return false;\n    return block.children.head instanceof Break;\n  }\n\n  removeFormat(index: number, length: number): Delta {\n    const text = this.getText(index, length);\n    const [line, offset] = this.scroll.line(index + length);\n    let suffixLength = 0;\n    let suffix = new Delta();\n    if (line != null) {\n      suffixLength = line.length() - offset;\n      suffix = line\n        .delta()\n        .slice(offset, offset + suffixLength - 1)\n        .insert('\\n');\n    }\n    const contents = this.getContents(index, length + suffixLength);\n    const diff = contents.diff(new Delta().insert(text).concat(suffix));\n    const delta = new Delta().retain(index).concat(diff);\n    return this.applyDelta(delta);\n  }\n\n  update(\n    change: Delta | null,\n    mutations: MutationRecord[] = [],\n    selectionInfo: SelectionInfo | undefined = undefined,\n  ): Delta {\n    const oldDelta = this.delta;\n    if (\n      mutations.length === 1 &&\n      mutations[0].type === 'characterData' &&\n      // @ts-expect-error Fix me later\n      mutations[0].target.data.match(ASCII) &&\n      this.scroll.find(mutations[0].target)\n    ) {\n      // Optimization for character changes\n      const textBlot = this.scroll.find(mutations[0].target) as Blot;\n      const formats = bubbleFormats(textBlot);\n      const index = textBlot.offset(this.scroll);\n      // @ts-expect-error Fix me later\n      const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');\n      const oldText = new Delta().insert(oldValue);\n      // @ts-expect-error\n      const newText = new Delta().insert(textBlot.value());\n      const relativeSelectionInfo = selectionInfo && {\n        oldRange: shiftRange(selectionInfo.oldRange, -index),\n        newRange: shiftRange(selectionInfo.newRange, -index),\n      };\n      const diffDelta = new Delta()\n        .retain(index)\n        .concat(oldText.diff(newText, relativeSelectionInfo));\n      change = diffDelta.reduce((delta, op) => {\n        if (op.insert) {\n          return delta.insert(op.insert, formats);\n        }\n        return delta.push(op);\n      }, new Delta());\n      this.delta = oldDelta.compose(change);\n    } else {\n      this.delta = this.getDelta();\n      if (!change || !isEqual(oldDelta.compose(change), this.delta)) {\n        change = oldDelta.diff(this.delta, selectionInfo);\n      }\n    }\n    return change;\n  }\n}\n\ninterface ListItem {\n  child: Blot;\n  offset: number;\n  length: number;\n  indent: number;\n  type: string;\n}\nfunction convertListHTML(\n  items: ListItem[],\n  lastIndent: number,\n  types: string[],\n): string {\n  if (items.length === 0) {\n    const [endTag] = getListType(types.pop());\n    if (lastIndent <= 0) {\n      return `</li></${endTag}>`;\n    }\n    return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types)}`;\n  }\n  const [{ child, offset, length, indent, type }, ...rest] = items;\n  const [tag, attribute] = getListType(type);\n  if (indent > lastIndent) {\n    types.push(type);\n    if (indent === lastIndent + 1) {\n      return `<${tag}><li${attribute}>${convertHTML(\n        child,\n        offset,\n        length,\n      )}${convertListHTML(rest, indent, types)}`;\n    }\n    return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types)}`;\n  }\n  const previousType = types[types.length - 1];\n  if (indent === lastIndent && type === previousType) {\n    return `</li><li${attribute}>${convertHTML(\n      child,\n      offset,\n      length,\n    )}${convertListHTML(rest, indent, types)}`;\n  }\n  const [endTag] = getListType(types.pop());\n  return `</li></${endTag}>${convertListHTML(items, lastIndent - 1, types)}`;\n}\n\nfunction convertHTML(\n  blot: Blot,\n  index: number,\n  length: number,\n  isRoot = false,\n): string {\n  if ('html' in blot && typeof blot.html === 'function') {\n    return blot.html(index, length);\n  }\n  if (blot instanceof TextBlot) {\n    const escapedText = escapeText(blot.value().slice(index, index + length));\n    return escapedText.replaceAll(' ', '&nbsp;');\n  }\n  if (blot instanceof ParentBlot) {\n    // TODO fix API\n    if (blot.statics.blotName === 'list-container') {\n      const items: any[] = [];\n      blot.children.forEachAt(index, length, (child, offset, childLength) => {\n        const formats =\n          'formats' in child && typeof child.formats === 'function'\n            ? child.formats()\n            : {};\n        items.push({\n          child,\n          offset,\n          length: childLength,\n          indent: formats.indent || 0,\n          type: formats.list,\n        });\n      });\n      return convertListHTML(items, -1, []);\n    }\n    const parts: string[] = [];\n    blot.children.forEachAt(index, length, (child, offset, childLength) => {\n      parts.push(convertHTML(child, offset, childLength));\n    });\n    if (isRoot || blot.statics.blotName === 'list') {\n      return parts.join('');\n    }\n    const { outerHTML, innerHTML } = blot.domNode as Element;\n    const [start, end] = outerHTML.split(`>${innerHTML}<`);\n    // TODO cleanup\n    if (start === '<table') {\n      return `<table style=\"border: 1px solid #000;\">${parts.join('')}<${end}`;\n    }\n    return `${start}>${parts.join('')}<${end}`;\n  }\n  return blot.domNode instanceof Element ? blot.domNode.outerHTML : '';\n}\n\nfunction combineFormats(\n  formats: Record<string, unknown>,\n  combined: Record<string, unknown>,\n): Record<string, unknown> {\n  return Object.keys(combined).reduce(\n    (merged, name) => {\n      if (formats[name] == null) return merged;\n      const combinedValue = combined[name];\n      if (combinedValue === formats[name]) {\n        merged[name] = combinedValue;\n      } else if (Array.isArray(combinedValue)) {\n        if (combinedValue.indexOf(formats[name]) < 0) {\n          merged[name] = combinedValue.concat([formats[name]]);\n        } else {\n          // If style already exists, don't add to an array, but don't lose other styles\n          merged[name] = combinedValue;\n        }\n      } else {\n        merged[name] = [combinedValue, formats[name]];\n      }\n      return merged;\n    },\n    {} as Record<string, unknown>,\n  );\n}\n\nfunction getListType(type: string | undefined) {\n  const tag = type === 'ordered' ? 'ol' : 'ul';\n  switch (type) {\n    case 'checked':\n      return [tag, ' data-list=\"checked\"'];\n    case 'unchecked':\n      return [tag, ' data-list=\"unchecked\"'];\n    default:\n      return [tag, ''];\n  }\n}\n\nfunction normalizeDelta(delta: Delta) {\n  return delta.reduce((normalizedDelta, op) => {\n    if (typeof op.insert === 'string') {\n      const text = op.insert.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n      return normalizedDelta.insert(text, op.attributes);\n    }\n    return normalizedDelta.push(op);\n  }, new Delta());\n}\n\nfunction shiftRange({ index, length }: Range, amount: number) {\n  return new Range(index + amount, length);\n}\n\nfunction splitOpLines(ops: Op[]) {\n  const split: Op[] = [];\n  ops.forEach((op) => {\n    if (typeof op.insert === 'string') {\n      const lines = op.insert.split('\\n');\n      lines.forEach((line, index) => {\n        if (index) split.push({ insert: '\\n', attributes: op.attributes });\n        if (line) split.push({ insert: line, attributes: op.attributes });\n      });\n    } else {\n      split.push(op);\n    }\n  });\n\n  return split;\n}\n\nexport default Editor;\n"
  },
  {
    "path": "packages/quill/src/core/emitter.ts",
    "content": "import { EventEmitter } from 'eventemitter3';\nimport instances from './instances.js';\nimport logger from './logger.js';\n\nconst debug = logger('quill:events');\nconst EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click'];\n\nEVENTS.forEach((eventName) => {\n  document.addEventListener(eventName, (...args) => {\n    Array.from(document.querySelectorAll('.ql-container')).forEach((node) => {\n      const quill = instances.get(node);\n      if (quill && quill.emitter) {\n        quill.emitter.handleDOM(...args);\n      }\n    });\n  });\n});\n\nclass Emitter extends EventEmitter<string> {\n  static events = {\n    EDITOR_CHANGE: 'editor-change',\n    SCROLL_BEFORE_UPDATE: 'scroll-before-update',\n    SCROLL_BLOT_MOUNT: 'scroll-blot-mount',\n    SCROLL_BLOT_UNMOUNT: 'scroll-blot-unmount',\n    SCROLL_OPTIMIZE: 'scroll-optimize',\n    SCROLL_UPDATE: 'scroll-update',\n    SCROLL_EMBED_UPDATE: 'scroll-embed-update',\n    SELECTION_CHANGE: 'selection-change',\n    TEXT_CHANGE: 'text-change',\n    COMPOSITION_BEFORE_START: 'composition-before-start',\n    COMPOSITION_START: 'composition-start',\n    COMPOSITION_BEFORE_END: 'composition-before-end',\n    COMPOSITION_END: 'composition-end',\n  } as const;\n\n  static sources = {\n    API: 'api',\n    SILENT: 'silent',\n    USER: 'user',\n  } as const;\n\n  protected domListeners: Record<string, { node: Node; handler: Function }[]>;\n\n  constructor() {\n    super();\n    this.domListeners = {};\n    this.on('error', debug.error);\n  }\n\n  emit(...args: unknown[]): boolean {\n    debug.log.call(debug, ...args);\n    // @ts-expect-error\n    return super.emit(...args);\n  }\n\n  handleDOM(event: Event, ...args: unknown[]) {\n    (this.domListeners[event.type] || []).forEach(({ node, handler }) => {\n      if (event.target === node || node.contains(event.target as Node)) {\n        handler(event, ...args);\n      }\n    });\n  }\n\n  listenDOM(eventName: string, node: Node, handler: EventListener) {\n    if (!this.domListeners[eventName]) {\n      this.domListeners[eventName] = [];\n    }\n    this.domListeners[eventName].push({ node, handler });\n  }\n}\n\nexport type EmitterSource =\n  (typeof Emitter.sources)[keyof typeof Emitter.sources];\n\nexport default Emitter;\n"
  },
  {
    "path": "packages/quill/src/core/instances.ts",
    "content": "import type Quill from '../core.js';\n\nexport default new WeakMap<Node, Quill>();\n"
  },
  {
    "path": "packages/quill/src/core/logger.ts",
    "content": "const levels = ['error', 'warn', 'log', 'info'] as const;\nexport type DebugLevel = (typeof levels)[number];\nlet level: DebugLevel | false = 'warn';\n\nfunction debug(method: DebugLevel, ...args: unknown[]) {\n  if (level) {\n    if (levels.indexOf(method) <= levels.indexOf(level)) {\n      console[method](...args); // eslint-disable-line no-console\n    }\n  }\n}\n\nfunction namespace(\n  ns: string,\n): Record<DebugLevel, (...args: unknown[]) => void> {\n  return levels.reduce(\n    (logger, method) => {\n      logger[method] = debug.bind(console, method, ns);\n      return logger;\n    },\n    {} as Record<DebugLevel, (...args: unknown[]) => void>,\n  );\n}\n\nnamespace.level = (newLevel: DebugLevel | false) => {\n  level = newLevel;\n};\ndebug.level = namespace.level;\n\nexport default namespace;\n"
  },
  {
    "path": "packages/quill/src/core/module.ts",
    "content": "import type Quill from './quill.js';\n\nabstract class Module<T extends {} = {}> {\n  static DEFAULTS = {};\n\n  constructor(\n    public quill: Quill,\n    protected options: Partial<T> = {},\n  ) {}\n}\n\nexport default Module;\n"
  },
  {
    "path": "packages/quill/src/core/quill.ts",
    "content": "import { merge } from 'lodash-es';\nimport * as Parchment from 'parchment';\nimport type { Op } from 'quill-delta';\nimport Delta from 'quill-delta';\nimport type { BlockEmbed } from '../blots/block.js';\nimport type Block from '../blots/block.js';\nimport type Scroll from '../blots/scroll.js';\nimport type Clipboard from '../modules/clipboard.js';\nimport type History from '../modules/history.js';\nimport type Keyboard from '../modules/keyboard.js';\nimport type Uploader from '../modules/uploader.js';\nimport Editor from './editor.js';\nimport Emitter from './emitter.js';\nimport type { EmitterSource } from './emitter.js';\nimport instances from './instances.js';\nimport logger from './logger.js';\nimport type { DebugLevel } from './logger.js';\nimport Module from './module.js';\nimport Selection, { Range } from './selection.js';\nimport type { Bounds } from './selection.js';\nimport Composition from './composition.js';\nimport Theme from './theme.js';\nimport type { ThemeConstructor } from './theme.js';\nimport scrollRectIntoView from './utils/scrollRectIntoView.js';\nimport type {\n  Rect,\n  ScrollRectIntoViewOptions,\n} from './utils/scrollRectIntoView.js';\nimport createRegistryWithFormats from './utils/createRegistryWithFormats.js';\n\nconst debug = logger('quill');\n\nconst globalRegistry = new Parchment.Registry();\nParchment.ParentBlot.uiClass = 'ql-ui';\n\n/**\n * Options for initializing a Quill instance\n */\nexport interface QuillOptions {\n  theme?: string;\n  debug?: DebugLevel | boolean;\n  registry?: Parchment.Registry;\n  /**\n   * Whether to disable the editing\n   * @default false\n   */\n  readOnly?: boolean;\n\n  /**\n   * Placeholder text to display when the editor is empty\n   * @default \"\"\n   */\n  placeholder?: string;\n  bounds?: HTMLElement | string | null;\n  modules?: Record<string, unknown>;\n\n  /**\n   * A list of formats that are recognized and can exist within the editor contents.\n   * `null` means all formats are allowed.\n   * @default null\n   */\n  formats?: string[] | null;\n}\n\n/**\n * Similar to QuillOptions, but with all properties expanded to their default values,\n * and all selectors resolved to HTMLElements.\n */\nexport interface ExpandedQuillOptions\n  extends Omit<QuillOptions, 'theme' | 'formats'> {\n  theme: ThemeConstructor;\n  registry: Parchment.Registry;\n  container: HTMLElement;\n  modules: Record<string, unknown>;\n  bounds?: HTMLElement | null;\n  readOnly: boolean;\n}\n\nclass Quill {\n  static DEFAULTS = {\n    bounds: null,\n    modules: {\n      clipboard: true,\n      keyboard: true,\n      history: true,\n      uploader: true,\n    },\n    placeholder: '',\n    readOnly: false,\n    registry: globalRegistry,\n    theme: 'default',\n  } satisfies Partial<QuillOptions>;\n  static events = Emitter.events;\n  static sources = Emitter.sources;\n  static version = typeof QUILL_VERSION === 'undefined' ? 'dev' : QUILL_VERSION;\n\n  static imports: Record<string, unknown> = {\n    delta: Delta,\n    parchment: Parchment,\n    'core/module': Module,\n    'core/theme': Theme,\n  };\n\n  static debug(limit: DebugLevel | boolean) {\n    if (limit === true) {\n      limit = 'log';\n    }\n    logger.level(limit);\n  }\n\n  static find(node: Node, bubble = false) {\n    return instances.get(node) || globalRegistry.find(node, bubble);\n  }\n\n  static import(name: 'core/module'): typeof Module;\n  static import(name: `themes/${string}`): typeof Theme;\n  static import(name: 'parchment'): typeof Parchment;\n  static import(name: 'delta'): typeof Delta;\n  static import(name: string): unknown;\n  static import(name: string) {\n    if (this.imports[name] == null) {\n      debug.error(`Cannot import ${name}. Are you sure it was registered?`);\n    }\n    return this.imports[name];\n  }\n\n  static register(\n    targets: Record<\n      string,\n      | Parchment.RegistryDefinition\n      | Record<string, unknown> // any objects\n      | Theme\n      | Module\n      | Function // ES5 constructors\n    >,\n    overwrite?: boolean,\n  ): void;\n  static register(\n    target: Parchment.RegistryDefinition,\n    overwrite?: boolean,\n  ): void;\n  static register(path: string, target: any, overwrite?: boolean): void;\n  static register(...args: any[]): void {\n    if (typeof args[0] !== 'string') {\n      const target = args[0];\n      const overwrite = !!args[1];\n\n      const name = 'attrName' in target ? target.attrName : target.blotName;\n      if (typeof name === 'string') {\n        // Shortcut for formats:\n        // register(Blot | Attributor, overwrite)\n        this.register(`formats/${name}`, target, overwrite);\n      } else {\n        Object.keys(target).forEach((key) => {\n          this.register(key, target[key], overwrite);\n        });\n      }\n    } else {\n      const path = args[0];\n      const target = args[1];\n      const overwrite = !!args[2];\n\n      if (this.imports[path] != null && !overwrite) {\n        debug.warn(`Overwriting ${path} with`, target);\n      }\n      this.imports[path] = target;\n      if (\n        (path.startsWith('blots/') || path.startsWith('formats/')) &&\n        target &&\n        typeof target !== 'boolean' &&\n        target.blotName !== 'abstract'\n      ) {\n        globalRegistry.register(target);\n      }\n      if (typeof target.register === 'function') {\n        target.register(globalRegistry);\n      }\n    }\n  }\n\n  container: HTMLElement;\n  root: HTMLDivElement;\n  scroll: Scroll;\n  emitter: Emitter;\n  protected allowReadOnlyEdits: boolean;\n  editor: Editor;\n  composition: Composition;\n  selection: Selection;\n\n  theme: Theme;\n  keyboard: Keyboard;\n  clipboard: Clipboard;\n  history: History;\n  uploader: Uploader;\n\n  options: ExpandedQuillOptions;\n\n  constructor(container: HTMLElement | string, options: QuillOptions = {}) {\n    this.options = expandConfig(container, options);\n    this.container = this.options.container;\n    if (this.container == null) {\n      debug.error('Invalid Quill container', container);\n      return;\n    }\n    if (this.options.debug) {\n      Quill.debug(this.options.debug);\n    }\n    const html = this.container.innerHTML.trim();\n    this.container.classList.add('ql-container');\n    this.container.innerHTML = '';\n    instances.set(this.container, this);\n    this.root = this.addContainer('ql-editor');\n    this.root.classList.add('ql-blank');\n    this.emitter = new Emitter();\n    const scrollBlotName = Parchment.ScrollBlot.blotName;\n    const ScrollBlot = this.options.registry.query(scrollBlotName);\n    if (!ScrollBlot || !('blotName' in ScrollBlot)) {\n      throw new Error(\n        `Cannot initialize Quill without \"${scrollBlotName}\" blot`,\n      );\n    }\n    this.scroll = new ScrollBlot(this.options.registry, this.root, {\n      emitter: this.emitter,\n    }) as Scroll;\n    this.editor = new Editor(this.scroll);\n    this.selection = new Selection(this.scroll, this.emitter);\n    this.composition = new Composition(this.scroll, this.emitter);\n    this.theme = new this.options.theme(this, this.options); // eslint-disable-line new-cap\n    this.keyboard = this.theme.addModule('keyboard');\n    this.clipboard = this.theme.addModule('clipboard');\n    this.history = this.theme.addModule('history');\n    this.uploader = this.theme.addModule('uploader');\n    this.theme.addModule('input');\n    this.theme.addModule('uiNode');\n    this.theme.init();\n    this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {\n      if (type === Emitter.events.TEXT_CHANGE) {\n        this.root.classList.toggle('ql-blank', this.editor.isBlank());\n      }\n    });\n    this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {\n      const oldRange = this.selection.lastRange;\n      const [newRange] = this.selection.getRange();\n      const selectionInfo =\n        oldRange && newRange ? { oldRange, newRange } : undefined;\n      modify.call(\n        this,\n        () => this.editor.update(null, mutations, selectionInfo),\n        source,\n      );\n    });\n    this.emitter.on(Emitter.events.SCROLL_EMBED_UPDATE, (blot, delta) => {\n      const oldRange = this.selection.lastRange;\n      const [newRange] = this.selection.getRange();\n      const selectionInfo =\n        oldRange && newRange ? { oldRange, newRange } : undefined;\n      modify.call(\n        this,\n        () => {\n          const change = new Delta()\n            .retain(blot.offset(this))\n            .retain({ [blot.statics.blotName]: delta });\n          return this.editor.update(change, [], selectionInfo);\n        },\n        Quill.sources.USER,\n      );\n    });\n    if (html) {\n      const contents = this.clipboard.convert({\n        html: `${html}<p><br></p>`,\n        text: '\\n',\n      });\n      this.setContents(contents);\n    }\n    this.history.clear();\n    if (this.options.placeholder) {\n      this.root.setAttribute('data-placeholder', this.options.placeholder);\n    }\n    if (this.options.readOnly) {\n      this.disable();\n    }\n    this.allowReadOnlyEdits = false;\n  }\n\n  addContainer(container: string, refNode?: Node | null): HTMLDivElement;\n  addContainer(container: HTMLElement, refNode?: Node | null): HTMLElement;\n  addContainer(\n    container: string | HTMLElement,\n    refNode: Node | null = null,\n  ): HTMLDivElement | HTMLElement {\n    if (typeof container === 'string') {\n      const className = container;\n      container = document.createElement('div');\n      container.classList.add(className);\n    }\n    this.container.insertBefore(container, refNode);\n    return container;\n  }\n\n  blur() {\n    this.selection.setRange(null);\n  }\n\n  deleteText(range: Range, source?: EmitterSource): Delta;\n  deleteText(index: number, length: number, source?: EmitterSource): Delta;\n  deleteText(\n    index: number | Range,\n    length?: number | EmitterSource,\n    source?: EmitterSource,\n  ): Delta {\n    // @ts-expect-error\n    [index, length, , source] = overload(index, length, source);\n    return modify.call(\n      this,\n      () => {\n        return this.editor.deleteText(index, length);\n      },\n      source,\n      index,\n      -1 * length,\n    );\n  }\n\n  disable() {\n    this.enable(false);\n  }\n\n  editReadOnly<T>(modifier: () => T): T {\n    this.allowReadOnlyEdits = true;\n    const value = modifier();\n    this.allowReadOnlyEdits = false;\n    return value;\n  }\n\n  enable(enabled = true) {\n    this.scroll.enable(enabled);\n    this.container.classList.toggle('ql-disabled', !enabled);\n  }\n\n  focus(options: { preventScroll?: boolean } = {}) {\n    this.selection.focus();\n    if (!options.preventScroll) {\n      this.scrollSelectionIntoView();\n    }\n  }\n\n  format(\n    name: string,\n    value: unknown,\n    source: EmitterSource = Emitter.sources.API,\n  ): Delta {\n    return modify.call(\n      this,\n      () => {\n        const range = this.getSelection(true);\n        let change = new Delta();\n        if (range == null) return change;\n        if (this.scroll.query(name, Parchment.Scope.BLOCK)) {\n          change = this.editor.formatLine(range.index, range.length, {\n            [name]: value,\n          });\n        } else if (range.length === 0) {\n          this.selection.format(name, value);\n          return change;\n        } else {\n          change = this.editor.formatText(range.index, range.length, {\n            [name]: value,\n          });\n        }\n        this.setSelection(range, Emitter.sources.SILENT);\n        return change;\n      },\n      source,\n    );\n  }\n\n  formatLine(\n    index: number,\n    length: number,\n    formats: Record<string, unknown>,\n    source?: EmitterSource,\n  ): Delta;\n  formatLine(\n    index: number,\n    length: number,\n    name: string,\n    value?: unknown,\n    source?: EmitterSource,\n  ): Delta;\n  formatLine(\n    index: number,\n    length: number,\n    name: string | Record<string, unknown>,\n    value?: unknown | EmitterSource,\n    source?: EmitterSource,\n  ): Delta {\n    let formats: Record<string, unknown>;\n    // eslint-disable-next-line prefer-const\n    [index, length, formats, source] = overload(\n      index,\n      length,\n      // @ts-expect-error\n      name,\n      value,\n      source,\n    );\n    return modify.call(\n      this,\n      () => {\n        return this.editor.formatLine(index, length, formats);\n      },\n      source,\n      index,\n      0,\n    );\n  }\n\n  formatText(\n    range: Range,\n    name: string,\n    value: unknown,\n    source?: EmitterSource,\n  ): Delta;\n  formatText(\n    index: number,\n    length: number,\n    name: string,\n    value: unknown,\n    source?: EmitterSource,\n  ): Delta;\n  formatText(\n    index: number,\n    length: number,\n    formats: Record<string, unknown>,\n    source?: EmitterSource,\n  ): Delta;\n  formatText(\n    index: number | Range,\n    length: number | string,\n    name: string | unknown,\n    value?: unknown | EmitterSource,\n    source?: EmitterSource,\n  ): Delta {\n    let formats: Record<string, unknown>;\n    // eslint-disable-next-line prefer-const\n    [index, length, formats, source] = overload(\n      // @ts-expect-error\n      index,\n      length,\n      name,\n      value,\n      source,\n    );\n    return modify.call(\n      this,\n      () => {\n        return this.editor.formatText(index, length, formats);\n      },\n      source,\n      index,\n      0,\n    );\n  }\n\n  getBounds(index: number | Range, length = 0): Bounds | null {\n    let bounds: Bounds | null = null;\n    if (typeof index === 'number') {\n      bounds = this.selection.getBounds(index, length);\n    } else {\n      bounds = this.selection.getBounds(index.index, index.length);\n    }\n    if (!bounds) return null;\n    const containerBounds = this.container.getBoundingClientRect();\n    return {\n      bottom: bounds.bottom - containerBounds.top,\n      height: bounds.height,\n      left: bounds.left - containerBounds.left,\n      right: bounds.right - containerBounds.left,\n      top: bounds.top - containerBounds.top,\n      width: bounds.width,\n    };\n  }\n\n  getContents(index = 0, length = this.getLength() - index) {\n    [index, length] = overload(index, length);\n    return this.editor.getContents(index, length);\n  }\n\n  getFormat(index?: number, length?: number): { [format: string]: unknown };\n  getFormat(range?: Range): {\n    [format: string]: unknown;\n  };\n  getFormat(\n    index: Range | number = this.getSelection(true),\n    length = 0,\n  ): { [format: string]: unknown } {\n    if (typeof index === 'number') {\n      return this.editor.getFormat(index, length);\n    }\n    return this.editor.getFormat(index.index, index.length);\n  }\n\n  getIndex(blot: Parchment.Blot) {\n    return blot.offset(this.scroll);\n  }\n\n  getLength() {\n    return this.scroll.length();\n  }\n\n  getLeaf(index: number) {\n    return this.scroll.leaf(index);\n  }\n\n  getLine(index: number) {\n    return this.scroll.line(index);\n  }\n\n  getLines(range: Range): (Block | BlockEmbed)[];\n  getLines(index?: number, length?: number): (Block | BlockEmbed)[];\n  getLines(\n    index: Range | number = 0,\n    length = Number.MAX_VALUE,\n  ): (Block | BlockEmbed)[] {\n    if (typeof index !== 'number') {\n      return this.scroll.lines(index.index, index.length);\n    }\n    return this.scroll.lines(index, length);\n  }\n\n  getModule(name: string) {\n    return this.theme.modules[name];\n  }\n\n  getSelection(focus: true): Range;\n  getSelection(focus?: boolean): Range | null;\n  getSelection(focus = false): Range | null {\n    if (focus) this.focus();\n    this.update(); // Make sure we access getRange with editor in consistent state\n    return this.selection.getRange()[0];\n  }\n\n  getSemanticHTML(range: Range): string;\n  getSemanticHTML(index?: number, length?: number): string;\n  getSemanticHTML(index: Range | number = 0, length?: number) {\n    if (typeof index === 'number') {\n      length = length ?? this.getLength() - index;\n    }\n    // @ts-expect-error\n    [index, length] = overload(index, length);\n    return this.editor.getHTML(index, length);\n  }\n\n  getText(range?: Range): string;\n  getText(index?: number, length?: number): string;\n  getText(index: Range | number = 0, length?: number): string {\n    if (typeof index === 'number') {\n      length = length ?? this.getLength() - index;\n    }\n    // @ts-expect-error\n    [index, length] = overload(index, length);\n    return this.editor.getText(index, length);\n  }\n\n  hasFocus() {\n    return this.selection.hasFocus();\n  }\n\n  insertEmbed(\n    index: number,\n    embed: string,\n    value: unknown,\n    source: EmitterSource = Quill.sources.API,\n  ): Delta {\n    return modify.call(\n      this,\n      () => {\n        return this.editor.insertEmbed(index, embed, value);\n      },\n      source,\n      index,\n    );\n  }\n\n  insertText(index: number, text: string, source?: EmitterSource): Delta;\n  insertText(\n    index: number,\n    text: string,\n    formats: Record<string, unknown>,\n    source?: EmitterSource,\n  ): Delta;\n  insertText(\n    index: number,\n    text: string,\n    name: string,\n    value: unknown,\n    source?: EmitterSource,\n  ): Delta;\n  insertText(\n    index: number,\n    text: string,\n    name?: string | Record<string, unknown> | EmitterSource,\n    value?: unknown,\n    source?: EmitterSource,\n  ): Delta {\n    let formats: Record<string, unknown>;\n    // eslint-disable-next-line prefer-const\n    // @ts-expect-error\n    [index, , formats, source] = overload(index, 0, name, value, source);\n    return modify.call(\n      this,\n      () => {\n        return this.editor.insertText(index, text, formats);\n      },\n      source,\n      index,\n      text.length,\n    );\n  }\n\n  isEnabled() {\n    return this.scroll.isEnabled();\n  }\n\n  off(...args: Parameters<(typeof Emitter)['prototype']['off']>) {\n    return this.emitter.off(...args);\n  }\n\n  on(\n    event: (typeof Emitter)['events']['TEXT_CHANGE'],\n    handler: (delta: Delta, oldContent: Delta, source: EmitterSource) => void,\n  ): Emitter;\n  on(\n    event: (typeof Emitter)['events']['SELECTION_CHANGE'],\n    handler: (range: Range, oldRange: Range, source: EmitterSource) => void,\n  ): Emitter;\n  on(\n    event: (typeof Emitter)['events']['EDITOR_CHANGE'],\n    handler: (\n      ...args:\n        | [\n            (typeof Emitter)['events']['TEXT_CHANGE'],\n            Delta,\n            Delta,\n            EmitterSource,\n          ]\n        | [\n            (typeof Emitter)['events']['SELECTION_CHANGE'],\n            Range,\n            Range,\n            EmitterSource,\n          ]\n    ) => void,\n  ): Emitter;\n  on(event: string, ...args: unknown[]): Emitter;\n  on(...args: Parameters<(typeof Emitter)['prototype']['on']>): Emitter {\n    return this.emitter.on(...args);\n  }\n\n  once(...args: Parameters<(typeof Emitter)['prototype']['once']>) {\n    return this.emitter.once(...args);\n  }\n\n  removeFormat(index: number, length: number, source?: EmitterSource): Delta {\n    [index, length, , source] = overload(index, length, source);\n    return modify.call(\n      this,\n      () => {\n        return this.editor.removeFormat(index, length);\n      },\n      source,\n      index,\n    );\n  }\n\n  scrollRectIntoView(rect: Rect, options: ScrollRectIntoViewOptions = {}) {\n    scrollRectIntoView(this.root, rect, options);\n  }\n\n  /**\n   * @deprecated Use Quill#scrollSelectionIntoView() instead.\n   */\n  scrollIntoView() {\n    console.warn(\n      'Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead.',\n    );\n    this.scrollSelectionIntoView();\n  }\n\n  /**\n   * Scroll the current selection into the visible area.\n   * If the selection is already visible, no scrolling will occur.\n   */\n  scrollSelectionIntoView(options: ScrollRectIntoViewOptions = {}) {\n    const range = this.selection.lastRange;\n    const bounds = range && this.selection.getBounds(range.index, range.length);\n    if (bounds) {\n      this.scrollRectIntoView(bounds, options);\n    }\n  }\n\n  setContents(\n    delta: Delta | Op[],\n    source: EmitterSource = Emitter.sources.API,\n  ): Delta {\n    return modify.call(\n      this,\n      () => {\n        delta = new Delta(delta);\n        const length = this.getLength();\n        // Quill will set empty editor to \\n\n        const delete1 = this.editor.deleteText(0, length);\n        const applied = this.editor.insertContents(0, delta);\n        // Remove extra \\n from empty editor initialization\n        const delete2 = this.editor.deleteText(this.getLength() - 1, 1);\n        return delete1.compose(applied).compose(delete2);\n      },\n      source,\n    );\n  }\n  setSelection(range: Range | null, source?: EmitterSource): void;\n  setSelection(index: number, source?: EmitterSource): void;\n  setSelection(index: number, length?: number, source?: EmitterSource): void;\n  setSelection(index: number, source?: EmitterSource): void;\n  setSelection(\n    index: Range | null | number,\n    length?: EmitterSource | number,\n    source?: EmitterSource,\n  ): void {\n    if (index == null) {\n      // @ts-expect-error https://github.com/microsoft/TypeScript/issues/22609\n      this.selection.setRange(null, length || Quill.sources.API);\n    } else {\n      // @ts-expect-error\n      [index, length, , source] = overload(index, length, source);\n      this.selection.setRange(new Range(Math.max(0, index), length), source);\n      if (source !== Emitter.sources.SILENT) {\n        this.scrollSelectionIntoView();\n      }\n    }\n  }\n\n  setText(text: string, source: EmitterSource = Emitter.sources.API) {\n    const delta = new Delta().insert(text);\n    return this.setContents(delta, source);\n  }\n\n  update(source: EmitterSource = Emitter.sources.USER) {\n    const change = this.scroll.update(source); // Will update selection before selection.update() does if text changes\n    this.selection.update(source);\n    // TODO this is usually undefined\n    return change;\n  }\n\n  updateContents(\n    delta: Delta | Op[],\n    source: EmitterSource = Emitter.sources.API,\n  ): Delta {\n    return modify.call(\n      this,\n      () => {\n        delta = new Delta(delta);\n        return this.editor.applyDelta(delta);\n      },\n      source,\n      true,\n    );\n  }\n}\n\nfunction resolveSelector(selector: string | HTMLElement | null | undefined) {\n  return typeof selector === 'string'\n    ? document.querySelector<HTMLElement>(selector)\n    : selector;\n}\n\nfunction expandModuleConfig(config: Record<string, unknown> | undefined) {\n  return Object.entries(config ?? {}).reduce(\n    (expanded, [key, value]) => ({\n      ...expanded,\n      [key]: value === true ? {} : value,\n    }),\n    {} as Record<string, unknown>,\n  );\n}\n\nfunction omitUndefinedValuesFromOptions(obj: QuillOptions) {\n  return Object.fromEntries(\n    Object.entries(obj).filter((entry) => entry[1] !== undefined),\n  );\n}\n\nfunction expandConfig(\n  containerOrSelector: HTMLElement | string,\n  options: QuillOptions,\n): ExpandedQuillOptions {\n  const container = resolveSelector(containerOrSelector);\n  if (!container) {\n    throw new Error('Invalid Quill container');\n  }\n\n  const shouldUseDefaultTheme =\n    !options.theme || options.theme === Quill.DEFAULTS.theme;\n  const theme = shouldUseDefaultTheme\n    ? Theme\n    : Quill.import(`themes/${options.theme}`);\n  if (!theme) {\n    throw new Error(`Invalid theme ${options.theme}. Did you register it?`);\n  }\n\n  const { modules: quillModuleDefaults, ...quillDefaults } = Quill.DEFAULTS;\n  const { modules: themeModuleDefaults, ...themeDefaults } = theme.DEFAULTS;\n\n  let userModuleOptions = expandModuleConfig(options.modules);\n  // Special case toolbar shorthand\n  if (\n    userModuleOptions != null &&\n    userModuleOptions.toolbar &&\n    userModuleOptions.toolbar.constructor !== Object\n  ) {\n    userModuleOptions = {\n      ...userModuleOptions,\n      toolbar: { container: userModuleOptions.toolbar },\n    };\n  }\n\n  const modules: ExpandedQuillOptions['modules'] = merge(\n    {},\n    expandModuleConfig(quillModuleDefaults),\n    expandModuleConfig(themeModuleDefaults),\n    userModuleOptions,\n  );\n\n  const config = {\n    ...quillDefaults,\n    ...omitUndefinedValuesFromOptions(themeDefaults),\n    ...omitUndefinedValuesFromOptions(options),\n  };\n\n  let registry = options.registry;\n  if (registry) {\n    if (options.formats) {\n      debug.warn('Ignoring \"formats\" option because \"registry\" is specified');\n    }\n  } else {\n    registry = options.formats\n      ? createRegistryWithFormats(options.formats, config.registry, debug)\n      : config.registry;\n  }\n\n  return {\n    ...config,\n    registry,\n    container,\n    theme,\n    modules: Object.entries(modules).reduce(\n      (modulesWithDefaults, [name, value]) => {\n        if (!value) return modulesWithDefaults;\n\n        const moduleClass = Quill.import(`modules/${name}`);\n        if (moduleClass == null) {\n          debug.error(\n            `Cannot load ${name} module. Are you sure you registered it?`,\n          );\n          return modulesWithDefaults;\n        }\n        return {\n          ...modulesWithDefaults,\n          // @ts-expect-error\n          [name]: merge({}, moduleClass.DEFAULTS || {}, value),\n        };\n      },\n      {},\n    ),\n    bounds: resolveSelector(config.bounds),\n  };\n}\n\n// Handle selection preservation and TEXT_CHANGE emission\n// common to modification APIs\nfunction modify(\n  modifier: () => Delta,\n  source: EmitterSource,\n  index: number | boolean,\n  shift: number | null,\n) {\n  if (\n    !this.isEnabled() &&\n    source === Emitter.sources.USER &&\n    !this.allowReadOnlyEdits\n  ) {\n    return new Delta();\n  }\n  let range = index == null ? null : this.getSelection();\n  const oldDelta = this.editor.delta;\n  const change = modifier();\n  if (range != null) {\n    if (index === true) {\n      index = range.index; // eslint-disable-line prefer-destructuring\n    }\n    if (shift == null) {\n      range = shiftRange(range, change, source);\n    } else if (shift !== 0) {\n      // @ts-expect-error index should always be number\n      range = shiftRange(range, index, shift, source);\n    }\n    this.setSelection(range, Emitter.sources.SILENT);\n  }\n  if (change.length() > 0) {\n    const args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source];\n    this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);\n    if (source !== Emitter.sources.SILENT) {\n      this.emitter.emit(...args);\n    }\n  }\n  return change;\n}\n\ntype NormalizedIndexLength = [\n  number,\n  number,\n  Record<string, unknown>,\n  EmitterSource,\n];\nfunction overload(index: number, source?: EmitterSource): NormalizedIndexLength;\nfunction overload(\n  index: number,\n  length: number,\n  source?: EmitterSource,\n): NormalizedIndexLength;\nfunction overload(\n  index: number,\n  length: number,\n  format: string,\n  value: unknown,\n  source?: EmitterSource,\n): NormalizedIndexLength;\nfunction overload(\n  index: number,\n  length: number,\n  format: Record<string, unknown>,\n  source?: EmitterSource,\n): NormalizedIndexLength;\nfunction overload(range: Range, source?: EmitterSource): NormalizedIndexLength;\nfunction overload(\n  range: Range,\n  format: string,\n  value: unknown,\n  source?: EmitterSource,\n): NormalizedIndexLength;\nfunction overload(\n  range: Range,\n  format: Record<string, unknown>,\n  source?: EmitterSource,\n): NormalizedIndexLength;\nfunction overload(\n  index: Range | number,\n  length?: number | string | Record<string, unknown> | EmitterSource,\n  name?: string | unknown | Record<string, unknown> | EmitterSource,\n  value?: unknown | EmitterSource,\n  source?: EmitterSource,\n): NormalizedIndexLength {\n  let formats: Record<string, unknown> = {};\n  // @ts-expect-error\n  if (typeof index.index === 'number' && typeof index.length === 'number') {\n    // Allow for throwaway end (used by insertText/insertEmbed)\n    if (typeof length !== 'number') {\n      // @ts-expect-error\n      source = value;\n      value = name;\n      name = length;\n      // @ts-expect-error\n      length = index.length; // eslint-disable-line prefer-destructuring\n      // @ts-expect-error\n      index = index.index; // eslint-disable-line prefer-destructuring\n    } else {\n      // @ts-expect-error\n      length = index.length; // eslint-disable-line prefer-destructuring\n      // @ts-expect-error\n      index = index.index; // eslint-disable-line prefer-destructuring\n    }\n  } else if (typeof length !== 'number') {\n    // @ts-expect-error\n    source = value;\n    value = name;\n    name = length;\n    length = 0;\n  }\n  // Handle format being object, two format name/value strings or excluded\n  if (typeof name === 'object') {\n    // @ts-expect-error Fix me later\n    formats = name;\n    // @ts-expect-error\n    source = value;\n  } else if (typeof name === 'string') {\n    if (value != null) {\n      formats[name] = value;\n    } else {\n      // @ts-expect-error\n      source = name;\n    }\n  }\n  // Handle optional source\n  source = source || Emitter.sources.API;\n  // @ts-expect-error\n  return [index, length, formats, source];\n}\n\nfunction shiftRange(range: Range, change: Delta, source?: EmitterSource): Range;\nfunction shiftRange(\n  range: Range,\n  index: number,\n  length?: number,\n  source?: EmitterSource,\n): Range;\nfunction shiftRange(\n  range: Range,\n  index: number | Delta,\n  lengthOrSource?: number | EmitterSource,\n  source?: EmitterSource,\n) {\n  const length = typeof lengthOrSource === 'number' ? lengthOrSource : 0;\n  if (range == null) return null;\n  let start;\n  let end;\n  // @ts-expect-error -- TODO: add a better type guard around `index`\n  if (index && typeof index.transformPosition === 'function') {\n    [start, end] = [range.index, range.index + range.length].map((pos) =>\n      // @ts-expect-error -- TODO: add a better type guard around `index`\n      index.transformPosition(pos, source !== Emitter.sources.USER),\n    );\n  } else {\n    [start, end] = [range.index, range.index + range.length].map((pos) => {\n      // @ts-expect-error -- TODO: add a better type guard around `index`\n      if (pos < index || (pos === index && source === Emitter.sources.USER))\n        return pos;\n      if (length >= 0) {\n        return pos + length;\n      }\n      // @ts-expect-error -- TODO: add a better type guard around `index`\n      return Math.max(index, pos + length);\n    });\n  }\n  return new Range(start, end - start);\n}\n\nexport type { Bounds, DebugLevel, EmitterSource };\nexport { Parchment, Range };\n\nexport { globalRegistry, expandConfig, overload, Quill as default };\n"
  },
  {
    "path": "packages/quill/src/core/selection.ts",
    "content": "import { LeafBlot, Scope } from 'parchment';\nimport { cloneDeep, isEqual } from 'lodash-es';\nimport Emitter from './emitter.js';\nimport type { EmitterSource } from './emitter.js';\nimport logger from './logger.js';\nimport type Cursor from '../blots/cursor.js';\nimport type Scroll from '../blots/scroll.js';\n\nconst debug = logger('quill:selection');\n\ntype NativeRange = AbstractRange;\n\ninterface NormalizedRange {\n  start: {\n    node: NativeRange['startContainer'];\n    offset: NativeRange['startOffset'];\n  };\n  end: { node: NativeRange['endContainer']; offset: NativeRange['endOffset'] };\n  native: NativeRange;\n}\n\nexport interface Bounds {\n  bottom: number;\n  height: number;\n  left: number;\n  right: number;\n  top: number;\n  width: number;\n}\n\nexport class Range {\n  constructor(\n    public index: number,\n    public length = 0,\n  ) {}\n}\n\nclass Selection {\n  scroll: Scroll;\n  emitter: Emitter;\n  composing: boolean;\n  mouseDown: boolean;\n\n  root: HTMLElement;\n  cursor: Cursor;\n  savedRange: Range;\n  lastRange: Range | null;\n  lastNative: NormalizedRange | null;\n\n  constructor(scroll: Scroll, emitter: Emitter) {\n    this.emitter = emitter;\n    this.scroll = scroll;\n    this.composing = false;\n    this.mouseDown = false;\n    this.root = this.scroll.domNode;\n    // @ts-expect-error\n    this.cursor = this.scroll.create('cursor', this);\n    // savedRange is last non-null range\n    this.savedRange = new Range(0, 0);\n    this.lastRange = this.savedRange;\n    this.lastNative = null;\n    this.handleComposition();\n    this.handleDragging();\n    this.emitter.listenDOM('selectionchange', document, () => {\n      if (!this.mouseDown && !this.composing) {\n        setTimeout(this.update.bind(this, Emitter.sources.USER), 1);\n      }\n    });\n    this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => {\n      if (!this.hasFocus()) return;\n      const native = this.getNativeRange();\n      if (native == null) return;\n      if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle\n      this.emitter.once(\n        Emitter.events.SCROLL_UPDATE,\n        (source, mutations: MutationRecord[]) => {\n          try {\n            if (\n              this.root.contains(native.start.node) &&\n              this.root.contains(native.end.node)\n            ) {\n              this.setNativeRange(\n                native.start.node,\n                native.start.offset,\n                native.end.node,\n                native.end.offset,\n              );\n            }\n            const triggeredByTyping = mutations.some(\n              (mutation) =>\n                mutation.type === 'characterData' ||\n                mutation.type === 'childList' ||\n                (mutation.type === 'attributes' &&\n                  mutation.target === this.root),\n            );\n            this.update(triggeredByTyping ? Emitter.sources.SILENT : source);\n          } catch (ignored) {\n            // ignore\n          }\n        },\n      );\n    });\n    this.emitter.on(Emitter.events.SCROLL_OPTIMIZE, (mutations, context) => {\n      if (context.range) {\n        const { startNode, startOffset, endNode, endOffset } = context.range;\n        this.setNativeRange(startNode, startOffset, endNode, endOffset);\n        this.update(Emitter.sources.SILENT);\n      }\n    });\n    this.update(Emitter.sources.SILENT);\n  }\n\n  handleComposition() {\n    this.emitter.on(Emitter.events.COMPOSITION_BEFORE_START, () => {\n      this.composing = true;\n    });\n    this.emitter.on(Emitter.events.COMPOSITION_END, () => {\n      this.composing = false;\n      if (this.cursor.parent) {\n        const range = this.cursor.restore();\n        if (!range) return;\n        setTimeout(() => {\n          this.setNativeRange(\n            range.startNode,\n            range.startOffset,\n            range.endNode,\n            range.endOffset,\n          );\n        }, 1);\n      }\n    });\n  }\n\n  handleDragging() {\n    this.emitter.listenDOM('mousedown', document.body, () => {\n      this.mouseDown = true;\n    });\n    this.emitter.listenDOM('mouseup', document.body, () => {\n      this.mouseDown = false;\n      this.update(Emitter.sources.USER);\n    });\n  }\n\n  focus() {\n    if (this.hasFocus()) return;\n    this.root.focus({ preventScroll: true });\n    this.setRange(this.savedRange);\n  }\n\n  format(format: string, value: unknown) {\n    this.scroll.update();\n    const nativeRange = this.getNativeRange();\n    if (\n      nativeRange == null ||\n      !nativeRange.native.collapsed ||\n      this.scroll.query(format, Scope.BLOCK)\n    )\n      return;\n    if (nativeRange.start.node !== this.cursor.textNode) {\n      const blot = this.scroll.find(nativeRange.start.node, false);\n      if (blot == null) return;\n      // TODO Give blot ability to not split\n      if (blot instanceof LeafBlot) {\n        const after = blot.split(nativeRange.start.offset);\n        blot.parent.insertBefore(this.cursor, after);\n      } else {\n        // @ts-expect-error TODO: nativeRange.start.node doesn't seem to match function signature\n        blot.insertBefore(this.cursor, nativeRange.start.node); // Should never happen\n      }\n      this.cursor.attach();\n    }\n    this.cursor.format(format, value);\n    this.scroll.optimize();\n    this.setNativeRange(this.cursor.textNode, this.cursor.textNode.data.length);\n    this.update();\n  }\n\n  getBounds(index: number, length = 0) {\n    const scrollLength = this.scroll.length();\n    index = Math.min(index, scrollLength - 1);\n    length = Math.min(index + length, scrollLength - 1) - index;\n    let node: Node;\n    let [leaf, offset] = this.scroll.leaf(index);\n    if (leaf == null) return null;\n    if (length > 0 && offset === leaf.length()) {\n      const [next] = this.scroll.leaf(index + 1);\n      if (next) {\n        const [line] = this.scroll.line(index);\n        const [nextLine] = this.scroll.line(index + 1);\n        if (line === nextLine) {\n          leaf = next;\n          offset = 0;\n        }\n      }\n    }\n    [node, offset] = leaf.position(offset, true);\n    const range = document.createRange();\n    if (length > 0) {\n      range.setStart(node, offset);\n      [leaf, offset] = this.scroll.leaf(index + length);\n      if (leaf == null) return null;\n      [node, offset] = leaf.position(offset, true);\n      range.setEnd(node, offset);\n      return range.getBoundingClientRect();\n    }\n    let side: 'left' | 'right' = 'left';\n    let rect: DOMRect;\n    if (node instanceof Text) {\n      // Return null if the text node is empty because it is\n      // not able to get a useful client rect:\n      // https://github.com/w3c/csswg-drafts/issues/2514.\n      // Empty text nodes are most likely caused by TextBlot#optimize()\n      // not getting called when editor content changes.\n      if (!node.data.length) {\n        return null;\n      }\n      if (offset < node.data.length) {\n        range.setStart(node, offset);\n        range.setEnd(node, offset + 1);\n      } else {\n        range.setStart(node, offset - 1);\n        range.setEnd(node, offset);\n        side = 'right';\n      }\n      rect = range.getBoundingClientRect();\n    } else {\n      if (!(leaf.domNode instanceof Element)) return null;\n      rect = leaf.domNode.getBoundingClientRect();\n      if (offset > 0) side = 'right';\n    }\n    return {\n      bottom: rect.top + rect.height,\n      height: rect.height,\n      left: rect[side],\n      right: rect[side],\n      top: rect.top,\n      width: 0,\n    };\n  }\n\n  getNativeRange(): NormalizedRange | null {\n    const selection = document.getSelection();\n    if (selection == null || selection.rangeCount <= 0) return null;\n    const nativeRange = selection.getRangeAt(0);\n    if (nativeRange == null) return null;\n    const range = this.normalizeNative(nativeRange);\n    debug.info('getNativeRange', range);\n    return range;\n  }\n\n  getRange(): [Range, NormalizedRange] | [null, null] {\n    const root = this.scroll.domNode;\n    if ('isConnected' in root && !root.isConnected) {\n      // document.getSelection() forces layout on Blink, so we trend to\n      // not calling it.\n      return [null, null];\n    }\n    const normalized = this.getNativeRange();\n    if (normalized == null) return [null, null];\n    const range = this.normalizedToRange(normalized);\n    return [range, normalized];\n  }\n\n  hasFocus(): boolean {\n    return (\n      document.activeElement === this.root ||\n      (document.activeElement != null &&\n        contains(this.root, document.activeElement))\n    );\n  }\n\n  normalizedToRange(range: NormalizedRange) {\n    const positions: [Node, number][] = [\n      [range.start.node, range.start.offset],\n    ];\n    if (!range.native.collapsed) {\n      positions.push([range.end.node, range.end.offset]);\n    }\n    const indexes = positions.map((position) => {\n      const [node, offset] = position;\n      const blot = this.scroll.find(node, true);\n      // @ts-expect-error Fix me later\n      const index = blot.offset(this.scroll);\n      if (offset === 0) {\n        return index;\n      }\n      if (blot instanceof LeafBlot) {\n        return index + blot.index(node, offset);\n      }\n      // @ts-expect-error Fix me later\n      return index + blot.length();\n    });\n    const end = Math.min(Math.max(...indexes), this.scroll.length() - 1);\n    const start = Math.min(end, ...indexes);\n    return new Range(start, end - start);\n  }\n\n  normalizeNative(nativeRange: NativeRange) {\n    if (\n      !contains(this.root, nativeRange.startContainer) ||\n      (!nativeRange.collapsed && !contains(this.root, nativeRange.endContainer))\n    ) {\n      return null;\n    }\n    const range = {\n      start: {\n        node: nativeRange.startContainer,\n        offset: nativeRange.startOffset,\n      },\n      end: { node: nativeRange.endContainer, offset: nativeRange.endOffset },\n      native: nativeRange,\n    };\n    [range.start, range.end].forEach((position) => {\n      let { node, offset } = position;\n      while (!(node instanceof Text) && node.childNodes.length > 0) {\n        if (node.childNodes.length > offset) {\n          node = node.childNodes[offset];\n          offset = 0;\n        } else if (node.childNodes.length === offset) {\n          // @ts-expect-error Fix me later\n          node = node.lastChild;\n          if (node instanceof Text) {\n            offset = node.data.length;\n          } else if (node.childNodes.length > 0) {\n            // Container case\n            offset = node.childNodes.length;\n          } else {\n            // Embed case\n            offset = node.childNodes.length + 1;\n          }\n        } else {\n          break;\n        }\n      }\n      position.node = node;\n      position.offset = offset;\n    });\n    return range;\n  }\n\n  rangeToNative(range: Range): [Node | null, number, Node | null, number] {\n    const scrollLength = this.scroll.length();\n\n    const getPosition = (\n      index: number,\n      inclusive: boolean,\n    ): [Node | null, number] => {\n      index = Math.min(scrollLength - 1, index);\n      const [leaf, leafOffset] = this.scroll.leaf(index);\n      return leaf ? leaf.position(leafOffset, inclusive) : [null, -1];\n    };\n    return [\n      ...getPosition(range.index, false),\n      ...getPosition(range.index + range.length, true),\n    ];\n  }\n\n  setNativeRange(\n    startNode: Node | null,\n    startOffset?: number,\n    endNode = startNode,\n    endOffset = startOffset,\n    force = false,\n  ) {\n    debug.info('setNativeRange', startNode, startOffset, endNode, endOffset);\n    if (\n      startNode != null &&\n      (this.root.parentNode == null ||\n        startNode.parentNode == null ||\n        // @ts-expect-error Fix me later\n        endNode.parentNode == null)\n    ) {\n      return;\n    }\n    const selection = document.getSelection();\n    if (selection == null) return;\n    if (startNode != null) {\n      if (!this.hasFocus()) this.root.focus({ preventScroll: true });\n      const { native } = this.getNativeRange() || {};\n      if (\n        native == null ||\n        force ||\n        startNode !== native.startContainer ||\n        startOffset !== native.startOffset ||\n        endNode !== native.endContainer ||\n        endOffset !== native.endOffset\n      ) {\n        if (startNode instanceof Element && startNode.tagName === 'BR') {\n          // @ts-expect-error Fix me later\n          startOffset = Array.from(startNode.parentNode.childNodes).indexOf(\n            startNode,\n          );\n          startNode = startNode.parentNode;\n        }\n        if (endNode instanceof Element && endNode.tagName === 'BR') {\n          // @ts-expect-error Fix me later\n          endOffset = Array.from(endNode.parentNode.childNodes).indexOf(\n            endNode,\n          );\n          endNode = endNode.parentNode;\n        }\n        const range = document.createRange();\n        // @ts-expect-error Fix me later\n        range.setStart(startNode, startOffset);\n        // @ts-expect-error Fix me later\n        range.setEnd(endNode, endOffset);\n        selection.removeAllRanges();\n        selection.addRange(range);\n      }\n    } else {\n      selection.removeAllRanges();\n      this.root.blur();\n    }\n  }\n\n  setRange(range: Range | null, force: boolean, source?: EmitterSource): void;\n  setRange(range: Range | null, source?: EmitterSource): void;\n  setRange(\n    range: Range | null,\n    force: boolean | EmitterSource = false,\n    source: EmitterSource = Emitter.sources.API,\n  ): void {\n    if (typeof force === 'string') {\n      source = force;\n      force = false;\n    }\n    debug.info('setRange', range);\n    if (range != null) {\n      const args = this.rangeToNative(range);\n      this.setNativeRange(...args, force);\n    } else {\n      this.setNativeRange(null);\n    }\n    this.update(source);\n  }\n\n  update(source: EmitterSource = Emitter.sources.USER) {\n    const oldRange = this.lastRange;\n    const [lastRange, nativeRange] = this.getRange();\n    this.lastRange = lastRange;\n    this.lastNative = nativeRange;\n    if (this.lastRange != null) {\n      this.savedRange = this.lastRange;\n    }\n    if (!isEqual(oldRange, this.lastRange)) {\n      if (\n        !this.composing &&\n        nativeRange != null &&\n        nativeRange.native.collapsed &&\n        nativeRange.start.node !== this.cursor.textNode\n      ) {\n        const range = this.cursor.restore();\n        if (range) {\n          this.setNativeRange(\n            range.startNode,\n            range.startOffset,\n            range.endNode,\n            range.endOffset,\n          );\n        }\n      }\n      const args = [\n        Emitter.events.SELECTION_CHANGE,\n        cloneDeep(this.lastRange),\n        cloneDeep(oldRange),\n        source,\n      ];\n      this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);\n      if (source !== Emitter.sources.SILENT) {\n        this.emitter.emit(...args);\n      }\n    }\n  }\n}\n\nfunction contains(parent: Node, descendant: Node) {\n  try {\n    // Firefox inserts inaccessible nodes around video elements\n    descendant.parentNode; // eslint-disable-line @typescript-eslint/no-unused-expressions\n  } catch (e) {\n    return false;\n  }\n  return parent.contains(descendant);\n}\n\nexport default Selection;\n"
  },
  {
    "path": "packages/quill/src/core/theme.ts",
    "content": "import type Quill from '../core.js';\nimport type Clipboard from '../modules/clipboard.js';\nimport type History from '../modules/history.js';\nimport type Keyboard from '../modules/keyboard.js';\nimport type { ToolbarProps } from '../modules/toolbar.js';\nimport type Uploader from '../modules/uploader.js';\n\nexport interface ThemeOptions {\n  modules: Record<string, unknown> & {\n    toolbar?: null | ToolbarProps;\n  };\n}\n\nclass Theme {\n  static DEFAULTS: ThemeOptions = {\n    modules: {},\n  };\n\n  static themes = {\n    default: Theme,\n  };\n\n  modules: ThemeOptions['modules'] = {};\n\n  constructor(\n    protected quill: Quill,\n    protected options: ThemeOptions,\n  ) {}\n\n  init() {\n    Object.keys(this.options.modules).forEach((name) => {\n      if (this.modules[name] == null) {\n        this.addModule(name);\n      }\n    });\n  }\n\n  addModule(name: 'clipboard'): Clipboard;\n  addModule(name: 'keyboard'): Keyboard;\n  addModule(name: 'uploader'): Uploader;\n  addModule(name: 'history'): History;\n  addModule(name: string): unknown;\n  addModule(name: string) {\n    // @ts-expect-error\n    const ModuleClass = this.quill.constructor.import(`modules/${name}`);\n    this.modules[name] = new ModuleClass(\n      this.quill,\n      this.options.modules[name] || {},\n    );\n    return this.modules[name];\n  }\n}\n\nexport interface ThemeConstructor {\n  new (quill: Quill, options: unknown): Theme;\n  DEFAULTS: ThemeOptions;\n}\n\nexport default Theme;\n"
  },
  {
    "path": "packages/quill/src/core/utils/createRegistryWithFormats.ts",
    "content": "import { Registry } from 'parchment';\n\nconst MAX_REGISTER_ITERATIONS = 100;\nconst CORE_FORMATS = ['block', 'break', 'cursor', 'inline', 'scroll', 'text'];\n\nconst createRegistryWithFormats = (\n  formats: string[],\n  sourceRegistry: Registry,\n  debug: { error: (errorMessage: string) => void },\n) => {\n  const registry = new Registry();\n  CORE_FORMATS.forEach((name) => {\n    const coreBlot = sourceRegistry.query(name);\n    if (coreBlot) registry.register(coreBlot);\n  });\n\n  formats.forEach((name) => {\n    let format = sourceRegistry.query(name);\n    if (!format) {\n      debug.error(\n        `Cannot register \"${name}\" specified in \"formats\" config. Are you sure it was registered?`,\n      );\n    }\n    let iterations = 0;\n    while (format) {\n      registry.register(format);\n      format = 'blotName' in format ? format.requiredContainer ?? null : null;\n\n      iterations += 1;\n      if (iterations > MAX_REGISTER_ITERATIONS) {\n        debug.error(\n          `Cycle detected in registering blot requiredContainer: \"${name}\"`,\n        );\n        break;\n      }\n    }\n  });\n\n  return registry;\n};\n\nexport default createRegistryWithFormats;\n"
  },
  {
    "path": "packages/quill/src/core/utils/scrollRectIntoView.ts",
    "content": "export type Rect = {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n};\n\nconst getParentElement = (element: Node): Element | null =>\n  element.parentElement || (element.getRootNode() as ShadowRoot).host || null;\n\nconst getElementRect = (element: Element): Rect => {\n  const rect = element.getBoundingClientRect();\n  const scaleX =\n    ('offsetWidth' in element &&\n      Math.abs(rect.width) / (element as HTMLElement).offsetWidth) ||\n    1;\n  const scaleY =\n    ('offsetHeight' in element &&\n      Math.abs(rect.height) / (element as HTMLElement).offsetHeight) ||\n    1;\n  return {\n    top: rect.top,\n    right: rect.left + element.clientWidth * scaleX,\n    bottom: rect.top + element.clientHeight * scaleY,\n    left: rect.left,\n  };\n};\n\nconst paddingValueToInt = (value: string) => {\n  const number = parseInt(value, 10);\n  return Number.isNaN(number) ? 0 : number;\n};\n\n// Follow the steps described in https://www.w3.org/TR/cssom-view-1/#element-scrolling-members,\n// assuming that the scroll option is set to 'nearest'.\nconst getScrollDistance = (\n  targetStart: number,\n  targetEnd: number,\n  scrollStart: number,\n  scrollEnd: number,\n  scrollPaddingStart: number,\n  scrollPaddingEnd: number,\n) => {\n  if (targetStart < scrollStart && targetEnd > scrollEnd) {\n    return 0;\n  }\n\n  if (targetStart < scrollStart) {\n    return -(scrollStart - targetStart + scrollPaddingStart);\n  }\n\n  if (targetEnd > scrollEnd) {\n    return targetEnd - targetStart > scrollEnd - scrollStart\n      ? targetStart + scrollPaddingStart - scrollStart\n      : targetEnd - scrollEnd + scrollPaddingEnd;\n  }\n  return 0;\n};\n\ninterface ScrollOffsetRecord {\n  element: Element | Window;\n  left: number;\n  top: number;\n}\n\nexport interface ScrollRectIntoViewOptions {\n  smooth?: boolean;\n}\n\nconst scrollRectIntoView = (\n  root: HTMLElement,\n  targetRect: Rect,\n  options: ScrollRectIntoViewOptions = {},\n) => {\n  const document = root.ownerDocument;\n\n  let rect = targetRect;\n\n  const records: ScrollOffsetRecord[] = [];\n\n  let current: Element | null = root;\n  while (current) {\n    const isDocumentBody: boolean = current === document.body;\n    const bounding = isDocumentBody\n      ? {\n          top: 0,\n          right:\n            window.visualViewport?.width ??\n            document.documentElement.clientWidth,\n          bottom:\n            window.visualViewport?.height ??\n            document.documentElement.clientHeight,\n          left: 0,\n        }\n      : getElementRect(current);\n\n    const style = getComputedStyle(current);\n    const scrollDistanceX = getScrollDistance(\n      rect.left,\n      rect.right,\n      bounding.left,\n      bounding.right,\n      paddingValueToInt(style.scrollPaddingLeft),\n      paddingValueToInt(style.scrollPaddingRight),\n    );\n    const scrollDistanceY = getScrollDistance(\n      rect.top,\n      rect.bottom,\n      bounding.top,\n      bounding.bottom,\n      paddingValueToInt(style.scrollPaddingTop),\n      paddingValueToInt(style.scrollPaddingBottom),\n    );\n    if (scrollDistanceX || scrollDistanceY) {\n      if (isDocumentBody) {\n        if (document.defaultView) {\n          records.push({\n            element: document.defaultView,\n            left: scrollDistanceX,\n            top: scrollDistanceY,\n          });\n          document.defaultView.scrollBy(scrollDistanceX, scrollDistanceY);\n        }\n      } else {\n        const { scrollLeft, scrollTop } = current;\n        if (scrollDistanceY) {\n          current.scrollTop += scrollDistanceY;\n        }\n        if (scrollDistanceX) {\n          current.scrollLeft += scrollDistanceX;\n        }\n\n        records.push({\n          element: current,\n          left: scrollDistanceX,\n          top: scrollDistanceY,\n        });\n\n        const scrolledLeft = current.scrollLeft - scrollLeft;\n        const scrolledTop = current.scrollTop - scrollTop;\n        rect = {\n          left: rect.left - scrolledLeft,\n          top: rect.top - scrolledTop,\n          right: rect.right - scrolledLeft,\n          bottom: rect.bottom - scrolledTop,\n        };\n      }\n    }\n\n    current =\n      isDocumentBody || style.position === 'fixed'\n        ? null\n        : getParentElement(current);\n  }\n\n  if (options.smooth) {\n    // Revert all the changes in the scroll position\n    // and then scroll to the target position with smooth animation\n    records.forEach(({ element, top, left }) => {\n      element.scrollBy({ top: -top, left: -left, behavior: 'instant' });\n      element.scrollBy({ top, left, behavior: 'smooth' });\n    });\n  }\n};\n\nexport default scrollRectIntoView;\n"
  },
  {
    "path": "packages/quill/src/core.ts",
    "content": "import Quill, { Parchment, Range } from './core/quill.js';\nimport type {\n  Bounds,\n  DebugLevel,\n  EmitterSource,\n  ExpandedQuillOptions,\n  QuillOptions,\n} from './core/quill.js';\n\nimport Block, { BlockEmbed } from './blots/block.js';\nimport Break from './blots/break.js';\nimport Container from './blots/container.js';\nimport Cursor from './blots/cursor.js';\nimport Embed from './blots/embed.js';\nimport Inline from './blots/inline.js';\nimport Scroll from './blots/scroll.js';\nimport TextBlot from './blots/text.js';\n\nimport Clipboard from './modules/clipboard.js';\nimport History from './modules/history.js';\nimport Keyboard from './modules/keyboard.js';\nimport Uploader from './modules/uploader.js';\nimport Delta, { Op, OpIterator, AttributeMap } from 'quill-delta';\nimport Input from './modules/input.js';\nimport UINode from './modules/uiNode.js';\n\nexport { default as Module } from './core/module.js';\nexport { Delta, Op, OpIterator, AttributeMap, Parchment, Range };\nexport type {\n  Bounds,\n  DebugLevel,\n  EmitterSource,\n  ExpandedQuillOptions,\n  QuillOptions,\n};\n\nQuill.register({\n  'blots/block': Block,\n  'blots/block/embed': BlockEmbed,\n  'blots/break': Break,\n  'blots/container': Container,\n  'blots/cursor': Cursor,\n  'blots/embed': Embed,\n  'blots/inline': Inline,\n  'blots/scroll': Scroll,\n  'blots/text': TextBlot,\n\n  'modules/clipboard': Clipboard,\n  'modules/history': History,\n  'modules/keyboard': Keyboard,\n  'modules/uploader': Uploader,\n  'modules/input': Input,\n  'modules/uiNode': UINode,\n});\n\nexport default Quill;\n"
  },
  {
    "path": "packages/quill/src/formats/align.ts",
    "content": "import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst config = {\n  scope: Scope.BLOCK,\n  whitelist: ['right', 'center', 'justify'],\n};\n\nconst AlignAttribute = new Attributor('align', 'align', config);\nconst AlignClass = new ClassAttributor('align', 'ql-align', config);\nconst AlignStyle = new StyleAttributor('align', 'text-align', config);\n\nexport { AlignAttribute, AlignClass, AlignStyle };\n"
  },
  {
    "path": "packages/quill/src/formats/background.ts",
    "content": "import { ClassAttributor, Scope } from 'parchment';\nimport { ColorAttributor } from './color.js';\n\nconst BackgroundClass = new ClassAttributor('background', 'ql-bg', {\n  scope: Scope.INLINE,\n});\nconst BackgroundStyle = new ColorAttributor('background', 'background-color', {\n  scope: Scope.INLINE,\n});\n\nexport { BackgroundClass, BackgroundStyle };\n"
  },
  {
    "path": "packages/quill/src/formats/blockquote.ts",
    "content": "import Block from '../blots/block.js';\n\nclass Blockquote extends Block {\n  static blotName = 'blockquote';\n  static tagName = 'blockquote';\n}\n\nexport default Blockquote;\n"
  },
  {
    "path": "packages/quill/src/formats/bold.ts",
    "content": "import Inline from '../blots/inline.js';\n\nclass Bold extends Inline {\n  static blotName = 'bold';\n  static tagName = ['STRONG', 'B'];\n\n  static create() {\n    return super.create();\n  }\n\n  static formats() {\n    return true;\n  }\n\n  optimize(context: { [key: string]: any }) {\n    super.optimize(context);\n    if (this.domNode.tagName !== this.statics.tagName[0]) {\n      this.replaceWith(this.statics.blotName);\n    }\n  }\n}\n\nexport default Bold;\n"
  },
  {
    "path": "packages/quill/src/formats/code.ts",
    "content": "import Block from '../blots/block.js';\nimport Break from '../blots/break.js';\nimport Cursor from '../blots/cursor.js';\nimport Inline from '../blots/inline.js';\nimport TextBlot, { escapeText } from '../blots/text.js';\nimport Container from '../blots/container.js';\nimport Quill from '../core/quill.js';\n\nclass CodeBlockContainer extends Container {\n  static create(value: string) {\n    const domNode = super.create(value) as Element;\n    domNode.setAttribute('spellcheck', 'false');\n    return domNode;\n  }\n\n  code(index: number, length: number) {\n    return (\n      this.children\n        // @ts-expect-error\n        .map((child) => (child.length() <= 1 ? '' : child.domNode.innerText))\n        .join('\\n')\n        .slice(index, index + length)\n    );\n  }\n\n  html(index: number, length: number) {\n    // `\\n`s are needed in order to support empty lines at the beginning and the end.\n    // https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions\n    return `<pre>\\n${escapeText(this.code(index, length))}\\n</pre>`;\n  }\n}\n\nclass CodeBlock extends Block {\n  static TAB = '  ';\n\n  static register() {\n    Quill.register(CodeBlockContainer);\n  }\n}\n\nclass Code extends Inline {}\nCode.blotName = 'code';\nCode.tagName = 'CODE';\n\nCodeBlock.blotName = 'code-block';\nCodeBlock.className = 'ql-code-block';\nCodeBlock.tagName = 'DIV';\nCodeBlockContainer.blotName = 'code-block-container';\nCodeBlockContainer.className = 'ql-code-block-container';\nCodeBlockContainer.tagName = 'DIV';\n\nCodeBlockContainer.allowedChildren = [CodeBlock];\n\nCodeBlock.allowedChildren = [TextBlot, Break, Cursor];\nCodeBlock.requiredContainer = CodeBlockContainer;\n\nexport { Code, CodeBlockContainer, CodeBlock as default };\n"
  },
  {
    "path": "packages/quill/src/formats/color.ts",
    "content": "import { ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nclass ColorAttributor extends StyleAttributor {\n  value(domNode: HTMLElement) {\n    let value = super.value(domNode) as string;\n    if (!value.startsWith('rgb(')) return value;\n    value = value.replace(/^[^\\d]+/, '').replace(/[^\\d]+$/, '');\n    const hex = value\n      .split(',')\n      .map((component) => `00${parseInt(component, 10).toString(16)}`.slice(-2))\n      .join('');\n    return `#${hex}`;\n  }\n}\n\nconst ColorClass = new ClassAttributor('color', 'ql-color', {\n  scope: Scope.INLINE,\n});\nconst ColorStyle = new ColorAttributor('color', 'color', {\n  scope: Scope.INLINE,\n});\n\nexport { ColorAttributor, ColorClass, ColorStyle };\n"
  },
  {
    "path": "packages/quill/src/formats/direction.ts",
    "content": "import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst config = {\n  scope: Scope.BLOCK,\n  whitelist: ['rtl'],\n};\n\nconst DirectionAttribute = new Attributor('direction', 'dir', config);\nconst DirectionClass = new ClassAttributor('direction', 'ql-direction', config);\nconst DirectionStyle = new StyleAttributor('direction', 'direction', config);\n\nexport { DirectionAttribute, DirectionClass, DirectionStyle };\n"
  },
  {
    "path": "packages/quill/src/formats/font.ts",
    "content": "import { ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst config = {\n  scope: Scope.INLINE,\n  whitelist: ['serif', 'monospace'],\n};\n\nconst FontClass = new ClassAttributor('font', 'ql-font', config);\n\nclass FontStyleAttributor extends StyleAttributor {\n  value(node: HTMLElement) {\n    return super.value(node).replace(/[\"']/g, '');\n  }\n}\n\nconst FontStyle = new FontStyleAttributor('font', 'font-family', config);\n\nexport { FontStyle, FontClass };\n"
  },
  {
    "path": "packages/quill/src/formats/formula.ts",
    "content": "import Embed from '../blots/embed.js';\n\nclass Formula extends Embed {\n  static blotName = 'formula';\n  static className = 'ql-formula';\n  static tagName = 'SPAN';\n\n  static create(value: string) {\n    // @ts-expect-error\n    if (window.katex == null) {\n      throw new Error('Formula module requires KaTeX.');\n    }\n    const node = super.create(value) as Element;\n    if (typeof value === 'string') {\n      // @ts-expect-error\n      window.katex.render(value, node, {\n        throwOnError: false,\n        errorColor: '#f00',\n      });\n      node.setAttribute('data-value', value);\n    }\n    return node;\n  }\n\n  static value(domNode: Element) {\n    return domNode.getAttribute('data-value');\n  }\n\n  html() {\n    const { formula } = this.value();\n    return `<span>${formula}</span>`;\n  }\n}\n\nexport default Formula;\n"
  },
  {
    "path": "packages/quill/src/formats/header.ts",
    "content": "import Block from '../blots/block.js';\n\nclass Header extends Block {\n  static blotName = 'header';\n  static tagName = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];\n\n  static formats(domNode: Element) {\n    return this.tagName.indexOf(domNode.tagName) + 1;\n  }\n}\n\nexport default Header;\n"
  },
  {
    "path": "packages/quill/src/formats/image.ts",
    "content": "import { EmbedBlot } from 'parchment';\nimport { sanitize } from './link.js';\n\nconst ATTRIBUTES = ['alt', 'height', 'width'];\n\nclass Image extends EmbedBlot {\n  static blotName = 'image';\n  static tagName = 'IMG';\n\n  static create(value: string) {\n    const node = super.create(value) as Element;\n    if (typeof value === 'string') {\n      node.setAttribute('src', this.sanitize(value));\n    }\n    return node;\n  }\n\n  static formats(domNode: Element) {\n    return ATTRIBUTES.reduce(\n      (formats: Record<string, string | null>, attribute) => {\n        if (domNode.hasAttribute(attribute)) {\n          formats[attribute] = domNode.getAttribute(attribute);\n        }\n        return formats;\n      },\n      {},\n    );\n  }\n\n  static match(url: string) {\n    return /\\.(jpe?g|gif|png)$/.test(url) || /^data:image\\/.+;base64/.test(url);\n  }\n\n  static sanitize(url: string) {\n    return sanitize(url, ['http', 'https', 'data']) ? url : '//:0';\n  }\n\n  static value(domNode: Element) {\n    return domNode.getAttribute('src');\n  }\n\n  domNode: HTMLImageElement;\n\n  format(name: string, value: string) {\n    if (ATTRIBUTES.indexOf(name) > -1) {\n      if (value) {\n        this.domNode.setAttribute(name, value);\n      } else {\n        this.domNode.removeAttribute(name);\n      }\n    } else {\n      super.format(name, value);\n    }\n  }\n}\n\nexport default Image;\n"
  },
  {
    "path": "packages/quill/src/formats/indent.ts",
    "content": "import { ClassAttributor, Scope } from 'parchment';\n\nclass IndentAttributor extends ClassAttributor {\n  add(node: HTMLElement, value: string | number) {\n    let normalizedValue = 0;\n    if (value === '+1' || value === '-1') {\n      const indent = this.value(node) || 0;\n      normalizedValue = value === '+1' ? indent + 1 : indent - 1;\n    } else if (typeof value === 'number') {\n      normalizedValue = value;\n    }\n    if (normalizedValue === 0) {\n      this.remove(node);\n      return true;\n    }\n    return super.add(node, normalizedValue.toString());\n  }\n\n  canAdd(node: HTMLElement, value: string) {\n    return super.canAdd(node, value) || super.canAdd(node, parseInt(value, 10));\n  }\n\n  value(node: HTMLElement) {\n    return parseInt(super.value(node), 10) || undefined; // Don't return NaN\n  }\n}\n\nconst IndentClass = new IndentAttributor('indent', 'ql-indent', {\n  scope: Scope.BLOCK,\n  // @ts-expect-error\n  whitelist: [1, 2, 3, 4, 5, 6, 7, 8],\n});\n\nexport default IndentClass;\n"
  },
  {
    "path": "packages/quill/src/formats/italic.ts",
    "content": "import Bold from './bold.js';\n\nclass Italic extends Bold {\n  static blotName = 'italic';\n  static tagName = ['EM', 'I'];\n}\n\nexport default Italic;\n"
  },
  {
    "path": "packages/quill/src/formats/link.ts",
    "content": "import Inline from '../blots/inline.js';\n\nclass Link extends Inline {\n  static blotName = 'link';\n  static tagName = 'A';\n  static SANITIZED_URL = 'about:blank';\n  static PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel', 'sms'];\n\n  static create(value: string) {\n    const node = super.create(value) as HTMLElement;\n    node.setAttribute('href', this.sanitize(value));\n    node.setAttribute('rel', 'noopener noreferrer');\n    node.setAttribute('target', '_blank');\n    return node;\n  }\n\n  static formats(domNode: HTMLElement) {\n    return domNode.getAttribute('href');\n  }\n\n  static sanitize(url: string) {\n    return sanitize(url, this.PROTOCOL_WHITELIST) ? url : this.SANITIZED_URL;\n  }\n\n  format(name: string, value: unknown) {\n    if (name !== this.statics.blotName || !value) {\n      super.format(name, value);\n    } else {\n      // @ts-expect-error\n      this.domNode.setAttribute('href', this.constructor.sanitize(value));\n    }\n  }\n}\n\nfunction sanitize(url: string, protocols: string[]) {\n  const anchor = document.createElement('a');\n  anchor.href = url;\n  const protocol = anchor.href.slice(0, anchor.href.indexOf(':'));\n  return protocols.indexOf(protocol) > -1;\n}\n\nexport { Link as default, sanitize };\n"
  },
  {
    "path": "packages/quill/src/formats/list.ts",
    "content": "import Block from '../blots/block.js';\nimport Container from '../blots/container.js';\nimport type Scroll from '../blots/scroll.js';\nimport Quill from '../core/quill.js';\n\nclass ListContainer extends Container {}\nListContainer.blotName = 'list-container';\nListContainer.tagName = 'OL';\n\nclass ListItem extends Block {\n  static create(value: string) {\n    const node = super.create() as HTMLElement;\n    node.setAttribute('data-list', value);\n    return node;\n  }\n\n  static formats(domNode: HTMLElement) {\n    return domNode.getAttribute('data-list') || undefined;\n  }\n\n  static register() {\n    Quill.register(ListContainer);\n  }\n\n  constructor(scroll: Scroll, domNode: HTMLElement) {\n    super(scroll, domNode);\n    const ui = domNode.ownerDocument.createElement('span');\n    const listEventHandler = (e: Event) => {\n      if (!scroll.isEnabled()) return;\n      const format = this.statics.formats(domNode, scroll);\n      if (format === 'checked') {\n        this.format('list', 'unchecked');\n        e.preventDefault();\n      } else if (format === 'unchecked') {\n        this.format('list', 'checked');\n        e.preventDefault();\n      }\n    };\n    ui.addEventListener('mousedown', listEventHandler);\n    ui.addEventListener('touchstart', listEventHandler);\n    this.attachUI(ui);\n  }\n\n  format(name: string, value: string) {\n    if (name === this.statics.blotName && value) {\n      this.domNode.setAttribute('data-list', value);\n    } else {\n      super.format(name, value);\n    }\n  }\n}\nListItem.blotName = 'list';\nListItem.tagName = 'LI';\n\nListContainer.allowedChildren = [ListItem];\nListItem.requiredContainer = ListContainer;\n\nexport { ListContainer, ListItem as default };\n"
  },
  {
    "path": "packages/quill/src/formats/script.ts",
    "content": "import Inline from '../blots/inline.js';\n\nclass Script extends Inline {\n  static blotName = 'script';\n  static tagName = ['SUB', 'SUP'];\n\n  static create(value: 'super' | 'sub' | (string & {})) {\n    if (value === 'super') {\n      return document.createElement('sup');\n    }\n    if (value === 'sub') {\n      return document.createElement('sub');\n    }\n    return super.create(value);\n  }\n\n  static formats(domNode: HTMLElement) {\n    if (domNode.tagName === 'SUB') return 'sub';\n    if (domNode.tagName === 'SUP') return 'super';\n    return undefined;\n  }\n}\n\nexport default Script;\n"
  },
  {
    "path": "packages/quill/src/formats/size.ts",
    "content": "import { ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst SizeClass = new ClassAttributor('size', 'ql-size', {\n  scope: Scope.INLINE,\n  whitelist: ['small', 'large', 'huge'],\n});\nconst SizeStyle = new StyleAttributor('size', 'font-size', {\n  scope: Scope.INLINE,\n  whitelist: ['10px', '18px', '32px'],\n});\n\nexport { SizeClass, SizeStyle };\n"
  },
  {
    "path": "packages/quill/src/formats/strike.ts",
    "content": "import Bold from './bold.js';\n\nclass Strike extends Bold {\n  static blotName = 'strike';\n  static tagName = ['S', 'STRIKE'];\n}\n\nexport default Strike;\n"
  },
  {
    "path": "packages/quill/src/formats/table.ts",
    "content": "import type { LinkedList } from 'parchment';\nimport Block from '../blots/block.js';\nimport Container from '../blots/container.js';\n\nclass TableCell extends Block {\n  static blotName = 'table';\n  static tagName = 'TD';\n\n  static create(value: string) {\n    const node = super.create() as HTMLElement;\n    if (value) {\n      node.setAttribute('data-row', value);\n    } else {\n      node.setAttribute('data-row', tableId());\n    }\n    return node;\n  }\n\n  static formats(domNode: HTMLElement) {\n    if (domNode.hasAttribute('data-row')) {\n      return domNode.getAttribute('data-row');\n    }\n    return undefined;\n  }\n\n  next: this | null;\n\n  cellOffset() {\n    if (this.parent) {\n      return this.parent.children.indexOf(this);\n    }\n    return -1;\n  }\n\n  format(name: string, value: string) {\n    if (name === TableCell.blotName && value) {\n      this.domNode.setAttribute('data-row', value);\n    } else {\n      super.format(name, value);\n    }\n  }\n\n  row(): TableRow {\n    return this.parent as TableRow;\n  }\n\n  rowOffset() {\n    if (this.row()) {\n      return this.row().rowOffset();\n    }\n    return -1;\n  }\n\n  table() {\n    return this.row() && this.row().table();\n  }\n}\n\nclass TableRow extends Container {\n  static blotName = 'table-row';\n  static tagName = 'TR';\n\n  children: LinkedList<TableCell>;\n  next: this | null;\n\n  checkMerge() {\n    // @ts-expect-error\n    if (super.checkMerge() && this.next.children.head != null) {\n      // @ts-expect-error\n      const thisHead = this.children.head.formats();\n      // @ts-expect-error\n      const thisTail = this.children.tail.formats();\n      // @ts-expect-error\n      const nextHead = this.next.children.head.formats();\n      // @ts-expect-error\n      const nextTail = this.next.children.tail.formats();\n      return (\n        thisHead.table === thisTail.table &&\n        thisHead.table === nextHead.table &&\n        thisHead.table === nextTail.table\n      );\n    }\n    return false;\n  }\n\n  optimize(context: { [key: string]: any }) {\n    super.optimize(context);\n    this.children.forEach((child) => {\n      if (child.next == null) return;\n      const childFormats = child.formats();\n      const nextFormats = child.next.formats();\n      if (childFormats.table !== nextFormats.table) {\n        const next = this.splitAfter(child);\n        if (next) {\n          // @ts-expect-error TODO: parameters of optimize() should be a optional\n          next.optimize();\n        }\n        // We might be able to merge with prev now\n        if (this.prev) {\n          // @ts-expect-error TODO: parameters of optimize() should be a optional\n          this.prev.optimize();\n        }\n      }\n    });\n  }\n\n  rowOffset() {\n    if (this.parent) {\n      return this.parent.children.indexOf(this);\n    }\n    return -1;\n  }\n\n  table() {\n    return this.parent && this.parent.parent;\n  }\n}\n\nclass TableBody extends Container {\n  static blotName = 'table-body';\n  static tagName = 'TBODY';\n\n  children: LinkedList<TableRow>;\n}\n\nclass TableContainer extends Container {\n  static blotName = 'table-container';\n  static tagName = 'TABLE';\n\n  children: LinkedList<TableBody>;\n\n  balanceCells() {\n    const rows = this.descendants(TableRow);\n    const maxColumns = rows.reduce((max, row) => {\n      return Math.max(row.children.length, max);\n    }, 0);\n    rows.forEach((row) => {\n      new Array(maxColumns - row.children.length).fill(0).forEach(() => {\n        let value;\n        if (row.children.head != null) {\n          value = TableCell.formats(row.children.head.domNode);\n        }\n        const blot = this.scroll.create(TableCell.blotName, value);\n        row.appendChild(blot);\n        // @ts-expect-error TODO: parameters of optimize() should be a optional\n        blot.optimize(); // Add break blot\n      });\n    });\n  }\n\n  cells(column: number) {\n    return this.rows().map((row) => row.children.at(column));\n  }\n\n  deleteColumn(index: number) {\n    // @ts-expect-error\n    const [body] = this.descendant(TableBody) as TableBody[];\n    if (body == null || body.children.head == null) return;\n    body.children.forEach((row) => {\n      const cell = row.children.at(index);\n      if (cell != null) {\n        cell.remove();\n      }\n    });\n  }\n\n  insertColumn(index: number) {\n    // @ts-expect-error\n    const [body] = this.descendant(TableBody) as TableBody[];\n    if (body == null || body.children.head == null) return;\n    body.children.forEach((row) => {\n      const ref = row.children.at(index);\n      // @ts-expect-error\n      const value = TableCell.formats(row.children.head.domNode);\n      const cell = this.scroll.create(TableCell.blotName, value);\n      row.insertBefore(cell, ref);\n    });\n  }\n\n  insertRow(index: number) {\n    // @ts-expect-error\n    const [body] = this.descendant(TableBody) as TableBody[];\n    if (body == null || body.children.head == null) return;\n    const id = tableId();\n    const row = this.scroll.create(TableRow.blotName) as TableRow;\n    body.children.head.children.forEach(() => {\n      const cell = this.scroll.create(TableCell.blotName, id);\n      row.appendChild(cell);\n    });\n    const ref = body.children.at(index);\n    body.insertBefore(row, ref);\n  }\n\n  rows() {\n    const body = this.children.head;\n    if (body == null) return [];\n    return body.children.map((row) => row);\n  }\n}\n\nTableContainer.allowedChildren = [TableBody];\nTableBody.requiredContainer = TableContainer;\n\nTableBody.allowedChildren = [TableRow];\nTableRow.requiredContainer = TableBody;\n\nTableRow.allowedChildren = [TableCell];\nTableCell.requiredContainer = TableRow;\n\nfunction tableId() {\n  const id = Math.random().toString(36).slice(2, 6);\n  return `row-${id}`;\n}\n\nexport { TableCell, TableRow, TableBody, TableContainer, tableId };\n"
  },
  {
    "path": "packages/quill/src/formats/underline.ts",
    "content": "import Inline from '../blots/inline.js';\n\nclass Underline extends Inline {\n  static blotName = 'underline';\n  static tagName = 'U';\n}\n\nexport default Underline;\n"
  },
  {
    "path": "packages/quill/src/formats/video.ts",
    "content": "import { BlockEmbed } from '../blots/block.js';\nimport Link from './link.js';\n\nconst ATTRIBUTES = ['height', 'width'];\n\nclass Video extends BlockEmbed {\n  static blotName = 'video';\n  static className = 'ql-video';\n  static tagName = 'IFRAME';\n\n  static create(value: string) {\n    const node = super.create(value) as Element;\n    node.setAttribute('frameborder', '0');\n    node.setAttribute('allowfullscreen', 'true');\n    node.setAttribute('src', this.sanitize(value));\n    return node;\n  }\n\n  static formats(domNode: Element) {\n    return ATTRIBUTES.reduce(\n      (formats: Record<string, string | null>, attribute) => {\n        if (domNode.hasAttribute(attribute)) {\n          formats[attribute] = domNode.getAttribute(attribute);\n        }\n        return formats;\n      },\n      {},\n    );\n  }\n\n  static sanitize(url: string) {\n    return Link.sanitize(url);\n  }\n\n  static value(domNode: Element) {\n    return domNode.getAttribute('src');\n  }\n\n  domNode: HTMLVideoElement;\n\n  format(name: string, value: string) {\n    if (ATTRIBUTES.indexOf(name) > -1) {\n      if (value) {\n        this.domNode.setAttribute(name, value);\n      } else {\n        this.domNode.removeAttribute(name);\n      }\n    } else {\n      super.format(name, value);\n    }\n  }\n\n  html() {\n    const { video } = this.value();\n    return `<a href=\"${video}\">${video}</a>`;\n  }\n}\n\nexport default Video;\n"
  },
  {
    "path": "packages/quill/src/modules/clipboard.ts",
    "content": "import type { ScrollBlot } from 'parchment';\nimport {\n  Attributor,\n  BlockBlot,\n  ClassAttributor,\n  EmbedBlot,\n  Scope,\n  StyleAttributor,\n} from 'parchment';\nimport Delta from 'quill-delta';\nimport { BlockEmbed } from '../blots/block.js';\nimport type { EmitterSource } from '../core/emitter.js';\nimport logger from '../core/logger.js';\nimport Module from '../core/module.js';\nimport Quill from '../core/quill.js';\nimport type { Range } from '../core/selection.js';\nimport { AlignAttribute, AlignStyle } from '../formats/align.js';\nimport { BackgroundStyle } from '../formats/background.js';\nimport CodeBlock from '../formats/code.js';\nimport { ColorStyle } from '../formats/color.js';\nimport { DirectionAttribute, DirectionStyle } from '../formats/direction.js';\nimport { FontStyle } from '../formats/font.js';\nimport { SizeStyle } from '../formats/size.js';\nimport { deleteRange } from './keyboard.js';\nimport normalizeExternalHTML from './normalizeExternalHTML/index.js';\n\nconst debug = logger('quill:clipboard');\n\ntype Selector = string | Node['TEXT_NODE'] | Node['ELEMENT_NODE'];\ntype Matcher = (node: Node, delta: Delta, scroll: ScrollBlot) => Delta;\n\nconst CLIPBOARD_CONFIG: [Selector, Matcher][] = [\n  [Node.TEXT_NODE, matchText],\n  [Node.TEXT_NODE, matchNewline],\n  ['br', matchBreak],\n  [Node.ELEMENT_NODE, matchNewline],\n  [Node.ELEMENT_NODE, matchBlot],\n  [Node.ELEMENT_NODE, matchAttributor],\n  [Node.ELEMENT_NODE, matchStyles],\n  ['li', matchIndent],\n  ['ol, ul', matchList],\n  ['pre', matchCodeBlock],\n  ['tr', matchTable],\n  ['b', createMatchAlias('bold')],\n  ['i', createMatchAlias('italic')],\n  ['strike', createMatchAlias('strike')],\n  ['style', matchIgnore],\n];\n\nconst ATTRIBUTE_ATTRIBUTORS = [AlignAttribute, DirectionAttribute].reduce(\n  (memo: Record<string, Attributor>, attr) => {\n    memo[attr.keyName] = attr;\n    return memo;\n  },\n  {},\n);\n\nconst STYLE_ATTRIBUTORS = [\n  AlignStyle,\n  BackgroundStyle,\n  ColorStyle,\n  DirectionStyle,\n  FontStyle,\n  SizeStyle,\n].reduce((memo: Record<string, Attributor>, attr) => {\n  memo[attr.keyName] = attr;\n  return memo;\n}, {});\n\ninterface ClipboardOptions {\n  matchers: [Selector, Matcher][];\n}\n\nclass Clipboard extends Module<ClipboardOptions> {\n  static DEFAULTS: ClipboardOptions = {\n    matchers: [],\n  };\n\n  matchers: [Selector, Matcher][];\n\n  constructor(quill: Quill, options: Partial<ClipboardOptions>) {\n    super(quill, options);\n    this.quill.root.addEventListener('copy', (e) =>\n      this.onCaptureCopy(e, false),\n    );\n    this.quill.root.addEventListener('cut', (e) => this.onCaptureCopy(e, true));\n    this.quill.root.addEventListener('paste', this.onCapturePaste.bind(this));\n    this.matchers = [];\n    CLIPBOARD_CONFIG.concat(this.options.matchers ?? []).forEach(\n      ([selector, matcher]) => {\n        this.addMatcher(selector, matcher);\n      },\n    );\n  }\n\n  addMatcher(selector: Selector, matcher: Matcher) {\n    this.matchers.push([selector, matcher]);\n  }\n\n  convert(\n    { html, text }: { html?: string; text?: string },\n    formats: Record<string, unknown> = {},\n  ) {\n    if (formats[CodeBlock.blotName]) {\n      return new Delta().insert(text || '', {\n        [CodeBlock.blotName]: formats[CodeBlock.blotName],\n      });\n    }\n    if (!html) {\n      return new Delta().insert(text || '', formats);\n    }\n    const delta = this.convertHTML(html);\n    // Remove trailing newline\n    if (\n      deltaEndsWith(delta, '\\n') &&\n      (delta.ops[delta.ops.length - 1].attributes == null || formats.table)\n    ) {\n      return delta.compose(new Delta().retain(delta.length() - 1).delete(1));\n    }\n    return delta;\n  }\n\n  protected normalizeHTML(doc: Document) {\n    normalizeExternalHTML(doc);\n  }\n\n  protected convertHTML(html: string) {\n    const doc = new DOMParser().parseFromString(html, 'text/html');\n    this.normalizeHTML(doc);\n    const container = doc.body;\n    const nodeMatches = new WeakMap();\n    const [elementMatchers, textMatchers] = this.prepareMatching(\n      container,\n      nodeMatches,\n    );\n    return traverse(\n      this.quill.scroll,\n      container,\n      elementMatchers,\n      textMatchers,\n      nodeMatches,\n    );\n  }\n\n  dangerouslyPasteHTML(html: string, source?: EmitterSource): void;\n  dangerouslyPasteHTML(\n    index: number,\n    html: string,\n    source?: EmitterSource,\n  ): void;\n  dangerouslyPasteHTML(\n    index: number | string,\n    html?: string,\n    source: EmitterSource = Quill.sources.API,\n  ) {\n    if (typeof index === 'string') {\n      const delta = this.convert({ html: index, text: '' });\n      // @ts-expect-error\n      this.quill.setContents(delta, html);\n      this.quill.setSelection(0, Quill.sources.SILENT);\n    } else {\n      const paste = this.convert({ html, text: '' });\n      this.quill.updateContents(\n        new Delta().retain(index).concat(paste),\n        source,\n      );\n      this.quill.setSelection(index + paste.length(), Quill.sources.SILENT);\n    }\n  }\n\n  onCaptureCopy(e: ClipboardEvent, isCut = false) {\n    if (e.defaultPrevented) return;\n    e.preventDefault();\n    const [range] = this.quill.selection.getRange();\n    if (range == null) return;\n    const { html, text } = this.onCopy(range, isCut);\n    e.clipboardData?.setData('text/plain', text);\n    e.clipboardData?.setData('text/html', html);\n    if (isCut) {\n      deleteRange({ range, quill: this.quill });\n    }\n  }\n\n  /*\n   * https://www.iana.org/assignments/media-types/text/uri-list\n   */\n  private normalizeURIList(urlList: string) {\n    return (\n      urlList\n        .split(/\\r?\\n/)\n        // Ignore all comments\n        .filter((url) => url[0] !== '#')\n        .join('\\n')\n    );\n  }\n\n  onCapturePaste(e: ClipboardEvent) {\n    if (e.defaultPrevented || !this.quill.isEnabled()) return;\n    e.preventDefault();\n    const range = this.quill.getSelection(true);\n    if (range == null) return;\n    const html = e.clipboardData?.getData('text/html');\n    let text = e.clipboardData?.getData('text/plain');\n    if (!html && !text) {\n      const urlList = e.clipboardData?.getData('text/uri-list');\n      if (urlList) {\n        text = this.normalizeURIList(urlList);\n      }\n    }\n    const files = Array.from(e.clipboardData?.files || []);\n    if (!html && files.length > 0) {\n      this.quill.uploader.upload(range, files);\n      return;\n    }\n    if (html && files.length > 0) {\n      const doc = new DOMParser().parseFromString(html, 'text/html');\n      if (\n        doc.body.childElementCount === 1 &&\n        doc.body.firstElementChild?.tagName === 'IMG'\n      ) {\n        this.quill.uploader.upload(range, files);\n        return;\n      }\n    }\n    this.onPaste(range, { html, text });\n  }\n\n  onCopy(range: Range, isCut: boolean): { html: string; text: string };\n  onCopy(range: Range) {\n    const text = this.quill.getText(range);\n    const html = this.quill.getSemanticHTML(range);\n    return { html, text };\n  }\n\n  onPaste(range: Range, { text, html }: { text?: string; html?: string }) {\n    const formats = this.quill.getFormat(range.index);\n    const pastedDelta = this.convert({ text, html }, formats);\n    debug.log('onPaste', pastedDelta, { text, html });\n    const delta = new Delta()\n      .retain(range.index)\n      .delete(range.length)\n      .concat(pastedDelta);\n    this.quill.updateContents(delta, Quill.sources.USER);\n    // range.length contributes to delta.length()\n    this.quill.setSelection(\n      delta.length() - range.length,\n      Quill.sources.SILENT,\n    );\n    this.quill.scrollSelectionIntoView();\n  }\n\n  prepareMatching(container: Element, nodeMatches: WeakMap<Node, Matcher[]>) {\n    const elementMatchers: Matcher[] = [];\n    const textMatchers: Matcher[] = [];\n    this.matchers.forEach((pair) => {\n      const [selector, matcher] = pair;\n      switch (selector) {\n        case Node.TEXT_NODE:\n          textMatchers.push(matcher);\n          break;\n        case Node.ELEMENT_NODE:\n          elementMatchers.push(matcher);\n          break;\n        default:\n          Array.from(container.querySelectorAll(selector)).forEach((node) => {\n            if (nodeMatches.has(node)) {\n              const matches = nodeMatches.get(node);\n              matches?.push(matcher);\n            } else {\n              nodeMatches.set(node, [matcher]);\n            }\n          });\n          break;\n      }\n    });\n    return [elementMatchers, textMatchers];\n  }\n}\n\nfunction applyFormat(\n  delta: Delta,\n  format: string,\n  value: unknown,\n  scroll: ScrollBlot,\n): Delta {\n  if (!scroll.query(format)) {\n    return delta;\n  }\n\n  return delta.reduce((newDelta, op) => {\n    if (!op.insert) return newDelta;\n    if (op.attributes && op.attributes[format]) {\n      return newDelta.push(op);\n    }\n    const formats = value ? { [format]: value } : {};\n    return newDelta.insert(op.insert, { ...formats, ...op.attributes });\n  }, new Delta());\n}\n\nfunction deltaEndsWith(delta: Delta, text: string) {\n  let endText = '';\n  for (\n    let i = delta.ops.length - 1;\n    i >= 0 && endText.length < text.length;\n    --i // eslint-disable-line no-plusplus\n  ) {\n    const op = delta.ops[i];\n    if (typeof op.insert !== 'string') break;\n    endText = op.insert + endText;\n  }\n  return endText.slice(-1 * text.length) === text;\n}\n\nfunction isLine(node: Node, scroll: ScrollBlot) {\n  if (!(node instanceof Element)) return false;\n  const match = scroll.query(node);\n  // @ts-expect-error\n  if (match && match.prototype instanceof EmbedBlot) return false;\n\n  return [\n    'address',\n    'article',\n    'blockquote',\n    'canvas',\n    'dd',\n    'div',\n    'dl',\n    'dt',\n    'fieldset',\n    'figcaption',\n    'figure',\n    'footer',\n    'form',\n    'h1',\n    'h2',\n    'h3',\n    'h4',\n    'h5',\n    'h6',\n    'header',\n    'iframe',\n    'li',\n    'main',\n    'nav',\n    'ol',\n    'output',\n    'p',\n    'pre',\n    'section',\n    'table',\n    'td',\n    'tr',\n    'ul',\n    'video',\n  ].includes(node.tagName.toLowerCase());\n}\n\nfunction isBetweenInlineElements(node: HTMLElement, scroll: ScrollBlot) {\n  return (\n    node.previousElementSibling &&\n    node.nextElementSibling &&\n    !isLine(node.previousElementSibling, scroll) &&\n    !isLine(node.nextElementSibling, scroll)\n  );\n}\n\nconst preNodes = new WeakMap();\nfunction isPre(node: Node | null) {\n  if (node == null) return false;\n  if (!preNodes.has(node)) {\n    // @ts-expect-error\n    if (node.tagName === 'PRE') {\n      preNodes.set(node, true);\n    } else {\n      preNodes.set(node, isPre(node.parentNode));\n    }\n  }\n  return preNodes.get(node);\n}\n\nfunction traverse(\n  scroll: ScrollBlot,\n  node: ChildNode,\n  elementMatchers: Matcher[],\n  textMatchers: Matcher[],\n  nodeMatches: WeakMap<Node, Matcher[]>,\n): Delta {\n  // Post-order\n  if (node.nodeType === node.TEXT_NODE) {\n    return textMatchers.reduce((delta: Delta, matcher) => {\n      return matcher(node, delta, scroll);\n    }, new Delta());\n  }\n  if (node.nodeType === node.ELEMENT_NODE) {\n    return Array.from(node.childNodes || []).reduce((delta, childNode) => {\n      let childrenDelta = traverse(\n        scroll,\n        childNode,\n        elementMatchers,\n        textMatchers,\n        nodeMatches,\n      );\n      if (childNode.nodeType === node.ELEMENT_NODE) {\n        childrenDelta = elementMatchers.reduce((reducedDelta, matcher) => {\n          return matcher(childNode as HTMLElement, reducedDelta, scroll);\n        }, childrenDelta);\n        childrenDelta = (nodeMatches.get(childNode) || []).reduce(\n          (reducedDelta, matcher) => {\n            return matcher(childNode, reducedDelta, scroll);\n          },\n          childrenDelta,\n        );\n      }\n      return delta.concat(childrenDelta);\n    }, new Delta());\n  }\n  return new Delta();\n}\n\nfunction createMatchAlias(format: string) {\n  return (_node: Element, delta: Delta, scroll: ScrollBlot) => {\n    return applyFormat(delta, format, true, scroll);\n  };\n}\n\nfunction matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {\n  const attributes = Attributor.keys(node);\n  const classes = ClassAttributor.keys(node);\n  const styles = StyleAttributor.keys(node);\n  const formats: Record<string, string | undefined> = {};\n  attributes\n    .concat(classes)\n    .concat(styles)\n    .forEach((name) => {\n      let attr = scroll.query(name, Scope.ATTRIBUTE) as Attributor;\n      if (attr != null) {\n        formats[attr.attrName] = attr.value(node);\n        if (formats[attr.attrName]) return;\n      }\n      attr = ATTRIBUTE_ATTRIBUTORS[name];\n      if (attr != null && (attr.attrName === name || attr.keyName === name)) {\n        formats[attr.attrName] = attr.value(node) || undefined;\n      }\n      attr = STYLE_ATTRIBUTORS[name];\n      if (attr != null && (attr.attrName === name || attr.keyName === name)) {\n        attr = STYLE_ATTRIBUTORS[name];\n        formats[attr.attrName] = attr.value(node) || undefined;\n      }\n    });\n\n  return Object.entries(formats).reduce(\n    (newDelta, [name, value]) => applyFormat(newDelta, name, value, scroll),\n    delta,\n  );\n}\n\nfunction matchBlot(node: Node, delta: Delta, scroll: ScrollBlot) {\n  const match = scroll.query(node);\n  if (match == null) return delta;\n  // @ts-expect-error\n  if (match.prototype instanceof EmbedBlot) {\n    const embed = {};\n    // @ts-expect-error\n    const value = match.value(node);\n    if (value != null) {\n      // @ts-expect-error\n      embed[match.blotName] = value;\n      // @ts-expect-error\n      return new Delta().insert(embed, match.formats(node, scroll));\n    }\n  } else {\n    // @ts-expect-error\n    if (match.prototype instanceof BlockBlot && !deltaEndsWith(delta, '\\n')) {\n      delta.insert('\\n');\n    }\n    if (\n      'blotName' in match &&\n      'formats' in match &&\n      typeof match.formats === 'function'\n    ) {\n      return applyFormat(\n        delta,\n        match.blotName,\n        match.formats(node, scroll),\n        scroll,\n      );\n    }\n  }\n  return delta;\n}\n\nfunction matchBreak(node: Node, delta: Delta) {\n  if (!deltaEndsWith(delta, '\\n')) {\n    delta.insert('\\n');\n  }\n  return delta;\n}\n\nfunction matchCodeBlock(node: Node, delta: Delta, scroll: ScrollBlot) {\n  const match = scroll.query('code-block');\n  const language =\n    match && 'formats' in match && typeof match.formats === 'function'\n      ? match.formats(node, scroll)\n      : true;\n  return applyFormat(delta, 'code-block', language, scroll);\n}\n\nfunction matchIgnore() {\n  return new Delta();\n}\n\nfunction matchIndent(node: Node, delta: Delta, scroll: ScrollBlot) {\n  const match = scroll.query(node);\n  if (\n    match == null ||\n    // @ts-expect-error\n    match.blotName !== 'list' ||\n    !deltaEndsWith(delta, '\\n')\n  ) {\n    return delta;\n  }\n  let indent = -1;\n  let parent = node.parentNode;\n  while (parent != null) {\n    // @ts-expect-error\n    if (['OL', 'UL'].includes(parent.tagName)) {\n      indent += 1;\n    }\n    parent = parent.parentNode;\n  }\n  if (indent <= 0) return delta;\n  return delta.reduce((composed, op) => {\n    if (!op.insert) return composed;\n    if (op.attributes && typeof op.attributes.indent === 'number') {\n      return composed.push(op);\n    }\n    return composed.insert(op.insert, { indent, ...(op.attributes || {}) });\n  }, new Delta());\n}\n\nfunction matchList(node: Node, delta: Delta, scroll: ScrollBlot) {\n  const element = node as Element;\n  let list = element.tagName === 'OL' ? 'ordered' : 'bullet';\n\n  const checkedAttr = element.getAttribute('data-checked');\n  if (checkedAttr) {\n    list = checkedAttr === 'true' ? 'checked' : 'unchecked';\n  }\n\n  return applyFormat(delta, 'list', list, scroll);\n}\n\nfunction matchNewline(node: Node, delta: Delta, scroll: ScrollBlot) {\n  if (!deltaEndsWith(delta, '\\n')) {\n    if (\n      isLine(node, scroll) &&\n      (node.childNodes.length > 0 || node instanceof HTMLParagraphElement)\n    ) {\n      return delta.insert('\\n');\n    }\n    if (delta.length() > 0 && node.nextSibling) {\n      let nextSibling: Node | null = node.nextSibling;\n      while (nextSibling != null) {\n        if (isLine(nextSibling, scroll)) {\n          return delta.insert('\\n');\n        }\n        const match = scroll.query(nextSibling);\n        // @ts-expect-error\n        if (match && match.prototype instanceof BlockEmbed) {\n          return delta.insert('\\n');\n        }\n        nextSibling = nextSibling.firstChild;\n      }\n    }\n  }\n  return delta;\n}\n\nfunction matchStyles(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {\n  const formats: Record<string, unknown> = {};\n  const style: Partial<CSSStyleDeclaration> = node.style || {};\n  if (style.fontStyle === 'italic') {\n    formats.italic = true;\n  }\n  if (style.textDecoration === 'underline') {\n    formats.underline = true;\n  }\n  if (style.textDecoration === 'line-through') {\n    formats.strike = true;\n  }\n  if (\n    style.fontWeight?.startsWith('bold') ||\n    // @ts-expect-error Fix me later\n    parseInt(style.fontWeight, 10) >= 700\n  ) {\n    formats.bold = true;\n  }\n  delta = Object.entries(formats).reduce(\n    (newDelta, [name, value]) => applyFormat(newDelta, name, value, scroll),\n    delta,\n  );\n  // @ts-expect-error\n  if (parseFloat(style.textIndent || 0) > 0) {\n    // Could be 0.5in\n    return new Delta().insert('\\t').concat(delta);\n  }\n  return delta;\n}\n\nfunction matchTable(\n  node: HTMLTableRowElement,\n  delta: Delta,\n  scroll: ScrollBlot,\n) {\n  const table =\n    node.parentElement?.tagName === 'TABLE'\n      ? node.parentElement\n      : node.parentElement?.parentElement;\n  if (table != null) {\n    const rows = Array.from(table.querySelectorAll('tr'));\n    const row = rows.indexOf(node) + 1;\n    return applyFormat(delta, 'table', row, scroll);\n  }\n  return delta;\n}\n\nfunction matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {\n  // @ts-expect-error\n  let text = node.data as string;\n  // Word represents empty line with <o:p>&nbsp;</o:p>\n  if (node.parentElement?.tagName === 'O:P') {\n    return delta.insert(text.trim());\n  }\n  if (!isPre(node)) {\n    if (\n      text.trim().length === 0 &&\n      text.includes('\\n') &&\n      !isBetweenInlineElements(node, scroll)\n    ) {\n      return delta;\n    }\n    // convert all non-nbsp whitespace into regular space\n    text = text.replace(/[^\\S\\u00a0]/g, ' ');\n    // collapse consecutive spaces into one\n    text = text.replace(/ {2,}/g, ' ');\n    if (\n      (node.previousSibling == null &&\n        node.parentElement != null &&\n        isLine(node.parentElement, scroll)) ||\n      (node.previousSibling instanceof Element &&\n        isLine(node.previousSibling, scroll))\n    ) {\n      // block structure means we don't need leading space\n      text = text.replace(/^ /, '');\n    }\n    if (\n      (node.nextSibling == null &&\n        node.parentElement != null &&\n        isLine(node.parentElement, scroll)) ||\n      (node.nextSibling instanceof Element && isLine(node.nextSibling, scroll))\n    ) {\n      // block structure means we don't need trailing space\n      text = text.replace(/ $/, '');\n    }\n    // done removing whitespace and can normalize all to regular space\n    text = text.replaceAll('\\u00a0', ' ');\n  }\n  return delta.insert(text);\n}\n\nexport {\n  Clipboard as default,\n  matchAttributor,\n  matchBlot,\n  matchNewline,\n  matchText,\n  traverse,\n};\n"
  },
  {
    "path": "packages/quill/src/modules/history.ts",
    "content": "import { Scope } from 'parchment';\nimport type Delta from 'quill-delta';\nimport Module from '../core/module.js';\nimport Quill from '../core/quill.js';\nimport type Scroll from '../blots/scroll.js';\nimport type { Range } from '../core/selection.js';\n\nexport interface HistoryOptions {\n  userOnly: boolean;\n  delay: number;\n  maxStack: number;\n}\n\nexport interface StackItem {\n  delta: Delta;\n  range: Range | null;\n}\n\ninterface Stack {\n  undo: StackItem[];\n  redo: StackItem[];\n}\n\nclass History extends Module<HistoryOptions> {\n  static DEFAULTS: HistoryOptions = {\n    delay: 1000,\n    maxStack: 100,\n    userOnly: false,\n  };\n\n  lastRecorded = 0;\n  ignoreChange = false;\n  stack: Stack = { undo: [], redo: [] };\n  currentRange: Range | null = null;\n\n  constructor(quill: Quill, options: Partial<HistoryOptions>) {\n    super(quill, options);\n    this.quill.on(\n      Quill.events.EDITOR_CHANGE,\n      (eventName, value, oldValue, source) => {\n        if (eventName === Quill.events.SELECTION_CHANGE) {\n          if (value && source !== Quill.sources.SILENT) {\n            this.currentRange = value;\n          }\n        } else if (eventName === Quill.events.TEXT_CHANGE) {\n          if (!this.ignoreChange) {\n            if (!this.options.userOnly || source === Quill.sources.USER) {\n              this.record(value, oldValue);\n            } else {\n              this.transform(value);\n            }\n          }\n\n          this.currentRange = transformRange(this.currentRange, value);\n        }\n      },\n    );\n\n    this.quill.keyboard.addBinding(\n      { key: 'z', shortKey: true },\n      this.undo.bind(this),\n    );\n    this.quill.keyboard.addBinding(\n      { key: ['z', 'Z'], shortKey: true, shiftKey: true },\n      this.redo.bind(this),\n    );\n    if (/Win/i.test(navigator.platform)) {\n      this.quill.keyboard.addBinding(\n        { key: 'y', shortKey: true },\n        this.redo.bind(this),\n      );\n    }\n\n    this.quill.root.addEventListener('beforeinput', (event) => {\n      if (event.inputType === 'historyUndo') {\n        this.undo();\n        event.preventDefault();\n      } else if (event.inputType === 'historyRedo') {\n        this.redo();\n        event.preventDefault();\n      }\n    });\n  }\n\n  change(source: 'undo' | 'redo', dest: 'redo' | 'undo') {\n    if (this.stack[source].length === 0) return;\n    const item = this.stack[source].pop();\n    if (!item) return;\n    const base = this.quill.getContents();\n    const inverseDelta = item.delta.invert(base);\n    this.stack[dest].push({\n      delta: inverseDelta,\n      range: transformRange(item.range, inverseDelta),\n    });\n    this.lastRecorded = 0;\n    this.ignoreChange = true;\n    this.quill.updateContents(item.delta, Quill.sources.USER);\n    this.ignoreChange = false;\n\n    this.restoreSelection(item);\n  }\n\n  clear() {\n    this.stack = { undo: [], redo: [] };\n  }\n\n  cutoff() {\n    this.lastRecorded = 0;\n  }\n\n  record(changeDelta: Delta, oldDelta: Delta) {\n    if (changeDelta.ops.length === 0) return;\n    this.stack.redo = [];\n    let undoDelta = changeDelta.invert(oldDelta);\n    let undoRange = this.currentRange;\n    const timestamp = Date.now();\n    if (\n      // @ts-expect-error Fix me later\n      this.lastRecorded + this.options.delay > timestamp &&\n      this.stack.undo.length > 0\n    ) {\n      const item = this.stack.undo.pop();\n      if (item) {\n        undoDelta = undoDelta.compose(item.delta);\n        undoRange = item.range;\n      }\n    } else {\n      this.lastRecorded = timestamp;\n    }\n    if (undoDelta.length() === 0) return;\n    this.stack.undo.push({ delta: undoDelta, range: undoRange });\n    // @ts-expect-error Fix me later\n    if (this.stack.undo.length > this.options.maxStack) {\n      this.stack.undo.shift();\n    }\n  }\n\n  redo() {\n    this.change('redo', 'undo');\n  }\n\n  transform(delta: Delta) {\n    transformStack(this.stack.undo, delta);\n    transformStack(this.stack.redo, delta);\n  }\n\n  undo() {\n    this.change('undo', 'redo');\n  }\n\n  protected restoreSelection(stackItem: StackItem) {\n    if (stackItem.range) {\n      this.quill.setSelection(stackItem.range, Quill.sources.USER);\n    } else {\n      const index = getLastChangeIndex(this.quill.scroll, stackItem.delta);\n      this.quill.setSelection(index, Quill.sources.USER);\n    }\n  }\n}\n\nfunction transformStack(stack: StackItem[], delta: Delta) {\n  let remoteDelta = delta;\n  for (let i = stack.length - 1; i >= 0; i -= 1) {\n    const oldItem = stack[i];\n    stack[i] = {\n      delta: remoteDelta.transform(oldItem.delta, true),\n      range: oldItem.range && transformRange(oldItem.range, remoteDelta),\n    };\n    remoteDelta = oldItem.delta.transform(remoteDelta);\n    if (stack[i].delta.length() === 0) {\n      stack.splice(i, 1);\n    }\n  }\n}\n\nfunction endsWithNewlineChange(scroll: Scroll, delta: Delta) {\n  const lastOp = delta.ops[delta.ops.length - 1];\n  if (lastOp == null) return false;\n  if (lastOp.insert != null) {\n    return typeof lastOp.insert === 'string' && lastOp.insert.endsWith('\\n');\n  }\n  if (lastOp.attributes != null) {\n    return Object.keys(lastOp.attributes).some((attr) => {\n      return scroll.query(attr, Scope.BLOCK) != null;\n    });\n  }\n  return false;\n}\n\nfunction getLastChangeIndex(scroll: Scroll, delta: Delta) {\n  const deleteLength = delta.reduce((length, op) => {\n    return length + (op.delete || 0);\n  }, 0);\n  let changeIndex = delta.length() - deleteLength;\n  if (endsWithNewlineChange(scroll, delta)) {\n    changeIndex -= 1;\n  }\n  return changeIndex;\n}\n\nfunction transformRange(range: Range | null, delta: Delta) {\n  if (!range) return range;\n  const start = delta.transformPosition(range.index);\n  const end = delta.transformPosition(range.index + range.length);\n  return { index: start, length: end - start };\n}\n\nexport { History as default, getLastChangeIndex };\n"
  },
  {
    "path": "packages/quill/src/modules/input.ts",
    "content": "import Delta from 'quill-delta';\nimport Module from '../core/module.js';\nimport Quill from '../core/quill.js';\nimport type { Range } from '../core/selection.js';\nimport { deleteRange } from './keyboard.js';\n\nconst INSERT_TYPES = ['insertText', 'insertReplacementText'];\n\nclass Input extends Module {\n  constructor(quill: Quill, options: Record<string, never>) {\n    super(quill, options);\n\n    quill.root.addEventListener('beforeinput', (event) => {\n      this.handleBeforeInput(event);\n    });\n\n    // Gboard with English input on Android triggers `compositionstart` sometimes even\n    // users are not going to type anything.\n    if (!/Android/i.test(navigator.userAgent)) {\n      quill.on(Quill.events.COMPOSITION_BEFORE_START, () => {\n        this.handleCompositionStart();\n      });\n    }\n  }\n\n  private deleteRange(range: Range) {\n    deleteRange({ range, quill: this.quill });\n  }\n\n  private replaceText(range: Range, text = '') {\n    if (range.length === 0) return false;\n\n    if (text) {\n      // Follow the native behavior that inherits the formats of the first character\n      const formats = this.quill.getFormat(range.index, 1);\n      this.deleteRange(range);\n      this.quill.updateContents(\n        new Delta().retain(range.index).insert(text, formats),\n        Quill.sources.USER,\n      );\n    } else {\n      this.deleteRange(range);\n    }\n\n    this.quill.setSelection(range.index + text.length, 0, Quill.sources.SILENT);\n    return true;\n  }\n\n  private handleBeforeInput(event: InputEvent) {\n    if (\n      this.quill.composition.isComposing ||\n      event.defaultPrevented ||\n      !INSERT_TYPES.includes(event.inputType)\n    ) {\n      return;\n    }\n\n    const staticRange = event.getTargetRanges\n      ? event.getTargetRanges()[0]\n      : null;\n    if (!staticRange || staticRange.collapsed === true) {\n      return;\n    }\n\n    const text = getPlainTextFromInputEvent(event);\n    if (text == null) {\n      return;\n    }\n    const normalized = this.quill.selection.normalizeNative(staticRange);\n    const range = normalized\n      ? this.quill.selection.normalizedToRange(normalized)\n      : null;\n    if (range && this.replaceText(range, text)) {\n      event.preventDefault();\n    }\n  }\n\n  private handleCompositionStart() {\n    const range = this.quill.getSelection();\n    if (range) {\n      this.replaceText(range);\n    }\n  }\n}\n\nfunction getPlainTextFromInputEvent(event: InputEvent) {\n  // When `inputType` is \"insertText\":\n  // - `event.data` should be string (Safari uses `event.dataTransfer`).\n  // - `event.dataTransfer` should be null.\n  // When `inputType` is \"insertReplacementText\":\n  // - `event.data` should be null.\n  // - `event.dataTransfer` should contain \"text/plain\" data.\n\n  if (typeof event.data === 'string') {\n    return event.data;\n  }\n  if (event.dataTransfer?.types.includes('text/plain')) {\n    return event.dataTransfer.getData('text/plain');\n  }\n  return null;\n}\n\nexport default Input;\n"
  },
  {
    "path": "packages/quill/src/modules/keyboard.ts",
    "content": "import { cloneDeep, isEqual } from 'lodash-es';\nimport Delta, { AttributeMap } from 'quill-delta';\nimport { EmbedBlot, Scope, TextBlot } from 'parchment';\nimport type { Blot, BlockBlot } from 'parchment';\nimport Quill from '../core/quill.js';\nimport logger from '../core/logger.js';\nimport Module from '../core/module.js';\nimport type { BlockEmbed } from '../blots/block.js';\nimport type { Range } from '../core/selection.js';\n\nconst debug = logger('quill:keyboard');\n\nconst SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';\n\nexport interface Context {\n  collapsed: boolean;\n  empty: boolean;\n  offset: number;\n  prefix: string;\n  suffix: string;\n  format: Record<string, unknown>;\n  event: KeyboardEvent;\n  line: BlockEmbed | BlockBlot;\n}\n\ninterface BindingObject\n  extends Partial<Omit<Context, 'prefix' | 'suffix' | 'format'>> {\n  key: number | string | string[];\n  shortKey?: boolean | null;\n  shiftKey?: boolean | null;\n  altKey?: boolean | null;\n  metaKey?: boolean | null;\n  ctrlKey?: boolean | null;\n  prefix?: RegExp;\n  suffix?: RegExp;\n  format?: Record<string, unknown> | string[];\n  handler?: (\n    this: { quill: Quill },\n    range: Range,\n    curContext: Context,\n    // eslint-disable-next-line no-use-before-define\n    binding: NormalizedBinding,\n  ) => boolean | void;\n}\n\ntype Binding = BindingObject | string | number;\n\ninterface NormalizedBinding extends Omit<BindingObject, 'key' | 'shortKey'> {\n  key: string | number;\n}\n\ninterface KeyboardOptions {\n  bindings: Record<string, Binding>;\n}\n\ninterface KeyboardOptions {\n  bindings: Record<string, Binding>;\n}\n\nclass Keyboard extends Module<KeyboardOptions> {\n  static DEFAULTS: KeyboardOptions;\n\n  static match(evt: KeyboardEvent, binding: BindingObject) {\n    if (\n      (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'] as const).some((key) => {\n        return !!binding[key] !== evt[key] && binding[key] !== null;\n      })\n    ) {\n      return false;\n    }\n    return binding.key === evt.key || binding.key === evt.which;\n  }\n\n  bindings: Record<string, NormalizedBinding[]>;\n\n  constructor(quill: Quill, options: Partial<KeyboardOptions>) {\n    super(quill, options);\n    this.bindings = {};\n    // @ts-expect-error Fix me later\n    Object.keys(this.options.bindings).forEach((name) => {\n      // @ts-expect-error Fix me later\n      if (this.options.bindings[name]) {\n        // @ts-expect-error Fix me later\n        this.addBinding(this.options.bindings[name]);\n      }\n    });\n    this.addBinding({ key: 'Enter', shiftKey: null }, this.handleEnter);\n    this.addBinding(\n      { key: 'Enter', metaKey: null, ctrlKey: null, altKey: null },\n      () => {},\n    );\n    if (/Firefox/i.test(navigator.userAgent)) {\n      // Need to handle delete and backspace for Firefox in the general case #1171\n      this.addBinding(\n        { key: 'Backspace' },\n        { collapsed: true },\n        this.handleBackspace,\n      );\n      this.addBinding(\n        { key: 'Delete' },\n        { collapsed: true },\n        this.handleDelete,\n      );\n    } else {\n      this.addBinding(\n        { key: 'Backspace' },\n        { collapsed: true, prefix: /^.?$/ },\n        this.handleBackspace,\n      );\n      this.addBinding(\n        { key: 'Delete' },\n        { collapsed: true, suffix: /^.?$/ },\n        this.handleDelete,\n      );\n    }\n    this.addBinding(\n      { key: 'Backspace' },\n      { collapsed: false },\n      this.handleDeleteRange,\n    );\n    this.addBinding(\n      { key: 'Delete' },\n      { collapsed: false },\n      this.handleDeleteRange,\n    );\n    this.addBinding(\n      {\n        key: 'Backspace',\n        altKey: null,\n        ctrlKey: null,\n        metaKey: null,\n        shiftKey: null,\n      },\n      { collapsed: true, offset: 0 },\n      this.handleBackspace,\n    );\n    this.listen();\n  }\n\n  addBinding(\n    keyBinding: Binding,\n    context:\n      | Required<BindingObject['handler']>\n      | Partial<Omit<BindingObject, 'key' | 'handler'>> = {},\n    handler:\n      | Required<BindingObject['handler']>\n      | Partial<Omit<BindingObject, 'key' | 'handler'>> = {},\n  ) {\n    const binding = normalize(keyBinding);\n    if (binding == null) {\n      debug.warn('Attempted to add invalid keyboard binding', binding);\n      return;\n    }\n    if (typeof context === 'function') {\n      context = { handler: context };\n    }\n    if (typeof handler === 'function') {\n      handler = { handler };\n    }\n    const keys = Array.isArray(binding.key) ? binding.key : [binding.key];\n    keys.forEach((key) => {\n      const singleBinding = {\n        ...binding,\n        key,\n        ...context,\n        ...handler,\n      };\n      this.bindings[singleBinding.key] = this.bindings[singleBinding.key] || [];\n      this.bindings[singleBinding.key].push(singleBinding);\n    });\n  }\n\n  listen() {\n    this.quill.root.addEventListener('keydown', (evt) => {\n      if (evt.defaultPrevented || evt.isComposing) return;\n\n      // evt.isComposing is false when pressing Enter/Backspace when composing in Safari\n      // https://bugs.webkit.org/show_bug.cgi?id=165004\n      const isComposing =\n        evt.keyCode === 229 && (evt.key === 'Enter' || evt.key === 'Backspace');\n      if (isComposing) return;\n\n      const bindings = (this.bindings[evt.key] || []).concat(\n        this.bindings[evt.which] || [],\n      );\n      const matches = bindings.filter((binding) =>\n        Keyboard.match(evt, binding),\n      );\n      if (matches.length === 0) return;\n      // @ts-expect-error\n      const blot = Quill.find(evt.target, true);\n      if (blot && blot.scroll !== this.quill.scroll) return;\n      const range = this.quill.getSelection();\n      if (range == null || !this.quill.hasFocus()) return;\n      const [line, offset] = this.quill.getLine(range.index);\n      const [leafStart, offsetStart] = this.quill.getLeaf(range.index);\n      const [leafEnd, offsetEnd] =\n        range.length === 0\n          ? [leafStart, offsetStart]\n          : this.quill.getLeaf(range.index + range.length);\n      const prefixText =\n        leafStart instanceof TextBlot\n          ? leafStart.value().slice(0, offsetStart)\n          : '';\n      const suffixText =\n        leafEnd instanceof TextBlot ? leafEnd.value().slice(offsetEnd) : '';\n      const curContext = {\n        collapsed: range.length === 0,\n        // @ts-expect-error Fix me later\n        empty: range.length === 0 && line.length() <= 1,\n        format: this.quill.getFormat(range),\n        line,\n        offset,\n        prefix: prefixText,\n        suffix: suffixText,\n        event: evt,\n      };\n      const prevented = matches.some((binding) => {\n        if (\n          binding.collapsed != null &&\n          binding.collapsed !== curContext.collapsed\n        ) {\n          return false;\n        }\n        if (binding.empty != null && binding.empty !== curContext.empty) {\n          return false;\n        }\n        if (binding.offset != null && binding.offset !== curContext.offset) {\n          return false;\n        }\n        if (Array.isArray(binding.format)) {\n          // any format is present\n          if (binding.format.every((name) => curContext.format[name] == null)) {\n            return false;\n          }\n        } else if (typeof binding.format === 'object') {\n          // all formats must match\n          if (\n            !Object.keys(binding.format).every((name) => {\n              // @ts-expect-error Fix me later\n              if (binding.format[name] === true)\n                return curContext.format[name] != null;\n              // @ts-expect-error Fix me later\n              if (binding.format[name] === false)\n                return curContext.format[name] == null;\n              // @ts-expect-error Fix me later\n              return isEqual(binding.format[name], curContext.format[name]);\n            })\n          ) {\n            return false;\n          }\n        }\n        if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) {\n          return false;\n        }\n        if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) {\n          return false;\n        }\n        // @ts-expect-error Fix me later\n        return binding.handler.call(this, range, curContext, binding) !== true;\n      });\n      if (prevented) {\n        evt.preventDefault();\n      }\n    });\n  }\n\n  handleBackspace(range: Range, context: Context) {\n    // Check for astral symbols\n    const length = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]$/.test(context.prefix)\n      ? 2\n      : 1;\n    if (range.index === 0 || this.quill.getLength() <= 1) return;\n    let formats = {};\n    const [line] = this.quill.getLine(range.index);\n    let delta = new Delta().retain(range.index - length).delete(length);\n    if (context.offset === 0) {\n      // Always deleting newline here, length always 1\n      const [prev] = this.quill.getLine(range.index - 1);\n      if (prev) {\n        const isPrevLineEmpty =\n          prev.statics.blotName === 'block' && prev.length() <= 1;\n        if (!isPrevLineEmpty) {\n          // @ts-expect-error Fix me later\n          const curFormats = line.formats();\n          const prevFormats = this.quill.getFormat(range.index - 1, 1);\n          formats = AttributeMap.diff(curFormats, prevFormats) || {};\n          if (Object.keys(formats).length > 0) {\n            // line.length() - 1 targets \\n in line, another -1 for newline being deleted\n            const formatDelta = new Delta()\n              // @ts-expect-error Fix me later\n              .retain(range.index + line.length() - 2)\n              .retain(1, formats);\n            delta = delta.compose(formatDelta);\n          }\n        }\n      }\n    }\n    this.quill.updateContents(delta, Quill.sources.USER);\n    this.quill.focus();\n  }\n\n  handleDelete(range: Range, context: Context) {\n    // Check for astral symbols\n    const length = /^[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/.test(context.suffix)\n      ? 2\n      : 1;\n    if (range.index >= this.quill.getLength() - length) return;\n    let formats = {};\n    const [line] = this.quill.getLine(range.index);\n    let delta = new Delta().retain(range.index).delete(length);\n    // @ts-expect-error Fix me later\n    if (context.offset >= line.length() - 1) {\n      const [next] = this.quill.getLine(range.index + 1);\n      if (next) {\n        // @ts-expect-error Fix me later\n        const curFormats = line.formats();\n        const nextFormats = this.quill.getFormat(range.index, 1);\n        formats = AttributeMap.diff(curFormats, nextFormats) || {};\n        if (Object.keys(formats).length > 0) {\n          delta = delta.retain(next.length() - 1).retain(1, formats);\n        }\n      }\n    }\n    this.quill.updateContents(delta, Quill.sources.USER);\n    this.quill.focus();\n  }\n\n  handleDeleteRange(range: Range) {\n    deleteRange({ range, quill: this.quill });\n    this.quill.focus();\n  }\n\n  handleEnter(range: Range, context: Context) {\n    const lineFormats = Object.keys(context.format).reduce(\n      (formats: Record<string, unknown>, format) => {\n        if (\n          this.quill.scroll.query(format, Scope.BLOCK) &&\n          !Array.isArray(context.format[format])\n        ) {\n          formats[format] = context.format[format];\n        }\n        return formats;\n      },\n      {},\n    );\n    const delta = new Delta()\n      .retain(range.index)\n      .delete(range.length)\n      .insert('\\n', lineFormats);\n    this.quill.updateContents(delta, Quill.sources.USER);\n    this.quill.setSelection(range.index + 1, Quill.sources.SILENT);\n    this.quill.focus();\n  }\n}\n\nconst defaultOptions: KeyboardOptions = {\n  bindings: {\n    bold: makeFormatHandler('bold'),\n    italic: makeFormatHandler('italic'),\n    underline: makeFormatHandler('underline'),\n    indent: {\n      // highlight tab or tab at beginning of list, indent or blockquote\n      key: 'Tab',\n      format: ['blockquote', 'indent', 'list'],\n      handler(range, context) {\n        if (context.collapsed && context.offset !== 0) return true;\n        this.quill.format('indent', '+1', Quill.sources.USER);\n        return false;\n      },\n    },\n    outdent: {\n      key: 'Tab',\n      shiftKey: true,\n      format: ['blockquote', 'indent', 'list'],\n      // highlight tab or tab at beginning of list, indent or blockquote\n      handler(range, context) {\n        if (context.collapsed && context.offset !== 0) return true;\n        this.quill.format('indent', '-1', Quill.sources.USER);\n        return false;\n      },\n    },\n    'outdent backspace': {\n      key: 'Backspace',\n      collapsed: true,\n      shiftKey: null,\n      metaKey: null,\n      ctrlKey: null,\n      altKey: null,\n      format: ['indent', 'list'],\n      offset: 0,\n      handler(range, context) {\n        if (context.format.indent != null) {\n          this.quill.format('indent', '-1', Quill.sources.USER);\n        } else if (context.format.list != null) {\n          this.quill.format('list', false, Quill.sources.USER);\n        }\n      },\n    },\n    'indent code-block': makeCodeBlockHandler(true),\n    'outdent code-block': makeCodeBlockHandler(false),\n    'remove tab': {\n      key: 'Tab',\n      shiftKey: true,\n      collapsed: true,\n      prefix: /\\t$/,\n      handler(range) {\n        this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);\n      },\n    },\n    tab: {\n      key: 'Tab',\n      handler(range, context) {\n        if (context.format.table) return true;\n        this.quill.history.cutoff();\n        const delta = new Delta()\n          .retain(range.index)\n          .delete(range.length)\n          .insert('\\t');\n        this.quill.updateContents(delta, Quill.sources.USER);\n        this.quill.history.cutoff();\n        this.quill.setSelection(range.index + 1, Quill.sources.SILENT);\n        return false;\n      },\n    },\n    'blockquote empty enter': {\n      key: 'Enter',\n      collapsed: true,\n      format: ['blockquote'],\n      empty: true,\n      handler() {\n        this.quill.format('blockquote', false, Quill.sources.USER);\n      },\n    },\n    'list empty enter': {\n      key: 'Enter',\n      collapsed: true,\n      format: ['list'],\n      empty: true,\n      handler(range, context) {\n        const formats: Record<string, unknown> = { list: false };\n        if (context.format.indent) {\n          formats.indent = false;\n        }\n        this.quill.formatLine(\n          range.index,\n          range.length,\n          formats,\n          Quill.sources.USER,\n        );\n      },\n    },\n    'checklist enter': {\n      key: 'Enter',\n      collapsed: true,\n      format: { list: 'checked' },\n      handler(range) {\n        const [line, offset] = this.quill.getLine(range.index);\n        const formats = {\n          // @ts-expect-error Fix me later\n          ...line.formats(),\n          list: 'checked',\n        };\n        const delta = new Delta()\n          .retain(range.index)\n          .insert('\\n', formats)\n          // @ts-expect-error Fix me later\n          .retain(line.length() - offset - 1)\n          .retain(1, { list: 'unchecked' });\n        this.quill.updateContents(delta, Quill.sources.USER);\n        this.quill.setSelection(range.index + 1, Quill.sources.SILENT);\n        this.quill.scrollSelectionIntoView();\n      },\n    },\n    'header enter': {\n      key: 'Enter',\n      collapsed: true,\n      format: ['header'],\n      suffix: /^$/,\n      handler(range, context) {\n        const [line, offset] = this.quill.getLine(range.index);\n        const delta = new Delta()\n          .retain(range.index)\n          .insert('\\n', context.format)\n          // @ts-expect-error Fix me later\n          .retain(line.length() - offset - 1)\n          .retain(1, { header: null });\n        this.quill.updateContents(delta, Quill.sources.USER);\n        this.quill.setSelection(range.index + 1, Quill.sources.SILENT);\n        this.quill.scrollSelectionIntoView();\n      },\n    },\n    'table backspace': {\n      key: 'Backspace',\n      format: ['table'],\n      collapsed: true,\n      offset: 0,\n      handler() {},\n    },\n    'table delete': {\n      key: 'Delete',\n      format: ['table'],\n      collapsed: true,\n      suffix: /^$/,\n      handler() {},\n    },\n    'table enter': {\n      key: 'Enter',\n      shiftKey: null,\n      format: ['table'],\n      handler(range) {\n        const module = this.quill.getModule('table');\n        if (module) {\n          // @ts-expect-error\n          const [table, row, cell, offset] = module.getTable(range);\n          const shift = tableSide(table, row, cell, offset);\n          if (shift == null) return;\n          let index = table.offset();\n          if (shift < 0) {\n            const delta = new Delta().retain(index).insert('\\n');\n            this.quill.updateContents(delta, Quill.sources.USER);\n            this.quill.setSelection(\n              range.index + 1,\n              range.length,\n              Quill.sources.SILENT,\n            );\n          } else if (shift > 0) {\n            index += table.length();\n            const delta = new Delta().retain(index).insert('\\n');\n            this.quill.updateContents(delta, Quill.sources.USER);\n            this.quill.setSelection(index, Quill.sources.USER);\n          }\n        }\n      },\n    },\n    'table tab': {\n      key: 'Tab',\n      shiftKey: null,\n      format: ['table'],\n      handler(range, context) {\n        const { event, line: cell } = context;\n        const offset = cell.offset(this.quill.scroll);\n        if (event.shiftKey) {\n          this.quill.setSelection(offset - 1, Quill.sources.USER);\n        } else {\n          this.quill.setSelection(offset + cell.length(), Quill.sources.USER);\n        }\n      },\n    },\n    'list autofill': {\n      key: ' ',\n      shiftKey: null,\n      collapsed: true,\n      format: {\n        'code-block': false,\n        blockquote: false,\n        table: false,\n      },\n      prefix: /^\\s*?(\\d+\\.|-|\\*|\\[ ?\\]|\\[x\\])$/,\n      handler(range, context) {\n        if (this.quill.scroll.query('list') == null) return true;\n        const { length } = context.prefix;\n        const [line, offset] = this.quill.getLine(range.index);\n        if (offset > length) return true;\n        let value;\n        switch (context.prefix.trim()) {\n          case '[]':\n          case '[ ]':\n            value = 'unchecked';\n            break;\n          case '[x]':\n            value = 'checked';\n            break;\n          case '-':\n          case '*':\n            value = 'bullet';\n            break;\n          default:\n            value = 'ordered';\n        }\n        this.quill.insertText(range.index, ' ', Quill.sources.USER);\n        this.quill.history.cutoff();\n        const delta = new Delta()\n          .retain(range.index - offset)\n          .delete(length + 1)\n          // @ts-expect-error Fix me later\n          .retain(line.length() - 2 - offset)\n          .retain(1, { list: value });\n        this.quill.updateContents(delta, Quill.sources.USER);\n        this.quill.history.cutoff();\n        this.quill.setSelection(range.index - length, Quill.sources.SILENT);\n        return false;\n      },\n    },\n    'code exit': {\n      key: 'Enter',\n      collapsed: true,\n      format: ['code-block'],\n      prefix: /^$/,\n      suffix: /^\\s*$/,\n      handler(range) {\n        const [line, offset] = this.quill.getLine(range.index);\n        let numLines = 2;\n        let cur = line;\n        while (\n          cur != null &&\n          cur.length() <= 1 &&\n          cur.formats()['code-block']\n        ) {\n          // @ts-expect-error\n          cur = cur.prev;\n          numLines -= 1;\n          // Requisite prev lines are empty\n          if (numLines <= 0) {\n            const delta = new Delta()\n              // @ts-expect-error Fix me later\n              .retain(range.index + line.length() - offset - 2)\n              .retain(1, { 'code-block': null })\n              .delete(1);\n            this.quill.updateContents(delta, Quill.sources.USER);\n            this.quill.setSelection(range.index - 1, Quill.sources.SILENT);\n            return false;\n          }\n        }\n        return true;\n      },\n    },\n    'embed left': makeEmbedArrowHandler('ArrowLeft', false),\n    'embed left shift': makeEmbedArrowHandler('ArrowLeft', true),\n    'embed right': makeEmbedArrowHandler('ArrowRight', false),\n    'embed right shift': makeEmbedArrowHandler('ArrowRight', true),\n    'table down': makeTableArrowHandler(false),\n    'table up': makeTableArrowHandler(true),\n  },\n};\n\nKeyboard.DEFAULTS = defaultOptions;\n\nfunction makeCodeBlockHandler(indent: boolean): BindingObject {\n  return {\n    key: 'Tab',\n    shiftKey: !indent,\n    format: { 'code-block': true },\n    handler(range, { event }) {\n      const CodeBlock = this.quill.scroll.query('code-block');\n      // @ts-expect-error\n      const { TAB } = CodeBlock;\n      if (range.length === 0 && !event.shiftKey) {\n        this.quill.insertText(range.index, TAB, Quill.sources.USER);\n        this.quill.setSelection(range.index + TAB.length, Quill.sources.SILENT);\n        return;\n      }\n\n      const lines =\n        range.length === 0\n          ? this.quill.getLines(range.index, 1)\n          : this.quill.getLines(range);\n      let { index, length } = range;\n      lines.forEach((line, i) => {\n        if (indent) {\n          line.insertAt(0, TAB);\n          if (i === 0) {\n            index += TAB.length;\n          } else {\n            length += TAB.length;\n          }\n          // @ts-expect-error Fix me later\n        } else if (line.domNode.textContent.startsWith(TAB)) {\n          line.deleteAt(0, TAB.length);\n          if (i === 0) {\n            index -= TAB.length;\n          } else {\n            length -= TAB.length;\n          }\n        }\n      });\n      this.quill.update(Quill.sources.USER);\n      this.quill.setSelection(index, length, Quill.sources.SILENT);\n    },\n  };\n}\n\nfunction makeEmbedArrowHandler(\n  key: string,\n  shiftKey: boolean | null,\n): BindingObject {\n  const where = key === 'ArrowLeft' ? 'prefix' : 'suffix';\n  return {\n    key,\n    shiftKey,\n    altKey: null,\n    [where]: /^$/,\n    handler(range) {\n      let { index } = range;\n      if (key === 'ArrowRight') {\n        index += range.length + 1;\n      }\n      const [leaf] = this.quill.getLeaf(index);\n      if (!(leaf instanceof EmbedBlot)) return true;\n      if (key === 'ArrowLeft') {\n        if (shiftKey) {\n          this.quill.setSelection(\n            range.index - 1,\n            range.length + 1,\n            Quill.sources.USER,\n          );\n        } else {\n          this.quill.setSelection(range.index - 1, Quill.sources.USER);\n        }\n      } else if (shiftKey) {\n        this.quill.setSelection(\n          range.index,\n          range.length + 1,\n          Quill.sources.USER,\n        );\n      } else {\n        this.quill.setSelection(\n          range.index + range.length + 1,\n          Quill.sources.USER,\n        );\n      }\n      return false;\n    },\n  };\n}\n\nfunction makeFormatHandler(format: string): BindingObject {\n  return {\n    key: format[0],\n    shortKey: true,\n    handler(range, context) {\n      this.quill.format(format, !context.format[format], Quill.sources.USER);\n    },\n  };\n}\n\nfunction makeTableArrowHandler(up: boolean): BindingObject {\n  return {\n    key: up ? 'ArrowUp' : 'ArrowDown',\n    collapsed: true,\n    format: ['table'],\n    handler(range, context) {\n      // TODO move to table module\n      const key = up ? 'prev' : 'next';\n      const cell = context.line;\n      const targetRow = cell.parent[key];\n      if (targetRow != null) {\n        if (targetRow.statics.blotName === 'table-row') {\n          // @ts-expect-error\n          let targetCell = targetRow.children.head;\n          let cur = cell;\n          while (cur.prev != null) {\n            // @ts-expect-error\n            cur = cur.prev;\n            targetCell = targetCell.next;\n          }\n          const index =\n            targetCell.offset(this.quill.scroll) +\n            Math.min(context.offset, targetCell.length() - 1);\n          this.quill.setSelection(index, 0, Quill.sources.USER);\n        }\n      } else {\n        // @ts-expect-error\n        const targetLine = cell.table()[key];\n        if (targetLine != null) {\n          if (up) {\n            this.quill.setSelection(\n              targetLine.offset(this.quill.scroll) + targetLine.length() - 1,\n              0,\n              Quill.sources.USER,\n            );\n          } else {\n            this.quill.setSelection(\n              targetLine.offset(this.quill.scroll),\n              0,\n              Quill.sources.USER,\n            );\n          }\n        }\n      }\n      return false;\n    },\n  };\n}\n\nfunction normalize(binding: Binding): BindingObject | null {\n  if (typeof binding === 'string' || typeof binding === 'number') {\n    binding = { key: binding };\n  } else if (typeof binding === 'object') {\n    binding = cloneDeep(binding);\n  } else {\n    return null;\n  }\n  if (binding.shortKey) {\n    binding[SHORTKEY] = binding.shortKey;\n    delete binding.shortKey;\n  }\n  return binding;\n}\n\n// TODO: Move into quill.ts or editor.ts\nfunction deleteRange({ quill, range }: { quill: Quill; range: Range }) {\n  const lines = quill.getLines(range);\n  let formats = {};\n  if (lines.length > 1) {\n    const firstFormats = lines[0].formats();\n    const lastFormats = lines[lines.length - 1].formats();\n    formats = AttributeMap.diff(lastFormats, firstFormats) || {};\n  }\n  quill.deleteText(range, Quill.sources.USER);\n  if (Object.keys(formats).length > 0) {\n    quill.formatLine(range.index, 1, formats, Quill.sources.USER);\n  }\n  quill.setSelection(range.index, Quill.sources.SILENT);\n}\n\nfunction tableSide(_table: unknown, row: Blot, cell: Blot, offset: number) {\n  if (row.prev == null && row.next == null) {\n    if (cell.prev == null && cell.next == null) {\n      return offset === 0 ? -1 : 1;\n    }\n    return cell.prev == null ? -1 : 1;\n  }\n  if (row.prev == null) {\n    return -1;\n  }\n  if (row.next == null) {\n    return 1;\n  }\n  return null;\n}\n\nexport { Keyboard as default, SHORTKEY, normalize, deleteRange };\n"
  },
  {
    "path": "packages/quill/src/modules/normalizeExternalHTML/index.ts",
    "content": "import googleDocs from './normalizers/googleDocs.js';\nimport msWord from './normalizers/msWord.js';\n\nconst NORMALIZERS = [msWord, googleDocs];\n\nconst normalizeExternalHTML = (doc: Document) => {\n  if (doc.documentElement) {\n    NORMALIZERS.forEach((normalize) => {\n      normalize(doc);\n    });\n  }\n};\n\nexport default normalizeExternalHTML;\n"
  },
  {
    "path": "packages/quill/src/modules/normalizeExternalHTML/normalizers/googleDocs.ts",
    "content": "const normalWeightRegexp = /font-weight:\\s*normal/;\nconst blockTagNames = ['P', 'OL', 'UL'];\n\nconst isBlockElement = (element: Element | null) => {\n  return element && blockTagNames.includes(element.tagName);\n};\n\nconst normalizeEmptyLines = (doc: Document) => {\n  Array.from(doc.querySelectorAll('br'))\n    .filter(\n      (br) =>\n        isBlockElement(br.previousElementSibling) &&\n        isBlockElement(br.nextElementSibling),\n    )\n    .forEach((br) => {\n      br.parentNode?.removeChild(br);\n    });\n};\n\nconst normalizeFontWeight = (doc: Document) => {\n  Array.from(doc.querySelectorAll('b[style*=\"font-weight\"]'))\n    .filter((node) => node.getAttribute('style')?.match(normalWeightRegexp))\n    .forEach((node) => {\n      const fragment = doc.createDocumentFragment();\n      fragment.append(...node.childNodes);\n      node.parentNode?.replaceChild(fragment, node);\n    });\n};\n\nexport default function normalize(doc: Document) {\n  if (doc.querySelector('[id^=\"docs-internal-guid-\"]')) {\n    normalizeFontWeight(doc);\n    normalizeEmptyLines(doc);\n  }\n}\n"
  },
  {
    "path": "packages/quill/src/modules/normalizeExternalHTML/normalizers/msWord.ts",
    "content": "const ignoreRegexp = /\\bmso-list:[^;]*ignore/i;\nconst idRegexp = /\\bmso-list:[^;]*\\bl(\\d+)/i;\nconst indentRegexp = /\\bmso-list:[^;]*\\blevel(\\d+)/i;\n\nconst parseListItem = (element: Element, html: string) => {\n  const style = element.getAttribute('style');\n  const idMatch = style?.match(idRegexp);\n  if (!idMatch) {\n    return null;\n  }\n  const id = Number(idMatch[1]);\n\n  const indentMatch = style?.match(indentRegexp);\n  const indent = indentMatch ? Number(indentMatch[1]) : 1;\n\n  const typeRegexp = new RegExp(\n    `@list l${id}:level${indent}\\\\s*\\\\{[^\\\\}]*mso-level-number-format:\\\\s*([\\\\w-]+)`,\n    'i',\n  );\n  const typeMatch = html.match(typeRegexp);\n  const type = typeMatch && typeMatch[1] === 'bullet' ? 'bullet' : 'ordered';\n\n  return { id, indent, type, element };\n};\n\n// list items are represented as `p` tags with styles like `mso-list: l0 level1` where:\n// 1. \"0\" in \"l0\" means the list item id;\n// 2. \"1\" in \"level1\" means the indent level, starting from 1.\nconst normalizeListItem = (doc: Document) => {\n  const msoList = Array.from(doc.querySelectorAll('[style*=mso-list]'));\n  const ignored: Element[] = [];\n  const others: Element[] = [];\n  msoList.forEach((node) => {\n    const shouldIgnore = (node.getAttribute('style') || '').match(ignoreRegexp);\n    if (shouldIgnore) {\n      ignored.push(node);\n    } else {\n      others.push(node);\n    }\n  });\n\n  // Each list item contains a marker wrapped with \"mso-list: Ignore\".\n  ignored.forEach((node) => node.parentNode?.removeChild(node));\n\n  // The list stype is not defined inline with the tag, instead, it's in the\n  // style tag so we need to pass the html as a string.\n  const html = doc.documentElement.innerHTML;\n  const listItems = others\n    .map((element) => parseListItem(element, html))\n    .filter((parsed) => parsed);\n\n  while (listItems.length) {\n    const childListItems = [];\n\n    let current = listItems.shift();\n    // Group continuous items into the same group (aka \"ul\")\n    while (current) {\n      childListItems.push(current);\n      current =\n        listItems.length &&\n        listItems[0]?.element === current.element.nextElementSibling &&\n        // Different id means the next item doesn't belong to this group.\n        listItems[0].id === current.id\n          ? listItems.shift()\n          : null;\n    }\n\n    const ul = document.createElement('ul');\n    childListItems.forEach((listItem) => {\n      const li = document.createElement('li');\n      li.setAttribute('data-list', listItem.type);\n      if (listItem.indent > 1) {\n        li.setAttribute('class', `ql-indent-${listItem.indent - 1}`);\n      }\n      li.innerHTML = listItem.element.innerHTML;\n      ul.appendChild(li);\n    });\n\n    const element = childListItems[0]?.element;\n    const { parentNode } = element ?? {};\n    if (element) {\n      parentNode?.replaceChild(ul, element);\n    }\n    childListItems.slice(1).forEach(({ element: e }) => {\n      parentNode?.removeChild(e);\n    });\n  }\n};\n\nexport default function normalize(doc: Document) {\n  if (\n    doc.documentElement.getAttribute('xmlns:w') ===\n    'urn:schemas-microsoft-com:office:word'\n  ) {\n    normalizeListItem(doc);\n  }\n}\n"
  },
  {
    "path": "packages/quill/src/modules/syntax.ts",
    "content": "import Delta from 'quill-delta';\nimport { ClassAttributor, Scope } from 'parchment';\nimport type { Blot, ScrollBlot } from 'parchment';\nimport Inline from '../blots/inline.js';\nimport Quill from '../core/quill.js';\nimport Module from '../core/module.js';\nimport { blockDelta } from '../blots/block.js';\nimport BreakBlot from '../blots/break.js';\nimport CursorBlot from '../blots/cursor.js';\nimport TextBlot, { escapeText } from '../blots/text.js';\nimport CodeBlock, { CodeBlockContainer } from '../formats/code.js';\nimport { traverse } from './clipboard.js';\n\nconst TokenAttributor = new ClassAttributor('code-token', 'hljs', {\n  scope: Scope.INLINE,\n});\nclass CodeToken extends Inline {\n  static formats(node: Element, scroll: ScrollBlot) {\n    while (node != null && node !== scroll.domNode) {\n      if (node.classList && node.classList.contains(CodeBlock.className)) {\n        // @ts-expect-error\n        return super.formats(node, scroll);\n      }\n      // @ts-expect-error\n      node = node.parentNode;\n    }\n    return undefined;\n  }\n\n  constructor(scroll: ScrollBlot, domNode: Node, value: unknown) {\n    // @ts-expect-error\n    super(scroll, domNode, value);\n    TokenAttributor.add(this.domNode, value);\n  }\n\n  format(format: string, value: unknown) {\n    if (format !== CodeToken.blotName) {\n      super.format(format, value);\n    } else if (value) {\n      TokenAttributor.add(this.domNode, value);\n    } else {\n      TokenAttributor.remove(this.domNode);\n      this.domNode.classList.remove(this.statics.className);\n    }\n  }\n\n  optimize(...args: unknown[]) {\n    // @ts-expect-error\n    super.optimize(...args);\n    if (!TokenAttributor.value(this.domNode)) {\n      this.unwrap();\n    }\n  }\n}\nCodeToken.blotName = 'code-token';\nCodeToken.className = 'ql-token';\n\nclass SyntaxCodeBlock extends CodeBlock {\n  static create(value: unknown) {\n    const domNode = super.create(value);\n    if (typeof value === 'string') {\n      domNode.setAttribute('data-language', value);\n    }\n    return domNode;\n  }\n\n  static formats(domNode: Node) {\n    // @ts-expect-error\n    return domNode.getAttribute('data-language') || 'plain';\n  }\n\n  static register() {} // Syntax module will register\n\n  format(name: string, value: unknown) {\n    if (name === this.statics.blotName && value) {\n      // @ts-expect-error\n      this.domNode.setAttribute('data-language', value);\n    } else {\n      super.format(name, value);\n    }\n  }\n\n  replaceWith(name: string | Blot, value?: any) {\n    this.formatAt(0, this.length(), CodeToken.blotName, false);\n    return super.replaceWith(name, value);\n  }\n}\n\nclass SyntaxCodeBlockContainer extends CodeBlockContainer {\n  forceNext?: boolean;\n  cachedText?: string | null;\n\n  attach() {\n    super.attach();\n    this.forceNext = false;\n    // @ts-expect-error\n    this.scroll.emitMount(this);\n  }\n\n  format(name: string, value: unknown) {\n    if (name === SyntaxCodeBlock.blotName) {\n      this.forceNext = true;\n      this.children.forEach((child) => {\n        // @ts-expect-error\n        child.format(name, value);\n      });\n    }\n  }\n\n  formatAt(index: number, length: number, name: string, value: unknown) {\n    if (name === SyntaxCodeBlock.blotName) {\n      this.forceNext = true;\n    }\n    super.formatAt(index, length, name, value);\n  }\n\n  highlight(\n    highlight: (text: string, language: string) => Delta,\n    forced = false,\n  ) {\n    if (this.children.head == null) return;\n    const nodes = Array.from(this.domNode.childNodes).filter(\n      (node) => node !== this.uiNode,\n    );\n    const text = `${nodes.map((node) => node.textContent).join('\\n')}\\n`;\n    const language = SyntaxCodeBlock.formats(this.children.head.domNode);\n    if (forced || this.forceNext || this.cachedText !== text) {\n      if (text.trim().length > 0 || this.cachedText == null) {\n        const oldDelta = this.children.reduce((delta, child) => {\n          // @ts-expect-error\n          return delta.concat(blockDelta(child, false));\n        }, new Delta());\n        const delta = highlight(text, language);\n        oldDelta.diff(delta).reduce((index, { retain, attributes }) => {\n          // Should be all retains\n          if (!retain) return index;\n          if (attributes) {\n            Object.keys(attributes).forEach((format) => {\n              if (\n                [SyntaxCodeBlock.blotName, CodeToken.blotName].includes(format)\n              ) {\n                // @ts-expect-error\n                this.formatAt(index, retain, format, attributes[format]);\n              }\n            });\n          }\n          // @ts-expect-error\n          return index + retain;\n        }, 0);\n      }\n      this.cachedText = text;\n      this.forceNext = false;\n    }\n  }\n\n  html(index: number, length: number) {\n    const [codeBlock] = this.children.find(index);\n    const language = codeBlock\n      ? SyntaxCodeBlock.formats(codeBlock.domNode)\n      : 'plain';\n\n    return `<pre data-language=\"${language}\">\\n${escapeText(\n      this.code(index, length),\n    )}\\n</pre>`;\n  }\n\n  optimize(context: Record<string, any>) {\n    super.optimize(context);\n    if (\n      this.parent != null &&\n      this.children.head != null &&\n      this.uiNode != null\n    ) {\n      const language = SyntaxCodeBlock.formats(this.children.head.domNode);\n      // @ts-expect-error\n      if (language !== this.uiNode.value) {\n        // @ts-expect-error\n        this.uiNode.value = language;\n      }\n    }\n  }\n}\n\nSyntaxCodeBlockContainer.allowedChildren = [SyntaxCodeBlock];\nSyntaxCodeBlock.requiredContainer = SyntaxCodeBlockContainer;\nSyntaxCodeBlock.allowedChildren = [CodeToken, CursorBlot, TextBlot, BreakBlot];\n\ninterface SyntaxOptions {\n  interval: number;\n  languages: { key: string; label: string }[];\n  hljs: any;\n}\n\nconst highlight = (lib: any, language: string, text: string) => {\n  if (typeof lib.versionString === 'string') {\n    const majorVersion = lib.versionString.split('.')[0];\n    if (parseInt(majorVersion, 10) >= 11) {\n      return lib.highlight(text, { language }).value;\n    }\n  }\n  return lib.highlight(language, text).value;\n};\n\nclass Syntax extends Module<SyntaxOptions> {\n  static DEFAULTS: SyntaxOptions & { hljs: any };\n\n  static register() {\n    Quill.register(CodeToken, true);\n    Quill.register(SyntaxCodeBlock, true);\n    Quill.register(SyntaxCodeBlockContainer, true);\n  }\n\n  languages: Record<string, true>;\n\n  constructor(quill: Quill, options: Partial<SyntaxOptions>) {\n    super(quill, options);\n    if (this.options.hljs == null) {\n      throw new Error(\n        'Syntax module requires highlight.js. Please include the library on the page before Quill.',\n      );\n    }\n    // @ts-expect-error Fix me later\n    this.languages = this.options.languages.reduce(\n      (memo: Record<string, unknown>, { key }) => {\n        memo[key] = true;\n        return memo;\n      },\n      {},\n    );\n    this.highlightBlot = this.highlightBlot.bind(this);\n    this.initListener();\n    this.initTimer();\n  }\n\n  initListener() {\n    this.quill.on(Quill.events.SCROLL_BLOT_MOUNT, (blot: Blot) => {\n      if (!(blot instanceof SyntaxCodeBlockContainer)) return;\n      const select = this.quill.root.ownerDocument.createElement('select');\n      // @ts-expect-error Fix me later\n      this.options.languages.forEach(({ key, label }) => {\n        const option = select.ownerDocument.createElement('option');\n        option.textContent = label;\n        option.setAttribute('value', key);\n        select.appendChild(option);\n      });\n      select.addEventListener('change', () => {\n        blot.format(SyntaxCodeBlock.blotName, select.value);\n        this.quill.root.focus(); // Prevent scrolling\n        this.highlight(blot, true);\n      });\n      if (blot.uiNode == null) {\n        blot.attachUI(select);\n        if (blot.children.head) {\n          select.value = SyntaxCodeBlock.formats(blot.children.head.domNode);\n        }\n      }\n    });\n  }\n\n  initTimer() {\n    let timer: ReturnType<typeof setTimeout> | null = null;\n    this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {\n      if (timer) {\n        clearTimeout(timer);\n      }\n      timer = setTimeout(() => {\n        this.highlight();\n        timer = null;\n      }, this.options.interval);\n    });\n  }\n\n  highlight(blot: SyntaxCodeBlockContainer | null = null, force = false) {\n    if (this.quill.selection.composing) return;\n    this.quill.update(Quill.sources.USER);\n    const range = this.quill.getSelection();\n    const blots =\n      blot == null\n        ? this.quill.scroll.descendants(SyntaxCodeBlockContainer)\n        : [blot];\n    blots.forEach((container) => {\n      container.highlight(this.highlightBlot, force);\n    });\n    this.quill.update(Quill.sources.SILENT);\n    if (range != null) {\n      this.quill.setSelection(range, Quill.sources.SILENT);\n    }\n  }\n\n  highlightBlot(text: string, language = 'plain') {\n    language = this.languages[language] ? language : 'plain';\n    if (language === 'plain') {\n      return escapeText(text)\n        .split('\\n')\n        .reduce((delta, line, i) => {\n          if (i !== 0) {\n            delta.insert('\\n', { [CodeBlock.blotName]: language });\n          }\n          return delta.insert(line);\n        }, new Delta());\n    }\n    const container = this.quill.root.ownerDocument.createElement('div');\n    container.classList.add(CodeBlock.className);\n    container.innerHTML = highlight(this.options.hljs, language, text);\n    return traverse(\n      this.quill.scroll,\n      container,\n      [\n        (node, delta) => {\n          // @ts-expect-error\n          const value = TokenAttributor.value(node);\n          if (value) {\n            return delta.compose(\n              new Delta().retain(delta.length(), {\n                [CodeToken.blotName]: value,\n              }),\n            );\n          }\n          return delta;\n        },\n      ],\n      [\n        (node, delta) => {\n          // @ts-expect-error\n          return node.data.split('\\n').reduce((memo, nodeText, i) => {\n            if (i !== 0) memo.insert('\\n', { [CodeBlock.blotName]: language });\n            return memo.insert(nodeText);\n          }, delta);\n        },\n      ],\n      new WeakMap(),\n    );\n  }\n}\nSyntax.DEFAULTS = {\n  hljs: (() => {\n    return window.hljs;\n  })(),\n  interval: 1000,\n  languages: [\n    { key: 'plain', label: 'Plain' },\n    { key: 'bash', label: 'Bash' },\n    { key: 'cpp', label: 'C++' },\n    { key: 'cs', label: 'C#' },\n    { key: 'css', label: 'CSS' },\n    { key: 'diff', label: 'Diff' },\n    { key: 'xml', label: 'HTML/XML' },\n    { key: 'java', label: 'Java' },\n    { key: 'javascript', label: 'JavaScript' },\n    { key: 'markdown', label: 'Markdown' },\n    { key: 'php', label: 'PHP' },\n    { key: 'python', label: 'Python' },\n    { key: 'ruby', label: 'Ruby' },\n    { key: 'sql', label: 'SQL' },\n  ],\n};\n\nexport { SyntaxCodeBlock as CodeBlock, CodeToken, Syntax as default };\n"
  },
  {
    "path": "packages/quill/src/modules/table.ts",
    "content": "import Delta from 'quill-delta';\nimport Quill from '../core/quill.js';\nimport Module from '../core/module.js';\nimport {\n  TableCell,\n  TableRow,\n  TableBody,\n  TableContainer,\n  tableId,\n} from '../formats/table.js';\n\nclass Table extends Module {\n  static register() {\n    Quill.register(TableCell);\n    Quill.register(TableRow);\n    Quill.register(TableBody);\n    Quill.register(TableContainer);\n  }\n\n  constructor(...args: ConstructorParameters<typeof Module>) {\n    super(...args);\n    this.listenBalanceCells();\n  }\n\n  balanceTables() {\n    this.quill.scroll.descendants(TableContainer).forEach((table) => {\n      table.balanceCells();\n    });\n  }\n\n  deleteColumn() {\n    const [table, , cell] = this.getTable();\n    if (cell == null) return;\n    // @ts-expect-error\n    table.deleteColumn(cell.cellOffset());\n    this.quill.update(Quill.sources.USER);\n  }\n\n  deleteRow() {\n    const [, row] = this.getTable();\n    if (row == null) return;\n    row.remove();\n    this.quill.update(Quill.sources.USER);\n  }\n\n  deleteTable() {\n    const [table] = this.getTable();\n    if (table == null) return;\n    // @ts-expect-error\n    const offset = table.offset();\n    // @ts-expect-error\n    table.remove();\n    this.quill.update(Quill.sources.USER);\n    this.quill.setSelection(offset, Quill.sources.SILENT);\n  }\n\n  getTable(\n    range = this.quill.getSelection(),\n  ): [null, null, null, -1] | [Table, TableRow, TableCell, number] {\n    if (range == null) return [null, null, null, -1];\n    const [cell, offset] = this.quill.getLine(range.index);\n    if (cell == null || cell.statics.blotName !== TableCell.blotName) {\n      return [null, null, null, -1];\n    }\n    const row = cell.parent;\n    const table = row.parent.parent;\n    // @ts-expect-error\n    return [table, row, cell, offset];\n  }\n\n  insertColumn(offset: number) {\n    const range = this.quill.getSelection();\n    if (!range) return;\n    const [table, row, cell] = this.getTable(range);\n    if (cell == null) return;\n    const column = cell.cellOffset();\n    table.insertColumn(column + offset);\n    this.quill.update(Quill.sources.USER);\n    let shift = row.rowOffset();\n    if (offset === 0) {\n      shift += 1;\n    }\n    this.quill.setSelection(\n      range.index + shift,\n      range.length,\n      Quill.sources.SILENT,\n    );\n  }\n\n  insertColumnLeft() {\n    this.insertColumn(0);\n  }\n\n  insertColumnRight() {\n    this.insertColumn(1);\n  }\n\n  insertRow(offset: number) {\n    const range = this.quill.getSelection();\n    if (!range) return;\n    const [table, row, cell] = this.getTable(range);\n    if (cell == null) return;\n    const index = row.rowOffset();\n    table.insertRow(index + offset);\n    this.quill.update(Quill.sources.USER);\n    if (offset > 0) {\n      this.quill.setSelection(range, Quill.sources.SILENT);\n    } else {\n      this.quill.setSelection(\n        range.index + row.children.length,\n        range.length,\n        Quill.sources.SILENT,\n      );\n    }\n  }\n\n  insertRowAbove() {\n    this.insertRow(0);\n  }\n\n  insertRowBelow() {\n    this.insertRow(1);\n  }\n\n  insertTable(rows: number, columns: number) {\n    const range = this.quill.getSelection();\n    if (range == null) return;\n    const delta = new Array(rows).fill(0).reduce((memo) => {\n      const text = new Array(columns).fill('\\n').join('');\n      return memo.insert(text, { table: tableId() });\n    }, new Delta().retain(range.index));\n    this.quill.updateContents(delta, Quill.sources.USER);\n    this.quill.setSelection(range.index, Quill.sources.SILENT);\n    this.balanceTables();\n  }\n\n  listenBalanceCells() {\n    this.quill.on(\n      Quill.events.SCROLL_OPTIMIZE,\n      (mutations: MutationRecord[]) => {\n        mutations.some((mutation) => {\n          if (\n            ['TD', 'TR', 'TBODY', 'TABLE'].includes(\n              (mutation.target as HTMLElement).tagName,\n            )\n          ) {\n            this.quill.once(Quill.events.TEXT_CHANGE, (delta, old, source) => {\n              if (source !== Quill.sources.USER) return;\n              this.balanceTables();\n            });\n            return true;\n          }\n          return false;\n        });\n      },\n    );\n  }\n}\n\nexport default Table;\n"
  },
  {
    "path": "packages/quill/src/modules/tableEmbed.ts",
    "content": "import Delta, { OpIterator } from 'quill-delta';\nimport type { Op, AttributeMap } from 'quill-delta';\nimport Module from '../core/module.js';\n\nexport type CellData = {\n  content?: Delta['ops'];\n  attributes?: Record<string, unknown>;\n};\n\nexport type TableRowColumnOp = Omit<Op, 'insert'> & {\n  insert?: { id: string };\n};\n\nexport interface TableData {\n  rows?: Delta['ops'];\n  columns?: Delta['ops'];\n  cells?: Record<string, CellData>;\n}\n\nconst parseCellIdentity = (identity: string) => {\n  const parts = identity.split(':');\n  return [Number(parts[0]) - 1, Number(parts[1]) - 1];\n};\n\nconst stringifyCellIdentity = (row: number, column: number) =>\n  `${row + 1}:${column + 1}`;\n\nexport const composePosition = (delta: Delta, index: number) => {\n  let newIndex = index;\n  const thisIter = new OpIterator(delta.ops);\n  let offset = 0;\n  while (thisIter.hasNext() && offset <= newIndex) {\n    const length = thisIter.peekLength();\n    const nextType = thisIter.peekType();\n    thisIter.next();\n    switch (nextType) {\n      case 'delete':\n        if (length > newIndex - offset) {\n          return null;\n        }\n        newIndex -= length;\n        break;\n      case 'insert':\n        newIndex += length;\n        offset += length;\n        break;\n      default:\n        offset += length;\n        break;\n    }\n  }\n  return newIndex;\n};\n\nconst compactCellData = ({\n  content,\n  attributes,\n}: {\n  content: Delta;\n  attributes: AttributeMap | undefined;\n}) => {\n  const data: CellData = {};\n  if (content.length() > 0) {\n    data.content = content.ops;\n  }\n  if (attributes && Object.keys(attributes).length > 0) {\n    data.attributes = attributes;\n  }\n  return Object.keys(data).length > 0 ? data : null;\n};\n\nconst compactTableData = ({\n  rows,\n  columns,\n  cells,\n}: {\n  rows: Delta;\n  columns: Delta;\n  cells: Record<string, CellData>;\n}) => {\n  const data: TableData = {};\n  if (rows.length() > 0) {\n    data.rows = rows.ops;\n  }\n\n  if (columns.length() > 0) {\n    data.columns = columns.ops;\n  }\n\n  if (Object.keys(cells).length) {\n    data.cells = cells;\n  }\n\n  return data;\n};\n\nconst reindexCellIdentities = (\n  cells: Record<string, CellData>,\n  { rows, columns }: { rows: Delta; columns: Delta },\n) => {\n  const reindexedCells: Record<string, CellData> = {};\n  Object.keys(cells).forEach((identity) => {\n    let [row, column] = parseCellIdentity(identity);\n\n    // @ts-expect-error Fix me later\n    row = composePosition(rows, row);\n    // @ts-expect-error Fix me later\n    column = composePosition(columns, column);\n\n    if (row !== null && column !== null) {\n      const newPosition = stringifyCellIdentity(row, column);\n      reindexedCells[newPosition] = cells[identity];\n    }\n  }, false);\n  return reindexedCells;\n};\n\nexport const tableHandler = {\n  compose(a: TableData, b: TableData, keepNull?: boolean) {\n    const rows = new Delta(a.rows || []).compose(new Delta(b.rows || []));\n    const columns = new Delta(a.columns || []).compose(\n      new Delta(b.columns || []),\n    );\n\n    const cells = reindexCellIdentities(a.cells || {}, {\n      rows: new Delta(b.rows || []),\n      columns: new Delta(b.columns || []),\n    });\n\n    Object.keys(b.cells || {}).forEach((identity) => {\n      const aCell = cells[identity] || {};\n      // @ts-expect-error Fix me later\n      const bCell = b.cells[identity];\n\n      const content = new Delta(aCell.content || []).compose(\n        new Delta(bCell.content || []),\n      );\n\n      const attributes = Delta.AttributeMap.compose(\n        aCell.attributes,\n        bCell.attributes,\n        keepNull,\n      );\n\n      const cell = compactCellData({ content, attributes });\n      if (cell) {\n        cells[identity] = cell;\n      } else {\n        delete cells[identity];\n      }\n    });\n\n    return compactTableData({ rows, columns, cells });\n  },\n  transform(a: TableData, b: TableData, priority: boolean) {\n    const aDeltas = {\n      rows: new Delta(a.rows || []),\n      columns: new Delta(a.columns || []),\n    };\n\n    const bDeltas = {\n      rows: new Delta(b.rows || []),\n      columns: new Delta(b.columns || []),\n    };\n\n    const rows = aDeltas.rows.transform(bDeltas.rows, priority);\n    const columns = aDeltas.columns.transform(bDeltas.columns, priority);\n\n    const cells = reindexCellIdentities(b.cells || {}, {\n      rows: bDeltas.rows.transform(aDeltas.rows, !priority),\n      columns: bDeltas.columns.transform(aDeltas.columns, !priority),\n    });\n\n    Object.keys(a.cells || {}).forEach((identity) => {\n      let [row, column] = parseCellIdentity(identity);\n      // @ts-expect-error Fix me later\n      row = composePosition(rows, row);\n      // @ts-expect-error Fix me later\n      column = composePosition(columns, column);\n\n      if (row !== null && column !== null) {\n        const newIdentity = stringifyCellIdentity(row, column);\n\n        // @ts-expect-error Fix me later\n        const aCell = a.cells[identity];\n        const bCell = cells[newIdentity];\n        if (bCell) {\n          const content = new Delta(aCell.content || []).transform(\n            new Delta(bCell.content || []),\n            priority,\n          );\n\n          const attributes = Delta.AttributeMap.transform(\n            aCell.attributes,\n            bCell.attributes,\n            priority,\n          );\n\n          const cell = compactCellData({ content, attributes });\n          if (cell) {\n            cells[newIdentity] = cell;\n          } else {\n            delete cells[newIdentity];\n          }\n        }\n      }\n    });\n\n    return compactTableData({ rows, columns, cells });\n  },\n  invert(change: TableData, base: TableData) {\n    const rows = new Delta(change.rows || []).invert(\n      new Delta(base.rows || []),\n    );\n    const columns = new Delta(change.columns || []).invert(\n      new Delta(base.columns || []),\n    );\n    const cells = reindexCellIdentities(change.cells || {}, {\n      rows,\n      columns,\n    });\n    Object.keys(cells).forEach((identity) => {\n      const changeCell = cells[identity] || {};\n      const baseCell = (base.cells || {})[identity] || {};\n      const content = new Delta(changeCell.content || []).invert(\n        new Delta(baseCell.content || []),\n      );\n      const attributes = Delta.AttributeMap.invert(\n        changeCell.attributes,\n        baseCell.attributes,\n      );\n      const cell = compactCellData({ content, attributes });\n      if (cell) {\n        cells[identity] = cell;\n      } else {\n        delete cells[identity];\n      }\n    });\n\n    // Cells may be removed when their row or column is removed\n    // by row/column deltas. We should add them back.\n    Object.keys(base.cells || {}).forEach((identity) => {\n      const [row, column] = parseCellIdentity(identity);\n      if (\n        composePosition(new Delta(change.rows || []), row) === null ||\n        composePosition(new Delta(change.columns || []), column) === null\n      ) {\n        // @ts-expect-error Fix me later\n        cells[identity] = base.cells[identity];\n      }\n    });\n\n    return compactTableData({ rows, columns, cells });\n  },\n};\n\nclass TableEmbed extends Module {\n  static register() {\n    Delta.registerEmbed('table-embed', tableHandler);\n  }\n}\n\nexport default TableEmbed;\n"
  },
  {
    "path": "packages/quill/src/modules/toolbar.ts",
    "content": "import Delta from 'quill-delta';\nimport { EmbedBlot, Scope } from 'parchment';\nimport Quill from '../core/quill.js';\nimport logger from '../core/logger.js';\nimport Module from '../core/module.js';\nimport type { Range } from '../core/selection.js';\n\nconst debug = logger('quill:toolbar');\n\ntype Handler = (this: Toolbar, value: any) => void;\n\nexport type ToolbarConfig = Array<\n  string[] | Array<string | Record<string, unknown>>\n>;\nexport interface ToolbarProps {\n  container?: HTMLElement | ToolbarConfig | null;\n  handlers?: Record<string, Handler>;\n  option?: number;\n  module?: boolean;\n  theme?: boolean;\n}\n\nclass Toolbar extends Module<ToolbarProps> {\n  static DEFAULTS: ToolbarProps;\n\n  container?: HTMLElement | null;\n  controls: [string, HTMLElement][];\n  handlers: Record<string, Handler>;\n\n  constructor(quill: Quill, options: Partial<ToolbarProps>) {\n    super(quill, options);\n    if (Array.isArray(this.options.container)) {\n      const container = document.createElement('div');\n      container.setAttribute('role', 'toolbar');\n      addControls(container, this.options.container);\n      quill.container?.parentNode?.insertBefore(container, quill.container);\n      this.container = container;\n    } else if (typeof this.options.container === 'string') {\n      this.container = document.querySelector(this.options.container);\n    } else {\n      this.container = this.options.container;\n    }\n    if (!(this.container instanceof HTMLElement)) {\n      debug.error('Container required for toolbar', this.options);\n      return;\n    }\n    this.container.classList.add('ql-toolbar');\n    this.controls = [];\n    this.handlers = {};\n    if (this.options.handlers) {\n      Object.keys(this.options.handlers).forEach((format) => {\n        const handler = this.options.handlers?.[format];\n        if (handler) {\n          this.addHandler(format, handler);\n        }\n      });\n    }\n    Array.from(this.container.querySelectorAll('button, select')).forEach(\n      (input) => {\n        // @ts-expect-error\n        this.attach(input);\n      },\n    );\n    this.quill.on(Quill.events.EDITOR_CHANGE, () => {\n      const [range] = this.quill.selection.getRange(); // quill.getSelection triggers update\n      this.update(range);\n    });\n  }\n\n  addHandler(format: string, handler: Handler) {\n    this.handlers[format] = handler;\n  }\n\n  attach(input: HTMLElement) {\n    let format = Array.from(input.classList).find((className) => {\n      return className.indexOf('ql-') === 0;\n    });\n    if (!format) return;\n    format = format.slice('ql-'.length);\n    if (input.tagName === 'BUTTON') {\n      input.setAttribute('type', 'button');\n    }\n    if (\n      this.handlers[format] == null &&\n      this.quill.scroll.query(format) == null\n    ) {\n      debug.warn('ignoring attaching to nonexistent format', format, input);\n      return;\n    }\n    const eventName = input.tagName === 'SELECT' ? 'change' : 'click';\n    input.addEventListener(eventName, (e) => {\n      let value;\n      if (input.tagName === 'SELECT') {\n        // @ts-expect-error\n        if (input.selectedIndex < 0) return;\n        // @ts-expect-error\n        const selected = input.options[input.selectedIndex];\n        if (selected.hasAttribute('selected')) {\n          value = false;\n        } else {\n          value = selected.value || false;\n        }\n      } else {\n        if (input.classList.contains('ql-active')) {\n          value = false;\n        } else {\n          // @ts-expect-error\n          value = input.value || !input.hasAttribute('value');\n        }\n        e.preventDefault();\n      }\n      this.quill.focus();\n      const [range] = this.quill.selection.getRange();\n      if (this.handlers[format] != null) {\n        this.handlers[format].call(this, value);\n      } else if (\n        // @ts-expect-error\n        this.quill.scroll.query(format).prototype instanceof EmbedBlot\n      ) {\n        value = prompt(`Enter ${format}`); // eslint-disable-line no-alert\n        if (!value) return;\n        this.quill.updateContents(\n          new Delta()\n            // @ts-expect-error Fix me later\n            .retain(range.index)\n            // @ts-expect-error Fix me later\n            .delete(range.length)\n            .insert({ [format]: value }),\n          Quill.sources.USER,\n        );\n      } else {\n        this.quill.format(format, value, Quill.sources.USER);\n      }\n      this.update(range);\n    });\n    this.controls.push([format, input]);\n  }\n\n  update(range: Range | null) {\n    const formats = range == null ? {} : this.quill.getFormat(range);\n    this.controls.forEach((pair) => {\n      const [format, input] = pair;\n      if (input.tagName === 'SELECT') {\n        let option: HTMLOptionElement | null = null;\n        if (range == null) {\n          option = null;\n        } else if (formats[format] == null) {\n          option = input.querySelector('option[selected]');\n        } else if (!Array.isArray(formats[format])) {\n          let value = formats[format];\n          if (typeof value === 'string') {\n            value = value.replace(/\"/g, '\\\\\"');\n          }\n          option = input.querySelector(`option[value=\"${value}\"]`);\n        }\n        if (option == null) {\n          // @ts-expect-error TODO fix me later\n          input.value = ''; // TODO make configurable?\n          // @ts-expect-error TODO fix me later\n          input.selectedIndex = -1;\n        } else {\n          option.selected = true;\n        }\n      } else if (range == null) {\n        input.classList.remove('ql-active');\n        input.setAttribute('aria-pressed', 'false');\n      } else if (input.hasAttribute('value')) {\n        // both being null should match (default values)\n        // '1' should match with 1 (headers)\n        const value = formats[format] as boolean | number | string | object;\n        const isActive =\n          value === input.getAttribute('value') ||\n          (value != null && value.toString() === input.getAttribute('value')) ||\n          (value == null && !input.getAttribute('value'));\n        input.classList.toggle('ql-active', isActive);\n        input.setAttribute('aria-pressed', isActive.toString());\n      } else {\n        const isActive = formats[format] != null;\n        input.classList.toggle('ql-active', isActive);\n        input.setAttribute('aria-pressed', isActive.toString());\n      }\n    });\n  }\n}\nToolbar.DEFAULTS = {};\n\nfunction addButton(container: HTMLElement, format: string, value?: string) {\n  const input = document.createElement('button');\n  input.setAttribute('type', 'button');\n  input.classList.add(`ql-${format}`);\n  input.setAttribute('aria-pressed', 'false');\n  if (value != null) {\n    input.value = value;\n    input.setAttribute('aria-label', `${format}: ${value}`);\n  } else {\n    input.setAttribute('aria-label', format);\n  }\n  container.appendChild(input);\n}\n\nfunction addControls(\n  container: HTMLElement,\n  groups:\n    | (string | Record<string, unknown>)[][]\n    | (string | Record<string, unknown>)[],\n) {\n  if (!Array.isArray(groups[0])) {\n    // @ts-expect-error\n    groups = [groups];\n  }\n  groups.forEach((controls: any) => {\n    const group = document.createElement('span');\n    group.classList.add('ql-formats');\n    controls.forEach((control: any) => {\n      if (typeof control === 'string') {\n        addButton(group, control);\n      } else {\n        const format = Object.keys(control)[0];\n        const value = control[format];\n        if (Array.isArray(value)) {\n          addSelect(group, format, value);\n        } else {\n          addButton(group, format, value);\n        }\n      }\n    });\n    container.appendChild(group);\n  });\n}\n\nfunction addSelect(\n  container: HTMLElement,\n  format: string,\n  values: Array<string | boolean>,\n) {\n  const input = document.createElement('select');\n  input.classList.add(`ql-${format}`);\n  values.forEach((value) => {\n    const option = document.createElement('option');\n    if (value !== false) {\n      option.setAttribute('value', String(value));\n    } else {\n      option.setAttribute('selected', 'selected');\n    }\n    input.appendChild(option);\n  });\n  container.appendChild(input);\n}\n\nToolbar.DEFAULTS = {\n  container: null,\n  handlers: {\n    clean() {\n      const range = this.quill.getSelection();\n      if (range == null) return;\n      if (range.length === 0) {\n        const formats = this.quill.getFormat();\n        Object.keys(formats).forEach((name) => {\n          // Clean functionality in existing apps only clean inline formats\n          if (this.quill.scroll.query(name, Scope.INLINE) != null) {\n            this.quill.format(name, false, Quill.sources.USER);\n          }\n        });\n      } else {\n        this.quill.removeFormat(range.index, range.length, Quill.sources.USER);\n      }\n    },\n    direction(value) {\n      const { align } = this.quill.getFormat();\n      if (value === 'rtl' && align == null) {\n        this.quill.format('align', 'right', Quill.sources.USER);\n      } else if (!value && align === 'right') {\n        this.quill.format('align', false, Quill.sources.USER);\n      }\n      this.quill.format('direction', value, Quill.sources.USER);\n    },\n    indent(value) {\n      const range = this.quill.getSelection();\n      // @ts-expect-error\n      const formats = this.quill.getFormat(range);\n      // @ts-expect-error\n      const indent = parseInt(formats.indent || 0, 10);\n      if (value === '+1' || value === '-1') {\n        let modifier = value === '+1' ? 1 : -1;\n        if (formats.direction === 'rtl') modifier *= -1;\n        this.quill.format('indent', indent + modifier, Quill.sources.USER);\n      }\n    },\n    link(value) {\n      if (value === true) {\n        value = prompt('Enter link URL:'); // eslint-disable-line no-alert\n      }\n      this.quill.format('link', value, Quill.sources.USER);\n    },\n    list(value) {\n      const range = this.quill.getSelection();\n      // @ts-expect-error\n      const formats = this.quill.getFormat(range);\n      if (value === 'check') {\n        if (formats.list === 'checked' || formats.list === 'unchecked') {\n          this.quill.format('list', false, Quill.sources.USER);\n        } else {\n          this.quill.format('list', 'unchecked', Quill.sources.USER);\n        }\n      } else {\n        this.quill.format('list', value, Quill.sources.USER);\n      }\n    },\n  },\n};\n\nexport { Toolbar as default, addControls };\n"
  },
  {
    "path": "packages/quill/src/modules/uiNode.ts",
    "content": "import { ParentBlot } from 'parchment';\nimport Module from '../core/module.js';\nimport Quill from '../core/quill.js';\n\nconst isMac = /Mac/i.test(navigator.platform);\n\n// Export for testing\nexport const TTL_FOR_VALID_SELECTION_CHANGE = 100;\n\n// A loose check to determine if the shortcut can move the caret before a UI node:\n// <ANY_PARENT>[CARET]<div class=\"ql-ui\"></div>[CONTENT]</ANY_PARENT>\nconst canMoveCaretBeforeUINode = (event: KeyboardEvent) => {\n  if (\n    event.key === 'ArrowLeft' ||\n    event.key === 'ArrowRight' || // RTL scripts or moving from the end of the previous line\n    event.key === 'ArrowUp' ||\n    event.key === 'ArrowDown' ||\n    event.key === 'Home'\n  ) {\n    return true;\n  }\n\n  if (isMac && event.key === 'a' && event.ctrlKey === true) {\n    return true;\n  }\n\n  return false;\n};\n\nclass UINode extends Module {\n  isListening = false;\n  selectionChangeDeadline = 0;\n\n  constructor(quill: Quill, options: Record<string, never>) {\n    super(quill, options);\n\n    this.handleArrowKeys();\n    this.handleNavigationShortcuts();\n  }\n\n  private handleArrowKeys() {\n    this.quill.keyboard.addBinding({\n      key: ['ArrowLeft', 'ArrowRight'],\n      offset: 0,\n      shiftKey: null,\n      handler(range, { line, event }) {\n        if (!(line instanceof ParentBlot) || !line.uiNode) {\n          return true;\n        }\n\n        const isRTL = getComputedStyle(line.domNode)['direction'] === 'rtl';\n        if (\n          (isRTL && event.key !== 'ArrowRight') ||\n          (!isRTL && event.key !== 'ArrowLeft')\n        ) {\n          return true;\n        }\n\n        this.quill.setSelection(\n          range.index - 1,\n          range.length + (event.shiftKey ? 1 : 0),\n          Quill.sources.USER,\n        );\n        return false;\n      },\n    });\n  }\n\n  private handleNavigationShortcuts() {\n    this.quill.root.addEventListener('keydown', (event) => {\n      if (!event.defaultPrevented && canMoveCaretBeforeUINode(event)) {\n        this.ensureListeningToSelectionChange();\n      }\n    });\n  }\n\n  /**\n   * We only listen to the `selectionchange` event when\n   * there is an intention of moving the caret to the beginning using shortcuts.\n   * This is primarily implemented to prevent infinite loops, as we are changing\n   * the selection within the handler of a `selectionchange` event.\n   */\n  private ensureListeningToSelectionChange() {\n    this.selectionChangeDeadline = Date.now() + TTL_FOR_VALID_SELECTION_CHANGE;\n\n    if (this.isListening) return;\n    this.isListening = true;\n\n    const listener = () => {\n      this.isListening = false;\n\n      if (Date.now() <= this.selectionChangeDeadline) {\n        this.handleSelectionChange();\n      }\n    };\n\n    document.addEventListener('selectionchange', listener, {\n      once: true,\n    });\n  }\n\n  private handleSelectionChange() {\n    const selection = document.getSelection();\n    if (!selection) return;\n    const range = selection.getRangeAt(0);\n    if (range.collapsed !== true || range.startOffset !== 0) return;\n\n    const line = this.quill.scroll.find(range.startContainer);\n    if (!(line instanceof ParentBlot) || !line.uiNode) return;\n\n    const newRange = document.createRange();\n    newRange.setStartAfter(line.uiNode);\n    newRange.setEndAfter(line.uiNode);\n    selection.removeAllRanges();\n    selection.addRange(newRange);\n  }\n}\n\nexport default UINode;\n"
  },
  {
    "path": "packages/quill/src/modules/uploader.ts",
    "content": "import Delta from 'quill-delta';\nimport type Quill from '../core/quill.js';\nimport Emitter from '../core/emitter.js';\nimport Module from '../core/module.js';\nimport type { Range } from '../core/selection.js';\n\ninterface UploaderOptions {\n  mimetypes: string[];\n  handler: (this: { quill: Quill }, range: Range, files: File[]) => void;\n}\n\nclass Uploader extends Module<UploaderOptions> {\n  static DEFAULTS: UploaderOptions;\n\n  constructor(quill: Quill, options: Partial<UploaderOptions>) {\n    super(quill, options);\n    quill.root.addEventListener('drop', (e) => {\n      e.preventDefault();\n      let native: ReturnType<typeof document.createRange> | null = null;\n      if (document.caretRangeFromPoint) {\n        native = document.caretRangeFromPoint(e.clientX, e.clientY);\n        // @ts-expect-error\n      } else if (document.caretPositionFromPoint) {\n        // @ts-expect-error\n        const position = document.caretPositionFromPoint(e.clientX, e.clientY);\n        native = document.createRange();\n        native.setStart(position.offsetNode, position.offset);\n        native.setEnd(position.offsetNode, position.offset);\n      }\n\n      const normalized = native && quill.selection.normalizeNative(native);\n      if (normalized) {\n        const range = quill.selection.normalizedToRange(normalized);\n        if (e.dataTransfer?.files) {\n          this.upload(range, e.dataTransfer.files);\n        }\n      }\n    });\n  }\n\n  upload(range: Range, files: FileList | File[]) {\n    const uploads: File[] = [];\n    Array.from(files).forEach((file) => {\n      if (file && this.options.mimetypes?.includes(file.type)) {\n        uploads.push(file);\n      }\n    });\n    if (uploads.length > 0) {\n      // @ts-expect-error Fix me later\n      this.options.handler.call(this, range, uploads);\n    }\n  }\n}\n\nUploader.DEFAULTS = {\n  mimetypes: ['image/png', 'image/jpeg'],\n  handler(range: Range, files: File[]) {\n    if (!this.quill.scroll.query('image')) {\n      return;\n    }\n    const promises = files.map<Promise<string>>((file) => {\n      return new Promise((resolve) => {\n        const reader = new FileReader();\n        reader.onload = () => {\n          resolve(reader.result as string);\n        };\n        reader.readAsDataURL(file);\n      });\n    });\n    Promise.all(promises).then((images) => {\n      const update = images.reduce((delta: Delta, image) => {\n        return delta.insert({ image });\n      }, new Delta().retain(range.index).delete(range.length)) as Delta;\n      this.quill.updateContents(update, Emitter.sources.USER);\n      this.quill.setSelection(\n        range.index + images.length,\n        Emitter.sources.SILENT,\n      );\n    });\n  },\n};\n\nexport default Uploader;\n"
  },
  {
    "path": "packages/quill/src/quill.ts",
    "content": "import Quill from './core.js';\nimport type {\n  Bounds,\n  DebugLevel,\n  EmitterSource,\n  ExpandedQuillOptions,\n  QuillOptions,\n} from './core.js';\n\nimport { AlignClass, AlignStyle } from './formats/align.js';\nimport {\n  DirectionAttribute,\n  DirectionClass,\n  DirectionStyle,\n} from './formats/direction.js';\nimport Indent from './formats/indent.js';\n\nimport Blockquote from './formats/blockquote.js';\nimport Header from './formats/header.js';\nimport List from './formats/list.js';\n\nimport { BackgroundClass, BackgroundStyle } from './formats/background.js';\nimport { ColorClass, ColorStyle } from './formats/color.js';\nimport { FontClass, FontStyle } from './formats/font.js';\nimport { SizeClass, SizeStyle } from './formats/size.js';\n\nimport Bold from './formats/bold.js';\nimport Italic from './formats/italic.js';\nimport Link from './formats/link.js';\nimport Script from './formats/script.js';\nimport Strike from './formats/strike.js';\nimport Underline from './formats/underline.js';\n\nimport Formula from './formats/formula.js';\nimport Image from './formats/image.js';\nimport Video from './formats/video.js';\n\nimport CodeBlock, { Code as InlineCode } from './formats/code.js';\n\nimport Syntax from './modules/syntax.js';\nimport Table from './modules/table.js';\nimport Toolbar from './modules/toolbar.js';\n\nimport Icons from './ui/icons.js';\nimport Picker from './ui/picker.js';\nimport ColorPicker from './ui/color-picker.js';\nimport IconPicker from './ui/icon-picker.js';\nimport Tooltip from './ui/tooltip.js';\n\nimport BubbleTheme from './themes/bubble.js';\nimport SnowTheme from './themes/snow.js';\n\nQuill.register(\n  {\n    'attributors/attribute/direction': DirectionAttribute,\n\n    'attributors/class/align': AlignClass,\n    'attributors/class/background': BackgroundClass,\n    'attributors/class/color': ColorClass,\n    'attributors/class/direction': DirectionClass,\n    'attributors/class/font': FontClass,\n    'attributors/class/size': SizeClass,\n\n    'attributors/style/align': AlignStyle,\n    'attributors/style/background': BackgroundStyle,\n    'attributors/style/color': ColorStyle,\n    'attributors/style/direction': DirectionStyle,\n    'attributors/style/font': FontStyle,\n    'attributors/style/size': SizeStyle,\n  },\n  true,\n);\n\nQuill.register(\n  {\n    'formats/align': AlignClass,\n    'formats/direction': DirectionClass,\n    'formats/indent': Indent,\n\n    'formats/background': BackgroundStyle,\n    'formats/color': ColorStyle,\n    'formats/font': FontClass,\n    'formats/size': SizeClass,\n\n    'formats/blockquote': Blockquote,\n    'formats/code-block': CodeBlock,\n    'formats/header': Header,\n    'formats/list': List,\n\n    'formats/bold': Bold,\n    'formats/code': InlineCode,\n    'formats/italic': Italic,\n    'formats/link': Link,\n    'formats/script': Script,\n    'formats/strike': Strike,\n    'formats/underline': Underline,\n\n    'formats/formula': Formula,\n    'formats/image': Image,\n    'formats/video': Video,\n\n    'modules/syntax': Syntax,\n    'modules/table': Table,\n    'modules/toolbar': Toolbar,\n\n    'themes/bubble': BubbleTheme,\n    'themes/snow': SnowTheme,\n\n    'ui/icons': Icons,\n    'ui/picker': Picker,\n    'ui/icon-picker': IconPicker,\n    'ui/color-picker': ColorPicker,\n    'ui/tooltip': Tooltip,\n  },\n  true,\n);\n\nexport {\n  AttributeMap,\n  Delta,\n  Module,\n  Op,\n  OpIterator,\n  Parchment,\n  Range,\n} from './core.js';\nexport type {\n  Bounds,\n  DebugLevel,\n  EmitterSource,\n  ExpandedQuillOptions,\n  QuillOptions,\n};\n\nexport default Quill;\n"
  },
  {
    "path": "packages/quill/src/themes/base.ts",
    "content": "import { merge } from 'lodash-es';\nimport type Quill from '../core/quill.js';\nimport Emitter from '../core/emitter.js';\nimport Theme from '../core/theme.js';\nimport type { ThemeOptions } from '../core/theme.js';\nimport ColorPicker from '../ui/color-picker.js';\nimport IconPicker from '../ui/icon-picker.js';\nimport Picker from '../ui/picker.js';\nimport Tooltip from '../ui/tooltip.js';\nimport type { Range } from '../core/selection.js';\nimport type Clipboard from '../modules/clipboard.js';\nimport type History from '../modules/history.js';\nimport type Keyboard from '../modules/keyboard.js';\nimport type Uploader from '../modules/uploader.js';\nimport type Selection from '../core/selection.js';\n\nconst ALIGNS = [false, 'center', 'right', 'justify'];\n\nconst COLORS = [\n  '#000000',\n  '#e60000',\n  '#ff9900',\n  '#ffff00',\n  '#008a00',\n  '#0066cc',\n  '#9933ff',\n  '#ffffff',\n  '#facccc',\n  '#ffebcc',\n  '#ffffcc',\n  '#cce8cc',\n  '#cce0f5',\n  '#ebd6ff',\n  '#bbbbbb',\n  '#f06666',\n  '#ffc266',\n  '#ffff66',\n  '#66b966',\n  '#66a3e0',\n  '#c285ff',\n  '#888888',\n  '#a10000',\n  '#b26b00',\n  '#b2b200',\n  '#006100',\n  '#0047b2',\n  '#6b24b2',\n  '#444444',\n  '#5c0000',\n  '#663d00',\n  '#666600',\n  '#003700',\n  '#002966',\n  '#3d1466',\n];\n\nconst FONTS = [false, 'serif', 'monospace'];\n\nconst HEADERS = ['1', '2', '3', false];\n\nconst SIZES = ['small', false, 'large', 'huge'];\n\nclass BaseTheme extends Theme {\n  pickers: Picker[];\n  tooltip?: Tooltip;\n\n  constructor(quill: Quill, options: ThemeOptions) {\n    super(quill, options);\n    const listener = (e: MouseEvent) => {\n      if (!document.body.contains(quill.root)) {\n        document.body.removeEventListener('click', listener);\n        return;\n      }\n      if (\n        this.tooltip != null &&\n        // @ts-expect-error\n        !this.tooltip.root.contains(e.target) &&\n        // @ts-expect-error\n        document.activeElement !== this.tooltip.textbox &&\n        !this.quill.hasFocus()\n      ) {\n        this.tooltip.hide();\n      }\n      if (this.pickers != null) {\n        this.pickers.forEach((picker) => {\n          // @ts-expect-error\n          if (!picker.container.contains(e.target)) {\n            picker.close();\n          }\n        });\n      }\n    };\n    quill.emitter.listenDOM('click', document.body, listener);\n  }\n\n  addModule(name: 'clipboard'): Clipboard;\n  addModule(name: 'keyboard'): Keyboard;\n  addModule(name: 'uploader'): Uploader;\n  addModule(name: 'history'): History;\n  addModule(name: 'selection'): Selection;\n  addModule(name: string): unknown;\n  addModule(name: string) {\n    const module = super.addModule(name);\n    if (name === 'toolbar') {\n      // @ts-expect-error\n      this.extendToolbar(module);\n    }\n    return module;\n  }\n\n  buildButtons(\n    buttons: NodeListOf<HTMLElement>,\n    icons: Record<string, Record<string, string> | string>,\n  ) {\n    Array.from(buttons).forEach((button) => {\n      const className = button.getAttribute('class') || '';\n      className.split(/\\s+/).forEach((name) => {\n        if (!name.startsWith('ql-')) return;\n        name = name.slice('ql-'.length);\n        if (icons[name] == null) return;\n        if (name === 'direction') {\n          // @ts-expect-error\n          button.innerHTML = icons[name][''] + icons[name].rtl;\n        } else if (typeof icons[name] === 'string') {\n          // @ts-expect-error\n          button.innerHTML = icons[name];\n        } else {\n          // @ts-expect-error\n          const value = button.value || '';\n          // @ts-expect-error\n          if (value != null && icons[name][value]) {\n            // @ts-expect-error\n            button.innerHTML = icons[name][value];\n          }\n        }\n      });\n    });\n  }\n\n  buildPickers(\n    selects: NodeListOf<HTMLSelectElement>,\n    icons: Record<string, string | Record<string, string>>,\n  ) {\n    this.pickers = Array.from(selects).map((select) => {\n      if (select.classList.contains('ql-align')) {\n        if (select.querySelector('option') == null) {\n          fillSelect(select, ALIGNS);\n        }\n        if (typeof icons.align === 'object') {\n          return new IconPicker(select, icons.align);\n        }\n      }\n      if (\n        select.classList.contains('ql-background') ||\n        select.classList.contains('ql-color')\n      ) {\n        const format = select.classList.contains('ql-background')\n          ? 'background'\n          : 'color';\n        if (select.querySelector('option') == null) {\n          fillSelect(\n            select,\n            COLORS,\n            format === 'background' ? '#ffffff' : '#000000',\n          );\n        }\n        return new ColorPicker(select, icons[format] as string);\n      }\n      if (select.querySelector('option') == null) {\n        if (select.classList.contains('ql-font')) {\n          fillSelect(select, FONTS);\n        } else if (select.classList.contains('ql-header')) {\n          fillSelect(select, HEADERS);\n        } else if (select.classList.contains('ql-size')) {\n          fillSelect(select, SIZES);\n        }\n      }\n      return new Picker(select);\n    });\n    const update = () => {\n      this.pickers.forEach((picker) => {\n        picker.update();\n      });\n    };\n    this.quill.on(Emitter.events.EDITOR_CHANGE, update);\n  }\n}\nBaseTheme.DEFAULTS = merge({}, Theme.DEFAULTS, {\n  modules: {\n    toolbar: {\n      handlers: {\n        formula() {\n          this.quill.theme.tooltip.edit('formula');\n        },\n        image() {\n          let fileInput = this.container.querySelector(\n            'input.ql-image[type=file]',\n          );\n          if (fileInput == null) {\n            fileInput = document.createElement('input');\n            fileInput.setAttribute('type', 'file');\n            fileInput.setAttribute(\n              'accept',\n              this.quill.uploader.options.mimetypes.join(', '),\n            );\n            fileInput.classList.add('ql-image');\n            fileInput.addEventListener('change', () => {\n              const range = this.quill.getSelection(true);\n              this.quill.uploader.upload(range, fileInput.files);\n              fileInput.value = '';\n            });\n            this.container.appendChild(fileInput);\n          }\n          fileInput.click();\n        },\n        video() {\n          this.quill.theme.tooltip.edit('video');\n        },\n      },\n    },\n  },\n});\n\nclass BaseTooltip extends Tooltip {\n  textbox: HTMLInputElement | null;\n  linkRange?: Range;\n\n  constructor(quill: Quill, boundsContainer?: HTMLElement) {\n    super(quill, boundsContainer);\n    this.textbox = this.root.querySelector('input[type=\"text\"]');\n    this.listen();\n  }\n\n  listen() {\n    // @ts-expect-error Fix me later\n    this.textbox.addEventListener('keydown', (event) => {\n      if (event.key === 'Enter') {\n        this.save();\n        event.preventDefault();\n      } else if (event.key === 'Escape') {\n        this.cancel();\n        event.preventDefault();\n      }\n    });\n  }\n\n  cancel() {\n    this.hide();\n    this.restoreFocus();\n  }\n\n  edit(mode = 'link', preview: string | null = null) {\n    this.root.classList.remove('ql-hidden');\n    this.root.classList.add('ql-editing');\n    if (this.textbox == null) return;\n\n    if (preview != null) {\n      this.textbox.value = preview;\n    } else if (mode !== this.root.getAttribute('data-mode')) {\n      this.textbox.value = '';\n    }\n    const bounds = this.quill.getBounds(this.quill.selection.savedRange);\n    if (bounds != null) {\n      this.position(bounds);\n    }\n    this.textbox.select();\n    this.textbox.setAttribute(\n      'placeholder',\n      this.textbox.getAttribute(`data-${mode}`) || '',\n    );\n    this.root.setAttribute('data-mode', mode);\n  }\n\n  restoreFocus() {\n    this.quill.focus({ preventScroll: true });\n  }\n\n  save() {\n    // @ts-expect-error Fix me later\n    let { value } = this.textbox;\n    switch (this.root.getAttribute('data-mode')) {\n      case 'link': {\n        const { scrollTop } = this.quill.root;\n        if (this.linkRange) {\n          this.quill.formatText(\n            this.linkRange,\n            'link',\n            value,\n            Emitter.sources.USER,\n          );\n          delete this.linkRange;\n        } else {\n          this.restoreFocus();\n          this.quill.format('link', value, Emitter.sources.USER);\n        }\n        this.quill.root.scrollTop = scrollTop;\n        break;\n      }\n      case 'video': {\n        value = extractVideoUrl(value);\n      } // eslint-disable-next-line no-fallthrough\n      case 'formula': {\n        if (!value) break;\n        const range = this.quill.getSelection(true);\n        if (range != null) {\n          const index = range.index + range.length;\n          this.quill.insertEmbed(\n            index,\n            // @ts-expect-error Fix me later\n            this.root.getAttribute('data-mode'),\n            value,\n            Emitter.sources.USER,\n          );\n          if (this.root.getAttribute('data-mode') === 'formula') {\n            this.quill.insertText(index + 1, ' ', Emitter.sources.USER);\n          }\n          this.quill.setSelection(index + 2, Emitter.sources.USER);\n        }\n        break;\n      }\n      default:\n    }\n    // @ts-expect-error Fix me later\n    this.textbox.value = '';\n    this.hide();\n  }\n}\n\nfunction extractVideoUrl(url: string) {\n  let match =\n    url.match(\n      /^(?:(https?):\\/\\/)?(?:(?:www|m)\\.)?youtube\\.com\\/watch.*v=([a-zA-Z0-9_-]+)/,\n    ) ||\n    url.match(/^(?:(https?):\\/\\/)?(?:(?:www|m)\\.)?youtu\\.be\\/([a-zA-Z0-9_-]+)/);\n  if (match) {\n    return `${match[1] || 'https'}://www.youtube.com/embed/${\n      match[2]\n    }?showinfo=0`;\n  }\n  // eslint-disable-next-line no-cond-assign\n  if ((match = url.match(/^(?:(https?):\\/\\/)?(?:www\\.)?vimeo\\.com\\/(\\d+)/))) {\n    return `${match[1] || 'https'}://player.vimeo.com/video/${match[2]}/`;\n  }\n  return url;\n}\n\nfunction fillSelect(\n  select: HTMLSelectElement,\n  values: Array<string | boolean>,\n  defaultValue: unknown = false,\n) {\n  values.forEach((value) => {\n    const option = document.createElement('option');\n    if (value === defaultValue) {\n      option.setAttribute('selected', 'selected');\n    } else {\n      option.setAttribute('value', String(value));\n    }\n    select.appendChild(option);\n  });\n}\n\nexport { BaseTooltip, BaseTheme as default };\n"
  },
  {
    "path": "packages/quill/src/themes/bubble.ts",
    "content": "import { merge } from 'lodash-es';\nimport Emitter from '../core/emitter.js';\nimport BaseTheme, { BaseTooltip } from './base.js';\nimport { Range } from '../core/selection.js';\nimport type { Bounds } from '../core/selection.js';\nimport icons from '../ui/icons.js';\nimport Quill from '../core/quill.js';\nimport type { ThemeOptions } from '../core/theme.js';\nimport type Toolbar from '../modules/toolbar.js';\nimport type { ToolbarConfig } from '../modules/toolbar.js';\n\nconst TOOLBAR_CONFIG: ToolbarConfig = [\n  ['bold', 'italic', 'link'],\n  [{ header: 1 }, { header: 2 }, 'blockquote'],\n];\n\nclass BubbleTooltip extends BaseTooltip {\n  static TEMPLATE = [\n    '<span class=\"ql-tooltip-arrow\"></span>',\n    '<div class=\"ql-tooltip-editor\">',\n    '<input type=\"text\" data-formula=\"e=mc^2\" data-link=\"https://quilljs.com\" data-video=\"Embed URL\">',\n    '<a class=\"ql-close\"></a>',\n    '</div>',\n  ].join('');\n\n  constructor(quill: Quill, bounds?: HTMLElement) {\n    super(quill, bounds);\n    this.quill.on(\n      Emitter.events.EDITOR_CHANGE,\n      (type, range, oldRange, source) => {\n        if (type !== Emitter.events.SELECTION_CHANGE) return;\n        if (\n          range != null &&\n          range.length > 0 &&\n          source === Emitter.sources.USER\n        ) {\n          this.show();\n          // Lock our width so we will expand beyond our offsetParent boundaries\n          this.root.style.left = '0px';\n          this.root.style.width = '';\n          this.root.style.width = `${this.root.offsetWidth}px`;\n          const lines = this.quill.getLines(range.index, range.length);\n          if (lines.length === 1) {\n            const bounds = this.quill.getBounds(range);\n            if (bounds != null) {\n              this.position(bounds);\n            }\n          } else {\n            const lastLine = lines[lines.length - 1];\n            const index = this.quill.getIndex(lastLine);\n            const length = Math.min(\n              lastLine.length() - 1,\n              range.index + range.length - index,\n            );\n            const indexBounds = this.quill.getBounds(new Range(index, length));\n            if (indexBounds != null) {\n              this.position(indexBounds);\n            }\n          }\n        } else if (\n          document.activeElement !== this.textbox &&\n          this.quill.hasFocus()\n        ) {\n          this.hide();\n        }\n      },\n    );\n  }\n\n  listen() {\n    super.listen();\n    // @ts-expect-error Fix me later\n    this.root.querySelector('.ql-close').addEventListener('click', () => {\n      this.root.classList.remove('ql-editing');\n    });\n    this.quill.on(Emitter.events.SCROLL_OPTIMIZE, () => {\n      // Let selection be restored by toolbar handlers before repositioning\n      setTimeout(() => {\n        if (this.root.classList.contains('ql-hidden')) return;\n        const range = this.quill.getSelection();\n        if (range != null) {\n          const bounds = this.quill.getBounds(range);\n          if (bounds != null) {\n            this.position(bounds);\n          }\n        }\n      }, 1);\n    });\n  }\n\n  cancel() {\n    this.show();\n  }\n\n  position(reference: Bounds) {\n    const shift = super.position(reference);\n    const arrow = this.root.querySelector('.ql-tooltip-arrow');\n    // @ts-expect-error\n    arrow.style.marginLeft = '';\n    if (shift !== 0) {\n      // @ts-expect-error\n      arrow.style.marginLeft = `${-1 * shift - arrow.offsetWidth / 2}px`;\n    }\n    return shift;\n  }\n}\n\nclass BubbleTheme extends BaseTheme {\n  tooltip: BubbleTooltip;\n\n  constructor(quill: Quill, options: ThemeOptions) {\n    if (\n      options.modules.toolbar != null &&\n      options.modules.toolbar.container == null\n    ) {\n      options.modules.toolbar.container = TOOLBAR_CONFIG;\n    }\n    super(quill, options);\n    this.quill.container.classList.add('ql-bubble');\n  }\n\n  extendToolbar(toolbar: Toolbar) {\n    // @ts-expect-error\n    this.tooltip = new BubbleTooltip(this.quill, this.options.bounds);\n    if (toolbar.container != null) {\n      this.tooltip.root.appendChild<HTMLElement>(toolbar.container);\n      this.buildButtons(toolbar.container.querySelectorAll('button'), icons);\n      this.buildPickers(toolbar.container.querySelectorAll('select'), icons);\n    }\n  }\n}\nBubbleTheme.DEFAULTS = merge({}, BaseTheme.DEFAULTS, {\n  modules: {\n    toolbar: {\n      handlers: {\n        link(value: string) {\n          if (!value) {\n            this.quill.format('link', false, Quill.sources.USER);\n          } else {\n            // @ts-expect-error\n            this.quill.theme.tooltip.edit();\n          }\n        },\n      },\n    },\n  },\n} satisfies ThemeOptions);\n\nexport { BubbleTooltip, BubbleTheme as default };\n"
  },
  {
    "path": "packages/quill/src/themes/snow.ts",
    "content": "import { merge } from 'lodash-es';\nimport Emitter from '../core/emitter.js';\nimport BaseTheme, { BaseTooltip } from './base.js';\nimport LinkBlot from '../formats/link.js';\nimport { Range } from '../core/selection.js';\nimport icons from '../ui/icons.js';\nimport Quill from '../core/quill.js';\nimport type { Context } from '../modules/keyboard.js';\nimport type Toolbar from '../modules/toolbar.js';\nimport type { ToolbarConfig } from '../modules/toolbar.js';\nimport type { ThemeOptions } from '../core/theme.js';\n\nconst TOOLBAR_CONFIG: ToolbarConfig = [\n  [{ header: ['1', '2', '3', false] }],\n  ['bold', 'italic', 'underline', 'link'],\n  [{ list: 'ordered' }, { list: 'bullet' }],\n  ['clean'],\n];\n\nclass SnowTooltip extends BaseTooltip {\n  static TEMPLATE = [\n    '<a class=\"ql-preview\" rel=\"noopener noreferrer\" target=\"_blank\" href=\"about:blank\"></a>',\n    '<input type=\"text\" data-formula=\"e=mc^2\" data-link=\"https://quilljs.com\" data-video=\"Embed URL\">',\n    '<a class=\"ql-action\"></a>',\n    '<a class=\"ql-remove\"></a>',\n  ].join('');\n\n  preview = this.root.querySelector('a.ql-preview');\n\n  listen() {\n    super.listen();\n    // @ts-expect-error Fix me later\n    this.root\n      .querySelector('a.ql-action')\n      .addEventListener('click', (event) => {\n        if (this.root.classList.contains('ql-editing')) {\n          this.save();\n        } else {\n          // @ts-expect-error Fix me later\n          this.edit('link', this.preview.textContent);\n        }\n        event.preventDefault();\n      });\n    // @ts-expect-error Fix me later\n    this.root\n      .querySelector('a.ql-remove')\n      .addEventListener('click', (event) => {\n        if (this.linkRange != null) {\n          const range = this.linkRange;\n          this.restoreFocus();\n          this.quill.formatText(range, 'link', false, Emitter.sources.USER);\n          delete this.linkRange;\n        }\n        event.preventDefault();\n        this.hide();\n      });\n    this.quill.on(\n      Emitter.events.SELECTION_CHANGE,\n      (range, oldRange, source) => {\n        if (range == null) return;\n        if (range.length === 0 && source === Emitter.sources.USER) {\n          const [link, offset] = this.quill.scroll.descendant(\n            LinkBlot,\n            range.index,\n          );\n          if (link != null) {\n            this.linkRange = new Range(range.index - offset, link.length());\n            const preview = LinkBlot.formats(link.domNode);\n            // @ts-expect-error Fix me later\n            this.preview.textContent = preview;\n            // @ts-expect-error Fix me later\n            this.preview.setAttribute('href', preview);\n            this.show();\n            const bounds = this.quill.getBounds(this.linkRange);\n            if (bounds != null) {\n              this.position(bounds);\n            }\n            return;\n          }\n        } else {\n          delete this.linkRange;\n        }\n        this.hide();\n      },\n    );\n  }\n\n  show() {\n    super.show();\n    this.root.removeAttribute('data-mode');\n  }\n}\n\nclass SnowTheme extends BaseTheme {\n  constructor(quill: Quill, options: ThemeOptions) {\n    if (\n      options.modules.toolbar != null &&\n      options.modules.toolbar.container == null\n    ) {\n      options.modules.toolbar.container = TOOLBAR_CONFIG;\n    }\n    super(quill, options);\n    this.quill.container.classList.add('ql-snow');\n  }\n\n  extendToolbar(toolbar: Toolbar) {\n    if (toolbar.container != null) {\n      toolbar.container.classList.add('ql-snow');\n      this.buildButtons(toolbar.container.querySelectorAll('button'), icons);\n      this.buildPickers(toolbar.container.querySelectorAll('select'), icons);\n      // @ts-expect-error\n      this.tooltip = new SnowTooltip(this.quill, this.options.bounds);\n      if (toolbar.container.querySelector('.ql-link')) {\n        this.quill.keyboard.addBinding(\n          { key: 'k', shortKey: true },\n          (_range: Range, context: Context) => {\n            toolbar.handlers.link.call(toolbar, !context.format.link);\n          },\n        );\n      }\n    }\n  }\n}\nSnowTheme.DEFAULTS = merge({}, BaseTheme.DEFAULTS, {\n  modules: {\n    toolbar: {\n      handlers: {\n        link(value: string) {\n          if (value) {\n            const range = this.quill.getSelection();\n            if (range == null || range.length === 0) return;\n            let preview = this.quill.getText(range);\n            if (\n              /^\\S+@\\S+\\.\\S+$/.test(preview) &&\n              preview.indexOf('mailto:') !== 0\n            ) {\n              preview = `mailto:${preview}`;\n            }\n            // @ts-expect-error\n            const { tooltip } = this.quill.theme;\n            tooltip.edit('link', preview);\n          } else {\n            this.quill.format('link', false, Quill.sources.USER);\n          }\n        },\n      },\n    },\n  },\n} satisfies ThemeOptions);\n\nexport default SnowTheme;\n"
  },
  {
    "path": "packages/quill/src/types.d.ts",
    "content": "declare module '*.svg' {\n  const content: string;\n  export default content;\n}\n\ndeclare const QUILL_VERSION: string | undefined;\n"
  },
  {
    "path": "packages/quill/src/ui/color-picker.ts",
    "content": "import Picker from './picker.js';\n\nclass ColorPicker extends Picker {\n  constructor(select: HTMLSelectElement, label: string) {\n    super(select);\n    this.label.innerHTML = label;\n    this.container.classList.add('ql-color-picker');\n    Array.from(this.container.querySelectorAll('.ql-picker-item'))\n      .slice(0, 7)\n      .forEach((item) => {\n        item.classList.add('ql-primary');\n      });\n  }\n\n  buildItem(option: HTMLOptionElement) {\n    const item = super.buildItem(option);\n    item.style.backgroundColor = option.getAttribute('value') || '';\n    return item;\n  }\n\n  selectItem(item: HTMLElement | null, trigger?: boolean) {\n    super.selectItem(item, trigger);\n    const colorLabel = this.label.querySelector<HTMLElement>('.ql-color-label');\n    const value = item ? item.getAttribute('data-value') || '' : '';\n    if (colorLabel) {\n      if (colorLabel.tagName === 'line') {\n        colorLabel.style.stroke = value;\n      } else {\n        colorLabel.style.fill = value;\n      }\n    }\n  }\n}\n\nexport default ColorPicker;\n"
  },
  {
    "path": "packages/quill/src/ui/icon-picker.ts",
    "content": "import Picker from './picker.js';\n\nclass IconPicker extends Picker {\n  defaultItem: HTMLElement | null;\n\n  constructor(select: HTMLSelectElement, icons: Record<string, string>) {\n    super(select);\n    this.container.classList.add('ql-icon-picker');\n    Array.from(this.container.querySelectorAll('.ql-picker-item')).forEach(\n      (item) => {\n        item.innerHTML = icons[item.getAttribute('data-value') || ''];\n      },\n    );\n    this.defaultItem = this.container.querySelector('.ql-selected');\n    this.selectItem(this.defaultItem);\n  }\n\n  selectItem(target: HTMLElement | null, trigger?: boolean) {\n    super.selectItem(target, trigger);\n    const item = target || this.defaultItem;\n    if (item != null) {\n      if (this.label.innerHTML === item.innerHTML) return;\n      this.label.innerHTML = item.innerHTML;\n    }\n  }\n}\n\nexport default IconPicker;\n"
  },
  {
    "path": "packages/quill/src/ui/icons.ts",
    "content": "import alignLeftIcon from '../assets/icons/align-left.svg';\nimport alignCenterIcon from '../assets/icons/align-center.svg';\nimport alignRightIcon from '../assets/icons/align-right.svg';\nimport alignJustifyIcon from '../assets/icons/align-justify.svg';\nimport backgroundIcon from '../assets/icons/background.svg';\nimport blockquoteIcon from '../assets/icons/blockquote.svg';\nimport boldIcon from '../assets/icons/bold.svg';\nimport cleanIcon from '../assets/icons/clean.svg';\nimport codeIcon from '../assets/icons/code.svg';\nimport colorIcon from '../assets/icons/color.svg';\nimport directionLeftToRightIcon from '../assets/icons/direction-ltr.svg';\nimport directionRightToLeftIcon from '../assets/icons/direction-rtl.svg';\nimport formulaIcon from '../assets/icons/formula.svg';\nimport headerIcon from '../assets/icons/header.svg';\nimport header2Icon from '../assets/icons/header-2.svg';\nimport header3Icon from '../assets/icons/header-3.svg';\nimport header4Icon from '../assets/icons/header-4.svg';\nimport header5Icon from '../assets/icons/header-5.svg';\nimport header6Icon from '../assets/icons/header-6.svg';\nimport italicIcon from '../assets/icons/italic.svg';\nimport imageIcon from '../assets/icons/image.svg';\nimport indentIcon from '../assets/icons/indent.svg';\nimport outdentIcon from '../assets/icons/outdent.svg';\nimport linkIcon from '../assets/icons/link.svg';\nimport listBulletIcon from '../assets/icons/list-bullet.svg';\nimport listCheckIcon from '../assets/icons/list-check.svg';\nimport listOrderedIcon from '../assets/icons/list-ordered.svg';\nimport subscriptIcon from '../assets/icons/subscript.svg';\nimport superscriptIcon from '../assets/icons/superscript.svg';\nimport strikeIcon from '../assets/icons/strike.svg';\nimport tableIcon from '../assets/icons/table.svg';\nimport underlineIcon from '../assets/icons/underline.svg';\nimport videoIcon from '../assets/icons/video.svg';\n\nexport default {\n  align: {\n    '': alignLeftIcon,\n    center: alignCenterIcon,\n    right: alignRightIcon,\n    justify: alignJustifyIcon,\n  },\n  background: backgroundIcon,\n  blockquote: blockquoteIcon,\n  bold: boldIcon,\n  clean: cleanIcon,\n  code: codeIcon,\n  'code-block': codeIcon,\n  color: colorIcon,\n  direction: {\n    '': directionLeftToRightIcon,\n    rtl: directionRightToLeftIcon,\n  },\n  formula: formulaIcon,\n  header: {\n    '1': headerIcon,\n    '2': header2Icon,\n    '3': header3Icon,\n    '4': header4Icon,\n    '5': header5Icon,\n    '6': header6Icon,\n  },\n  italic: italicIcon,\n  image: imageIcon,\n  indent: {\n    '+1': indentIcon,\n    '-1': outdentIcon,\n  },\n  link: linkIcon,\n  list: {\n    bullet: listBulletIcon,\n    check: listCheckIcon,\n    ordered: listOrderedIcon,\n  },\n  script: {\n    sub: subscriptIcon,\n    super: superscriptIcon,\n  },\n  strike: strikeIcon,\n  table: tableIcon,\n  underline: underlineIcon,\n  video: videoIcon,\n};\n"
  },
  {
    "path": "packages/quill/src/ui/picker.ts",
    "content": "import DropdownIcon from '../assets/icons/dropdown.svg';\n\nlet optionsCounter = 0;\n\nfunction toggleAriaAttribute(element: HTMLElement, attribute: string) {\n  element.setAttribute(\n    attribute,\n    `${!(element.getAttribute(attribute) === 'true')}`,\n  );\n}\n\nclass Picker {\n  select: HTMLSelectElement;\n  container: HTMLElement;\n  label: HTMLElement;\n\n  constructor(select: HTMLSelectElement) {\n    this.select = select;\n    this.container = document.createElement('span');\n    this.buildPicker();\n    this.select.style.display = 'none';\n    // @ts-expect-error Fix me later\n    this.select.parentNode.insertBefore(this.container, this.select);\n\n    this.label.addEventListener('mousedown', () => {\n      this.togglePicker();\n    });\n    this.label.addEventListener('keydown', (event) => {\n      switch (event.key) {\n        case 'Enter':\n          this.togglePicker();\n          break;\n        case 'Escape':\n          this.escape();\n          event.preventDefault();\n          break;\n        default:\n      }\n    });\n    this.select.addEventListener('change', this.update.bind(this));\n  }\n\n  togglePicker() {\n    this.container.classList.toggle('ql-expanded');\n    // Toggle aria-expanded and aria-hidden to make the picker accessible\n    toggleAriaAttribute(this.label, 'aria-expanded');\n    // @ts-expect-error\n    toggleAriaAttribute(this.options, 'aria-hidden');\n  }\n\n  buildItem(option: HTMLOptionElement) {\n    const item = document.createElement('span');\n    // @ts-expect-error\n    item.tabIndex = '0';\n    item.setAttribute('role', 'button');\n    item.classList.add('ql-picker-item');\n    const value = option.getAttribute('value');\n    if (value) {\n      item.setAttribute('data-value', value);\n    }\n    if (option.textContent) {\n      item.setAttribute('data-label', option.textContent);\n    }\n    item.addEventListener('click', () => {\n      this.selectItem(item, true);\n    });\n    item.addEventListener('keydown', (event) => {\n      switch (event.key) {\n        case 'Enter':\n          this.selectItem(item, true);\n          event.preventDefault();\n          break;\n        case 'Escape':\n          this.escape();\n          event.preventDefault();\n          break;\n        default:\n      }\n    });\n\n    return item;\n  }\n\n  buildLabel() {\n    const label = document.createElement('span');\n    label.classList.add('ql-picker-label');\n    label.innerHTML = DropdownIcon;\n    // @ts-expect-error\n    label.tabIndex = '0';\n    label.setAttribute('role', 'button');\n    label.setAttribute('aria-expanded', 'false');\n    this.container.appendChild(label);\n    return label;\n  }\n\n  buildOptions() {\n    const options = document.createElement('span');\n    options.classList.add('ql-picker-options');\n\n    // Don't want screen readers to read this until options are visible\n    options.setAttribute('aria-hidden', 'true');\n    // @ts-expect-error\n    options.tabIndex = '-1';\n\n    // Need a unique id for aria-controls\n    options.id = `ql-picker-options-${optionsCounter}`;\n    optionsCounter += 1;\n    this.label.setAttribute('aria-controls', options.id);\n\n    // @ts-expect-error\n    this.options = options;\n\n    Array.from(this.select.options).forEach((option) => {\n      const item = this.buildItem(option);\n      options.appendChild(item);\n      if (option.selected === true) {\n        this.selectItem(item);\n      }\n    });\n    this.container.appendChild(options);\n  }\n\n  buildPicker() {\n    Array.from(this.select.attributes).forEach((item) => {\n      this.container.setAttribute(item.name, item.value);\n    });\n    this.container.classList.add('ql-picker');\n    this.label = this.buildLabel();\n    this.buildOptions();\n  }\n\n  escape() {\n    // Close menu and return focus to trigger label\n    this.close();\n    // Need setTimeout for accessibility to ensure that the browser executes\n    // focus on the next process thread and after any DOM content changes\n    setTimeout(() => this.label.focus(), 1);\n  }\n\n  close() {\n    this.container.classList.remove('ql-expanded');\n    this.label.setAttribute('aria-expanded', 'false');\n    // @ts-expect-error\n    this.options.setAttribute('aria-hidden', 'true');\n  }\n\n  selectItem(item: HTMLElement | null, trigger = false) {\n    const selected = this.container.querySelector('.ql-selected');\n    if (item === selected) return;\n    if (selected != null) {\n      selected.classList.remove('ql-selected');\n    }\n    if (item == null) return;\n    item.classList.add('ql-selected');\n    // @ts-expect-error Fix me later\n    this.select.selectedIndex = Array.from(item.parentNode.children).indexOf(\n      item,\n    );\n    if (item.hasAttribute('data-value')) {\n      // @ts-expect-error Fix me later\n      this.label.setAttribute('data-value', item.getAttribute('data-value'));\n    } else {\n      this.label.removeAttribute('data-value');\n    }\n    if (item.hasAttribute('data-label')) {\n      // @ts-expect-error Fix me later\n      this.label.setAttribute('data-label', item.getAttribute('data-label'));\n    } else {\n      this.label.removeAttribute('data-label');\n    }\n    if (trigger) {\n      this.select.dispatchEvent(new Event('change'));\n      this.close();\n    }\n  }\n\n  update() {\n    let option;\n    if (this.select.selectedIndex > -1) {\n      const item =\n        // @ts-expect-error Fix me later\n        this.container.querySelector('.ql-picker-options').children[\n          this.select.selectedIndex\n        ];\n      option = this.select.options[this.select.selectedIndex];\n      // @ts-expect-error\n      this.selectItem(item);\n    } else {\n      this.selectItem(null);\n    }\n    const isActive =\n      option != null &&\n      option !== this.select.querySelector('option[selected]');\n    this.label.classList.toggle('ql-active', isActive);\n  }\n}\n\nexport default Picker;\n"
  },
  {
    "path": "packages/quill/src/ui/tooltip.ts",
    "content": "import type Quill from '../core.js';\nimport type { Bounds } from '../core/selection.js';\n\nconst isScrollable = (el: Element) => {\n  const { overflowY } = getComputedStyle(el, null);\n  return overflowY !== 'visible' && overflowY !== 'clip';\n};\n\nclass Tooltip {\n  quill: Quill;\n  boundsContainer: HTMLElement;\n  root: HTMLDivElement;\n\n  constructor(quill: Quill, boundsContainer?: HTMLElement) {\n    this.quill = quill;\n    this.boundsContainer = boundsContainer || document.body;\n    this.root = quill.addContainer('ql-tooltip');\n    // @ts-expect-error\n    this.root.innerHTML = this.constructor.TEMPLATE;\n    if (isScrollable(this.quill.root)) {\n      this.quill.root.addEventListener('scroll', () => {\n        this.root.style.marginTop = `${-1 * this.quill.root.scrollTop}px`;\n      });\n    }\n    this.hide();\n  }\n\n  hide() {\n    this.root.classList.add('ql-hidden');\n  }\n\n  position(reference: Bounds) {\n    const left =\n      reference.left + reference.width / 2 - this.root.offsetWidth / 2;\n    // root.scrollTop should be 0 if scrollContainer !== root\n    const top = reference.bottom + this.quill.root.scrollTop;\n    this.root.style.left = `${left}px`;\n    this.root.style.top = `${top}px`;\n    this.root.classList.remove('ql-flip');\n    const containerBounds = this.boundsContainer.getBoundingClientRect();\n    const rootBounds = this.root.getBoundingClientRect();\n    let shift = 0;\n    if (rootBounds.right > containerBounds.right) {\n      shift = containerBounds.right - rootBounds.right;\n      this.root.style.left = `${left + shift}px`;\n    }\n    if (rootBounds.left < containerBounds.left) {\n      shift = containerBounds.left - rootBounds.left;\n      this.root.style.left = `${left + shift}px`;\n    }\n    if (rootBounds.bottom > containerBounds.bottom) {\n      const height = rootBounds.bottom - rootBounds.top;\n      const verticalShift = reference.bottom - reference.top + height;\n      this.root.style.top = `${top - verticalShift}px`;\n      this.root.classList.add('ql-flip');\n    }\n    return shift;\n  }\n\n  show() {\n    this.root.classList.remove('ql-editing');\n    this.root.classList.remove('ql-hidden');\n  }\n}\n\nexport default Tooltip;\n"
  },
  {
    "path": "packages/quill/test/e2e/__dev_server__/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\" />\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\" />\n  <title>Quill E2E Tests</title>\n  <script src=\"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js\"></script>\n  <link href=\"/quill.core.css\" rel=\"stylesheet\">\n  <link href=\"/quill.snow.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <div id=\"root\">\n    <div id=\"standalone-container\">\n      <div id=\"toolbar-container\">\n        <span class=\"ql-formats\">\n          <select class=\"ql-font\"></select>\n          <select class=\"ql-size\"></select>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-bold\"></button>\n          <button class=\"ql-italic\"></button>\n          <button class=\"ql-underline\"></button>\n          <button class=\"ql-strike\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <select class=\"ql-color\"></select>\n          <select class=\"ql-background\"></select>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-script\" value=\"sub\"></button>\n          <button class=\"ql-script\" value=\"super\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-header\" value=\"1\"></button>\n          <button class=\"ql-header\" value=\"2\"></button>\n          <button class=\"ql-blockquote\"></button>\n          <button class=\"ql-code-block\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-list\" value=\"ordered\"></button>\n          <button class=\"ql-list\" value=\"bullet\"></button>\n          <button class=\"ql-indent\" value=\"-1\"></button>\n          <button class=\"ql-indent\" value=\"+1\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-direction\" value=\"rtl\"></button>\n          <select class=\"ql-align\"></select>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-link\"></button>\n          <button class=\"ql-image\"></button>\n          <button class=\"ql-video\"></button>\n          <button class=\"ql-formula\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <button class=\"ql-clean\"></button>\n        </span>\n      </div>\n      <div id=\"editor\" style=\"height: 350px;\">\n      </div>\n      <script>\n        window.quill = new Quill(document.getElementById('editor'), {\n          modules: { syntax: true, toolbar: '#toolbar-container', },\n          placeholder: 'Compose an epic...', theme: 'snow',\n        })\n      </script>\n    </div>\n  </div>\n</body>\n\n</html>"
  },
  {
    "path": "packages/quill/test/e2e/__dev_server__/webpack.config.cjs",
    "content": "/*eslint-env node*/\n\nconst path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst common = require('../../../webpack.common.cjs');\nconst { merge } = require('webpack-merge');\nrequire('webpack-dev-server');\n\nmodule.exports = (env) =>\n  merge(common, {\n    plugins: [\n      new HtmlWebpackPlugin({\n        publicPath: '/',\n        filename: 'index.html',\n        template: path.resolve(__dirname, 'index.html'),\n        chunks: ['quill'],\n        inject: 'head',\n        scriptLoading: 'blocking',\n      }),\n    ],\n    devServer: {\n      port: env.port,\n      server: 'https',\n      hot: false,\n      liveReload: false,\n      compress: true,\n      client: {\n        overlay: false,\n      },\n      webSocketServer: false,\n    },\n  });\n"
  },
  {
    "path": "packages/quill/test/e2e/fixtures/Clipboard.ts",
    "content": "import type { Page } from '@playwright/test';\nimport { SHORTKEY } from '../utils/index.js';\n\nclass Clipboard {\n  constructor(private page: Page) {}\n\n  async copy() {\n    await this.page.keyboard.press(`${SHORTKEY}+c`);\n  }\n\n  async cut() {\n    await this.page.keyboard.press(`${SHORTKEY}+x`);\n  }\n\n  async paste() {\n    await this.page.keyboard.press(`${SHORTKEY}+v`);\n  }\n\n  async writeText(value: string) {\n    // Playwright + Safari + Linux doesn't support async clipboard API\n    // https://github.com/microsoft/playwright/issues/18901\n    const hasFallbackWritten = await this.page.evaluate((value) => {\n      if (navigator.clipboard) return false;\n      const textArea = document.createElement('textarea');\n      textArea.value = value;\n\n      textArea.style.top = '0';\n      textArea.style.left = '0';\n      textArea.style.position = 'fixed';\n\n      document.body.appendChild(textArea);\n      textArea.focus();\n      textArea.select();\n\n      const isSupported = document.execCommand('copy');\n      textArea.remove();\n      return isSupported;\n    }, value);\n\n    if (!hasFallbackWritten) {\n      await this.write(value, 'text/plain');\n    }\n  }\n\n  async writeHTML(value: string) {\n    return this.write(value, 'text/html');\n  }\n\n  async readText() {\n    return this.read('text/plain');\n  }\n\n  async readHTML() {\n    const html = await this.read('text/html');\n    return html.replace(/<meta[^>]*>/g, '');\n  }\n\n  private async read(type: string) {\n    const isHTML = type === 'text/html';\n    await this.page.evaluate((isHTML) => {\n      const dataContainer = document.createElement(isHTML ? 'div' : 'textarea');\n      if (isHTML) dataContainer.setAttribute('contenteditable', 'true');\n      dataContainer.id = '_readClipboard';\n      document.body.appendChild(dataContainer);\n      dataContainer.focus();\n      return dataContainer;\n    }, isHTML);\n    await this.paste();\n    const locator = this.page.locator('#_readClipboard');\n    const data = await (isHTML ? locator.innerHTML() : locator.inputValue());\n    await locator.evaluate((node) => node.remove());\n    return data;\n  }\n\n  private async write(data: string, type: string) {\n    await this.page.evaluate(\n      async ({ data, type }) => {\n        if (type === 'text/html') {\n          await navigator.clipboard.write([\n            new ClipboardItem({\n              'text/html': new Blob([data], { type: 'text/html' }),\n            }),\n          ]);\n        } else {\n          await navigator.clipboard.writeText(data);\n        }\n      },\n      { data, type },\n    );\n  }\n}\n\nexport default Clipboard;\n"
  },
  {
    "path": "packages/quill/test/e2e/fixtures/Composition.ts",
    "content": "import type {\n  CDPSession,\n  Page,\n  PlaywrightWorkerOptions,\n} from '@playwright/test';\n\nabstract class CompositionSession {\n  abstract update(key: string): Promise<void>;\n  abstract commit(committedText: string): Promise<void>;\n\n  protected composingData = '';\n\n  constructor(protected page: Page) {}\n\n  protected async withKeyboardEvents(\n    key: string,\n    callback: () => Promise<void>,\n  ) {\n    const activeElement = this.page.locator('*:focus');\n\n    await activeElement.dispatchEvent('keydown', { key });\n    await callback();\n    await activeElement.dispatchEvent('keyup', { key });\n  }\n}\n\nclass ChromiumCompositionSession extends CompositionSession {\n  constructor(\n    page: Page,\n    private session: CDPSession,\n  ) {\n    super(page);\n  }\n\n  async update(key: string) {\n    await this.withKeyboardEvents(key, async () => {\n      this.composingData += key;\n\n      await this.session.send('Input.imeSetComposition', {\n        selectionStart: this.composingData.length,\n        selectionEnd: this.composingData.length,\n        text: this.composingData,\n      });\n    });\n  }\n\n  async commit(committedText: string) {\n    await this.withKeyboardEvents('Space', async () => {\n      await this.session.send('Input.insertText', {\n        text: committedText,\n      });\n    });\n  }\n}\n\nclass Composition {\n  constructor(\n    private page: Page,\n    private browserName: PlaywrightWorkerOptions['browserName'],\n  ) {}\n\n  async start() {\n    switch (this.browserName) {\n      case 'chromium': {\n        const session = await this.page.context().newCDPSession(this.page);\n        return new ChromiumCompositionSession(this.page, session);\n      }\n      default:\n        throw new Error(`Unsupported browser: ${this.browserName}`);\n    }\n  }\n}\n\nexport default Composition;\n"
  },
  {
    "path": "packages/quill/test/e2e/fixtures/index.ts",
    "content": "import { test as base } from '@playwright/test';\nimport EditorPage from '../pageobjects/EditorPage.js';\nimport Composition from './Composition.js';\nimport Locker from './utils/Locker.js';\nimport Clipboard from './Clipboard.js';\n\nexport const test = base.extend<{\n  editorPage: EditorPage;\n  clipboard: Clipboard;\n  composition: Composition;\n}>({\n  editorPage: ({ page }, use) => {\n    use(new EditorPage(page));\n  },\n  composition: ({ page, browserName }, use) => {\n    test.fail(\n      browserName !== 'chromium',\n      'CDPSession is only available in Chromium',\n    );\n\n    use(new Composition(page, browserName));\n  },\n  clipboard: [\n    async ({ page }, use) => {\n      const locker = new Locker('clipboard');\n      await locker.lock();\n      await use(new Clipboard(page));\n      await locker.release();\n    },\n    { timeout: 30000 },\n  ],\n});\n\nexport const CHAPTER = 'Chapter 1. Loomings.';\nexport const P1 =\n  'Call me Ishmael. Some years ago—never mind how long precisely-having little or no money in my purse, and nothing particular to interest me on shore.';\nexport const P2 =\n  'There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf.';\n"
  },
  {
    "path": "packages/quill/test/e2e/fixtures/utils/Locker.ts",
    "content": "import { unlink, writeFile } from 'fs/promises';\nimport { unlinkSync } from 'fs';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\nimport { globSync } from 'glob';\n\nconst sleep = (ms: number) =>\n  new Promise((resolve) => {\n    setTimeout(resolve, ms);\n  });\n\nconst PREFIX = 'playwright_locker_';\n\nclass Locker {\n  public static clearAll() {\n    globSync(join(tmpdir(), `${PREFIX}*.txt`)).forEach(unlinkSync);\n  }\n\n  constructor(private key: string) {}\n\n  private get filePath() {\n    return join(tmpdir(), `${PREFIX}${this.key}.txt`);\n  }\n\n  async lock() {\n    try {\n      await writeFile(this.filePath, '', { flag: 'wx' });\n    } catch {\n      await sleep(50);\n      await this.lock();\n    }\n  }\n\n  async release() {\n    await unlink(this.filePath);\n  }\n}\n\nexport default Locker;\n"
  },
  {
    "path": "packages/quill/test/e2e/full.spec.ts",
    "content": "import { expect } from '@playwright/test';\nimport { getSelectionInTextNode, SHORTKEY } from './utils/index.js';\nimport { test, CHAPTER, P1, P2 } from './fixtures/index.js';\n\ntest('compose an epic', async ({ page, editorPage }) => {\n  await editorPage.open();\n  await editorPage.root.pressSequentially('The Whale');\n  expect(await editorPage.root.innerHTML()).toEqual('<p>The Whale</p>');\n\n  await page.keyboard.press('Enter');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    '<p>The Whale</p><p><br></p>',\n  );\n\n  await page.keyboard.press('Enter');\n  await page.keyboard.press('Tab');\n  await editorPage.root.pressSequentially(P1);\n  await page.keyboard.press('Enter');\n  await page.keyboard.press('Enter');\n  await editorPage.root.pressSequentially(P2);\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p>The Whale</p>',\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n\n  // More than enough to get to top\n  await Promise.all(\n    Array(40)\n      .fill(0)\n      .map(() => page.keyboard.press('ArrowUp')),\n  );\n  await page.keyboard.press('ArrowDown');\n  await page.keyboard.press('Enter');\n  await page.type('.ql-editor', CHAPTER);\n  await page.keyboard.press('Enter');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p>The Whale</p>',\n      '<p><br></p>',\n      `<p>${CHAPTER}</p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n\n  // More than enough to get to top\n  await Promise.all(\n    Array(20)\n      .fill(0)\n      .map(() => page.keyboard.press('ArrowUp')),\n  );\n  await page.keyboard.press('ArrowRight');\n  await page.keyboard.press('ArrowRight');\n  await page.keyboard.press('ArrowRight');\n  await page.keyboard.press('ArrowRight');\n  await page.keyboard.press('Backspace');\n  await page.keyboard.press('Backspace');\n  await page.keyboard.press('Backspace');\n  await page.keyboard.press('Backspace');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p>Whale</p>',\n      '<p><br></p>',\n      `<p>${CHAPTER}</p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n\n  await page.keyboard.press('Delete');\n  await page.keyboard.press('Delete');\n  await page.keyboard.press('Delete');\n  await page.keyboard.press('Delete');\n  await page.keyboard.press('Delete');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p><br></p>',\n      '<p><br></p>',\n      `<p>${CHAPTER}</p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n\n  await page.keyboard.press('Delete');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p><br></p>',\n      `<p>${CHAPTER}</p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n\n  await page.click('.ql-toolbar .ql-bold');\n  await page.click('.ql-toolbar .ql-italic');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p><strong><em><span class=\"ql-cursor\">\\uFEFF</span></em></strong></p>',\n      `<p>${CHAPTER}</p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n  let bold = await page.$('.ql-toolbar .ql-bold.ql-active');\n  let italic = await page.$('.ql-toolbar .ql-italic.ql-active');\n  expect(bold).not.toBe(null);\n  expect(italic).not.toBe(null);\n\n  await editorPage.root.pressSequentially('Moby Dick');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p><strong><em>Moby Dick</em></strong></p>',\n      `<p>${CHAPTER}</p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n  bold = await page.$('.ql-toolbar .ql-bold.ql-active');\n  italic = await page.$('.ql-toolbar .ql-italic.ql-active');\n  expect(bold).not.toBe(null);\n  expect(italic).not.toBe(null);\n\n  await page.keyboard.press('ArrowRight');\n  await page.keyboard.down('Shift');\n  await Promise.all(\n    Array(CHAPTER.length)\n      .fill(0)\n      .map(() => page.keyboard.press('ArrowRight')),\n  );\n  await page.keyboard.up('Shift');\n  bold = await page.$('.ql-toolbar .ql-bold.ql-active');\n  italic = await page.$('.ql-toolbar .ql-italic.ql-active');\n  expect(bold).toBe(null);\n  expect(italic).toBe(null);\n\n  await page.keyboard.down(SHORTKEY);\n  await page.keyboard.press('b');\n  await page.keyboard.up(SHORTKEY);\n  bold = await page.$('.ql-toolbar .ql-bold.ql-active');\n  expect(bold).not.toBe(null);\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<p><strong><em>Moby Dick</em></strong></p>',\n      `<p><strong>${CHAPTER}</strong></p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n\n  await page.keyboard.press('ArrowLeft');\n  await page.keyboard.press('ArrowUp');\n  await page.click('.ql-toolbar .ql-header[value=\"1\"]');\n  expect(await editorPage.root.innerHTML()).toEqual(\n    [\n      '<h1><strong><em>Moby Dick</em></strong></h1>',\n      `<p><strong>${CHAPTER}</strong></p>`,\n      '<p><br></p>',\n      `<p>\\t${P1}</p>`,\n      '<p><br></p>',\n      `<p>${P2}</p>`,\n    ].join(''),\n  );\n  const header = await page.$('.ql-toolbar .ql-header.ql-active[value=\"1\"]');\n  expect(header).not.toBe(null);\n\n  await page.keyboard.press('ArrowDown');\n  await page.keyboard.press('ArrowDown');\n  await page.keyboard.press('Enter');\n  await page.keyboard.press('Enter');\n  await page.keyboard.press('ArrowUp');\n  await editorPage.root.pressSequentially('AA');\n  await page.keyboard.press('ArrowLeft');\n  await page.keyboard.down(SHORTKEY);\n  await page.keyboard.press('b');\n  await page.keyboard.press('b');\n  await page.keyboard.up(SHORTKEY);\n  await editorPage.root.pressSequentially('B');\n  expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe('ABA');\n  await page.keyboard.down(SHORTKEY);\n  await page.keyboard.press('b');\n  await page.keyboard.up(SHORTKEY);\n  await editorPage.root.pressSequentially('C');\n  await page.keyboard.down(SHORTKEY);\n  await page.keyboard.press('b');\n  await page.keyboard.up(SHORTKEY);\n  await editorPage.root.pressSequentially('D');\n  expect(await editorPage.root.locator('p').nth(2).innerHTML()).toBe(\n    'AB<strong>C</strong>DA',\n  );\n  const selection = await page.evaluate(getSelectionInTextNode);\n  expect(selection).toBe('[\"DA\",1,\"DA\",1]');\n});\n"
  },
  {
    "path": "packages/quill/test/e2e/history.spec.ts",
    "content": "import { expect } from '@playwright/test';\nimport type { Page } from '@playwright/test';\nimport { test } from './fixtures/index.js';\nimport { SHORTKEY } from './utils/index.js';\n\nconst undo = (page: Page) => page.keyboard.press(`${SHORTKEY}+z`);\nconst redo = (page: Page) => page.keyboard.press(`${SHORTKEY}+Shift+z`);\n\nconst setUserOnly = (page: Page, value: boolean) =>\n  page.evaluate(\n    (value) => {\n      // @ts-expect-error\n      window.quill.history.options.userOnly = value;\n    },\n    [value],\n  );\n\ntest.describe('history', () => {\n  test.beforeEach(async ({ editorPage }) => {\n    await editorPage.open();\n    await editorPage.setContents([{ insert: '1234\\n' }]);\n    await editorPage.cutoffHistory();\n  });\n\n  test('skip changes reverted by api', async ({ page, editorPage }) => {\n    await setUserOnly(page, true);\n    await editorPage.moveCursorAfterText('12');\n    await page.keyboard.type('a');\n    await editorPage.cutoffHistory();\n    await editorPage.selectText('34');\n    await page.keyboard.press(`${SHORTKEY}+b`);\n    await editorPage.cutoffHistory();\n    await editorPage.updateContents([\n      { retain: 3 },\n      { retain: 2, attributes: { bold: null } },\n    ]);\n    await undo(page);\n    expect(await editorPage.getContents()).toEqual([{ insert: '1234\\n' }]);\n  });\n\n  test('clipboard', async ({ clipboard, page, editorPage }) => {\n    await editorPage.moveCursorAfterText('2');\n    await clipboard.writeText('a');\n    await clipboard.paste();\n    await undo(page);\n    expect(await editorPage.getContents()).toEqual([{ insert: '1234\\n' }]);\n  });\n\n  test.describe('selection', () => {\n    test('typing', async ({ page, editorPage }) => {\n      await editorPage.moveCursorAfterText('2');\n      await page.keyboard.type('a');\n      await editorPage.cutoffHistory();\n      await page.keyboard.type('b');\n      await editorPage.cutoffHistory();\n      await page.keyboard.press('Backspace');\n      await editorPage.cutoffHistory();\n      await page.keyboard.type('c');\n      await editorPage.cutoffHistory();\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 3, length: 0 });\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 4, length: 0 });\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 3, length: 0 });\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 2, length: 0 });\n    });\n\n    test('delete forward', async ({ page, editorPage }) => {\n      await editorPage.moveCursorAfterText('3');\n      await page.keyboard.press('Backspace');\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 3, length: 0 });\n      await redo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 2, length: 0 });\n    });\n\n    test('delete selection', async ({ page, editorPage }) => {\n      await editorPage.selectText('23');\n      await page.keyboard.press('Backspace');\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 1, length: 2 });\n      await redo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 1, length: 0 });\n    });\n\n    test('format selection', async ({ page, editorPage }) => {\n      await editorPage.selectText('23');\n      await page.keyboard.press(`${SHORTKEY}+b`);\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 1, length: 2 });\n      await redo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 1, length: 2 });\n    });\n\n    test('combine operations', async ({ page, editorPage }) => {\n      await editorPage.selectText('23');\n      await page.keyboard.type('a');\n      await editorPage.cutoffHistory();\n      await page.keyboard.type('bc');\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 2, length: 0 });\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 1, length: 2 });\n      await redo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 2, length: 0 });\n      await redo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 4, length: 0 });\n    });\n\n    test('api changes', async ({ page, editorPage }) => {\n      await setUserOnly(page, true);\n      await editorPage.selectText('23');\n      await page.keyboard.press('Backspace');\n      await editorPage.cutoffHistory();\n      await page.keyboard.type('a');\n      await editorPage.cutoffHistory();\n      await editorPage.updateContents([{ insert: '0' }]);\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 2, length: 0 });\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 2, length: 2 });\n    });\n\n    test('programmatic user changes', async ({ page, editorPage }) => {\n      await editorPage.moveCursorAfterText('12');\n      await page.keyboard.type('a');\n      await editorPage.cutoffHistory();\n      await editorPage.updateContents([{ insert: '0' }], 'user');\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 3, length: 0 });\n    });\n\n    test('no user selection', async ({ page, editorPage }) => {\n      await editorPage.updateContents([{ retain: 3 }, { insert: '0' }], 'user');\n      await editorPage.root.click();\n      await undo(page);\n      expect(await editorPage.getSelection()).toEqual({ index: 3, length: 0 });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/e2e/list.spec.ts",
    "content": "import { expect } from '@playwright/test';\nimport { test } from './fixtures/index.js';\nimport { isMac, sleep } from './utils/index.js';\n\nconst listTypes = ['bullet', 'checked'];\n\ntest.describe('list', () => {\n  test.beforeEach(async ({ editorPage }) => {\n    await editorPage.open();\n  });\n\n  for (const list of listTypes) {\n    test.describe(`navigation with shortcuts ${list}`, () => {\n      test('jump to line start', async ({ editorPage }) => {\n        await editorPage.setContents([\n          { insert: 'item 1' },\n          { insert: '\\n', attributes: { list } },\n        ]);\n\n        await editorPage.root.click(); // required by Firefox\n        await editorPage.moveCursorAfterText('item 1');\n        await editorPage.root.press(isMac ? `Meta+ArrowLeft` : 'Home');\n        await sleep(500); // internal(uiNode): wait for selectionchange to fire\n\n        expect(await editorPage.getSelection()).toEqual({\n          index: 0,\n          length: 0,\n        });\n\n        await sleep(500); // internal(uiNode): wait for selectionchange to fire\n        await editorPage.root.pressSequentially('start ');\n        expect(await editorPage.getContents()).toEqual([\n          { insert: 'start item 1' },\n          { insert: '\\n', attributes: { list } },\n        ]);\n      });\n\n      test.describe('navigation with left/right arrow keys', () => {\n        test('move to previous/next line', async ({ page, editorPage }) => {\n          const firstLine = 'first line';\n          await editorPage.setContents([\n            { insert: firstLine },\n            { insert: '\\n', attributes: { list } },\n            { insert: 'second line' },\n            { insert: '\\n', attributes: { list } },\n          ]);\n\n          await editorPage.setSelection(firstLine.length + 2, 0);\n          await page.keyboard.press('ArrowLeft');\n          await page.keyboard.press('ArrowLeft');\n          expect(await editorPage.getSelection()).toEqual({\n            index: firstLine.length,\n            length: 0,\n          });\n          await page.keyboard.press('ArrowRight');\n          await sleep(500); // internal(uiNode): wait for selectionchange to fire\n          await page.keyboard.press('ArrowRight');\n          expect(await editorPage.getSelection()).toEqual({\n            index: firstLine.length + 2,\n            length: 0,\n          });\n        });\n\n        test('RTL support', async ({ page, editorPage }) => {\n          const firstLine = 'اللغة العربية';\n          await editorPage.setContents([\n            { insert: firstLine },\n            { insert: '\\n', attributes: { list, direction: 'rtl' } },\n            { insert: 'توحيد اللهجات العربية' },\n            { insert: '\\n', attributes: { list, direction: 'rtl' } },\n          ]);\n\n          await editorPage.setSelection(firstLine.length + 2, 0);\n          await page.keyboard.press('ArrowRight');\n          await page.keyboard.press('ArrowRight');\n          expect(await editorPage.getSelection()).toEqual({\n            index: firstLine.length,\n            length: 0,\n          });\n          await page.keyboard.press('ArrowLeft');\n          await sleep(500); // internal(uiNode): wait for selectionchange to fire\n          await page.keyboard.press('ArrowLeft');\n          expect(await editorPage.getSelection()).toEqual({\n            index: firstLine.length + 2,\n            length: 0,\n          });\n        });\n\n        test('extend selection to previous/next line', async ({\n          page,\n          editorPage,\n        }) => {\n          await editorPage.setContents([\n            { insert: 'first line' },\n            { insert: '\\n', attributes: { list } },\n            { insert: 'second line' },\n            { insert: '\\n', attributes: { list } },\n          ]);\n\n          await editorPage.moveCursorTo('s_econd');\n          await page.keyboard.press('Shift+ArrowLeft');\n          await page.keyboard.press('Shift+ArrowLeft');\n          await page.keyboard.type('a');\n          expect(await editorPage.getContents()).toEqual([\n            { insert: 'first lineaecond line' },\n            { insert: '\\n', attributes: { list } },\n          ]);\n        });\n      });\n\n      // https://github.com/slab/quill/issues/3837\n      test('typing at beginning with IME', async ({\n        editorPage,\n        composition,\n      }) => {\n        await editorPage.setContents([\n          { insert: 'item 1' },\n          { insert: '\\n', attributes: { list } },\n          { insert: '' },\n          { insert: '\\n', attributes: { list } },\n        ]);\n\n        await editorPage.setSelection(7, 0);\n        await editorPage.typeWordWithIME(composition, '我');\n        expect(await editorPage.getContents()).toEqual([\n          { insert: 'item 1' },\n          { insert: '\\n', attributes: { list } },\n          { insert: '我' },\n          { insert: '\\n', attributes: { list } },\n        ]);\n      });\n\n      test('typing in an empty editor with IME and press Backspace', async ({\n        page,\n        editorPage,\n        composition,\n      }) => {\n        await editorPage.setContents([{ insert: '\\n' }]);\n\n        await editorPage.setSelection(9, 0);\n        await editorPage.typeWordWithIME(composition, '我');\n        await page.keyboard.press('Backspace');\n        expect(await editorPage.getContents()).toEqual([{ insert: '\\n' }]);\n      });\n    });\n  }\n\n  test('checklist is checkable', async ({ editorPage, page }) => {\n    await editorPage.setContents([\n      { insert: 'item 1' },\n      { insert: '\\n', attributes: { list: 'unchecked' } },\n    ]);\n\n    await editorPage.setSelection(7, 0);\n    const rect = await editorPage.root.locator('li').evaluate((element) => {\n      return element.getBoundingClientRect();\n    });\n    await page.mouse.click(rect.left + 5, rect.top + 5);\n    expect(await editorPage.getContents()).toEqual([\n      { insert: 'item 1' },\n      { insert: '\\n', attributes: { list: 'checked' } },\n    ]);\n    await page.mouse.click(rect.left + 5, rect.top + 5);\n    expect(await editorPage.getContents()).toEqual([\n      { insert: 'item 1' },\n      { insert: '\\n', attributes: { list: 'unchecked' } },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/e2e/pageobjects/EditorPage.ts",
    "content": "import type { Page } from '@playwright/test';\nimport type Composition from '../fixtures/Composition.js';\n\ninterface Op {\n  insert?: string | Record<string, unknown>;\n  delete?: number;\n  retain?: number | Record<string, unknown>;\n  attributes?: Record<string, unknown>;\n}\n\nconst getTextNodeDef = [\n  'el',\n  'match',\n  `\n  const walk = el.ownerDocument.createTreeWalker(\n    el,\n    NodeFilter.SHOW_TEXT,\n    null,\n    false,\n  );\n  if (!match) {\n    return walk.nextNode();\n  }\n\n  let node;\n  while ((node = walk.nextNode())) {\n    if (node.wholeText.includes(match)) {\n      return node;\n    }\n  }\n  return null;\n`,\n];\n\n// Return after a selection change event is triggered. The purpose is\n// to simulate the actions of a real user, because in reality,\n// users would not perform other actions before the selection event is triggered.\nconst updateSelectionDef = [\n  'range',\n  `\n  return new Promise((resolve) => {\n    document.addEventListener('selectionchange', () => {\n      setTimeout(() => {\n        resolve()\n      }, 1); // wait for Quill to update the internal selection\n    }, {\n      once: true,\n    });\n\n    const selection = document.getSelection();\n    selection?.removeAllRanges();\n    selection?.addRange(range);\n  });\n`,\n];\n\nexport default class EditorPage {\n  constructor(protected readonly page: Page) {}\n\n  get root() {\n    return this.page.locator('.ql-editor');\n  }\n\n  async open() {\n    await this.page.goto('/');\n    await this.page.waitForSelector('.ql-editor', { timeout: 10000 });\n  }\n\n  async html(content: string, title = '') {\n    await this.page.evaluate((html) => {\n      // @ts-expect-error\n      const contents = window.quill.clipboard.convert({ html, text: '\\n' });\n      // @ts-expect-error\n      return window.quill.setContents(contents);\n    }, `<p>${title}</p>${content}`);\n  }\n\n  getSelection() {\n    return this.page.evaluate(() => {\n      // @ts-expect-error\n      return window.quill.getSelection();\n    });\n  }\n\n  async setSelection(index: number, length: number): Promise<void>;\n  async setSelection(range: { index: number; length: number }): Promise<void>;\n  async setSelection(\n    range: { index: number; length: number } | number,\n    length?: number,\n  ) {\n    await this.page.evaluate(\n      // @ts-expect-error\n      (range) => window.quill.setSelection(range),\n      typeof range === 'number' ? { index: range, length: length || 0 } : range,\n    );\n  }\n\n  async typeWordWithIME(composition: Composition, composedWord: string) {\n    const ime = await composition.start();\n    await ime.update('w');\n    await ime.update('o');\n    await ime.commit(composedWord);\n  }\n\n  async cutoffHistory() {\n    await this.page.evaluate(() => {\n      // @ts-expect-error\n      window.quill.history.cutoff();\n    });\n  }\n\n  async updateContents(delta: Op[], source: 'api' | 'user' = 'api') {\n    await this.page.evaluate(\n      ({ delta, source }) => {\n        // @ts-expect-error\n        window.quill.updateContents(delta, source);\n      },\n      { delta, source },\n    );\n  }\n\n  async setContents(delta: Op[]) {\n    await this.page.evaluate((delta) => {\n      // @ts-expect-error\n      window.quill.setContents(delta);\n    }, delta);\n  }\n\n  getContents(): Promise<Op[]> {\n    return this.page.evaluate(() => {\n      // @ts-expect-error\n      return window.quill.getContents().ops;\n    });\n  }\n\n  /**\n   * Move the cursor\n   * @param {string} query text of the destination with `_` indicate the cursor place.\n   */\n  async moveCursorTo(query: string) {\n    const text = query.replace('_', '');\n    await this.waitForText(text);\n    await this.page.evaluate(\n      async ({ getTextNodeDef, updateSelectionDef, query, text }) => {\n        const getTextNode = new Function(...getTextNodeDef);\n        const updateSelection = new Function(...updateSelectionDef);\n\n        const editor = window.document.querySelector('.ql-editor');\n        const node = getTextNode(editor, text) as Text;\n        if (!node) return;\n        const offset = node.wholeText.indexOf(text) + query.indexOf('_');\n\n        const document = node.ownerDocument;\n        const range = document.createRange();\n        range.setStart(node, offset);\n        range.setEnd(node, offset);\n        await updateSelection(range);\n      },\n      { getTextNodeDef, updateSelectionDef, query, text },\n    );\n  }\n\n  moveCursorAfterText(text: string) {\n    return this.moveCursorTo(`${text}_`);\n  }\n\n  moveCursorBeforeText(text: string) {\n    return this.moveCursorTo(`_${text}`);\n  }\n\n  async selectText(start: string, end?: string) {\n    await this.waitForText(start);\n    if (end) {\n      await this.waitForText(end);\n    }\n    await this.page.evaluate(\n      async ({ getTextNodeDef, updateSelectionDef, start, end }) => {\n        const getTextNode = new Function(...getTextNodeDef);\n        const updateSelection = new Function(...updateSelectionDef);\n\n        const editor = window.document.querySelector('.ql-editor');\n        const anchorNode = getTextNode(editor, start) as Text;\n        const focusNode = end ? (getTextNode(editor, end) as Text) : anchorNode;\n        const anchorOffset = anchorNode.wholeText.indexOf(start);\n        const focusOffset = end\n          ? focusNode.wholeText.indexOf(end) + end.length\n          : anchorOffset + start.length;\n\n        const document = anchorNode.ownerDocument;\n        const range = document.createRange();\n        range.setStart(anchorNode, anchorOffset);\n        range.setEnd(focusNode, focusOffset);\n        await updateSelection(range);\n      },\n      { getTextNodeDef, updateSelectionDef, start, end },\n    );\n  }\n\n  private async waitForText(text: string) {\n    await this.page.waitForFunction(\n      ({ getTextNodeDef, text }) => {\n        const getTextNode = new Function(...getTextNodeDef);\n        const editor = window.document.querySelector('.ql-editor');\n        return getTextNode(editor, text);\n      },\n      { getTextNodeDef, text },\n    );\n  }\n}\n"
  },
  {
    "path": "packages/quill/test/e2e/replaceSelection.spec.ts",
    "content": "import { expect } from '@playwright/test';\nimport { test } from './fixtures/index.js';\n\ntest.describe('replace selection', () => {\n  test.beforeEach(async ({ editorPage }) => {\n    await editorPage.open();\n  });\n\n  test.describe('replace a colored text', () => {\n    test('after a normal text', async ({ page, editorPage }) => {\n      await editorPage.setContents([\n        { insert: '1' },\n        { insert: '2', attributes: { color: 'red' } },\n        { insert: '3\\n' },\n      ]);\n      await editorPage.selectText('2', '3');\n      await page.keyboard.type('a');\n      expect(await editorPage.root.innerHTML()).toEqual(\n        '<p>1<span style=\"color: red;\">a</span></p>',\n      );\n      expect(await editorPage.getContents()).toEqual([\n        { insert: '1' },\n        { insert: 'a', attributes: { color: 'red' } },\n        { insert: '\\n' },\n      ]);\n    });\n\n    test('with Enter key', async ({ page, editorPage }) => {\n      await editorPage.setContents([\n        { insert: '1' },\n        { insert: '2', attributes: { color: 'red' } },\n        { insert: '3\\n' },\n      ]);\n      await editorPage.selectText('2', '3');\n      await page.keyboard.press('Enter');\n      expect(await editorPage.root.innerHTML()).toEqual('<p>1</p><p><br></p>');\n      expect(await editorPage.getContents()).toEqual([{ insert: '1\\n\\n' }]);\n    });\n\n    test('with IME', async ({ editorPage, composition }) => {\n      await editorPage.setContents([\n        { insert: '1' },\n        { insert: '2', attributes: { color: 'red' } },\n        { insert: '3\\n' },\n      ]);\n      await editorPage.selectText('2', '3');\n      await editorPage.typeWordWithIME(composition, '我');\n      expect(await editorPage.root.innerHTML()).toEqual('<p>1我</p>');\n      expect(await editorPage.getContents()).toEqual([{ insert: '1我\\n' }]);\n    });\n\n    test('after a bold text', async ({ page, editorPage }) => {\n      await editorPage.setContents([\n        { insert: '1', attributes: { bold: true } },\n        { insert: '2', attributes: { color: 'red' } },\n        { insert: '3\\n' },\n      ]);\n      await editorPage.selectText('2', '3');\n      await page.keyboard.type('a');\n      expect(await editorPage.root.innerHTML()).toEqual(\n        '<p><strong>1</strong><span style=\"color: red;\">a</span></p>',\n      );\n      expect(await editorPage.getContents()).toEqual([\n        { insert: '1', attributes: { bold: true } },\n        { insert: 'a', attributes: { color: 'red' } },\n        { insert: '\\n' },\n      ]);\n    });\n\n    test('across lines', async ({ page, editorPage }) => {\n      await editorPage.setContents([\n        { insert: 'header', attributes: { color: 'red' } },\n        { insert: '\\n', attributes: { header: 1 } },\n        { insert: 'text\\n' },\n      ]);\n      await editorPage.selectText('header', 'text');\n      await page.keyboard.type('a');\n      expect(await editorPage.root.innerHTML()).toEqual(\n        '<h1><span style=\"color: red;\">a</span></h1>',\n      );\n      expect(await editorPage.getContents()).toEqual([\n        { insert: 'a', attributes: { color: 'red' } },\n        { insert: '\\n', attributes: { header: 1 } },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/e2e/utils/index.ts",
    "content": "export const isMac = process.platform === 'darwin';\nexport const SHORTKEY = isMac ? 'Meta' : 'Control';\n\nexport function getSelectionInTextNode() {\n  const selection = document.getSelection();\n  if (!selection) {\n    throw new Error('Selection is null');\n  }\n  const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;\n  return JSON.stringify([\n    (anchorNode as Text).data,\n    anchorOffset,\n    (focusNode as Text).data,\n    focusOffset,\n  ]);\n}\n\nexport const sleep = (ms: number) =>\n  new Promise<void>((r) => {\n    setTimeout(() => {\n      r();\n    }, ms);\n  });\n"
  },
  {
    "path": "packages/quill/test/fuzz/__helpers__/utils.ts",
    "content": "export function randomInt(max: number) {\n  return Math.floor(Math.random() * max);\n}\n\nexport function choose<T>(choices: T[]): T {\n  return choices[randomInt(choices.length)];\n}\n\nexport function runFuzz(testCase: () => void) {\n  const start = performance.now();\n  do {\n    testCase();\n  } while (performance.now() - start < 30 * 1000);\n}\n"
  },
  {
    "path": "packages/quill/test/fuzz/editor.spec.ts",
    "content": "import type { Op } from 'quill-delta';\nimport Delta, { AttributeMap } from 'quill-delta';\nimport { choose, randomInt, runFuzz } from './__helpers__/utils.js';\nimport { AlignClass } from '../../src/formats/align.js';\nimport { FontClass } from '../../src/formats/font.js';\nimport { SizeClass } from '../../src/formats/size.js';\nimport Quill from '../../src/quill.js';\nimport { describe, expect, test } from 'vitest';\n\ntype AttributeDef = { name: string; values: (number | string | boolean)[] };\nconst BLOCK_EMBED_NAME = 'video';\nconst INLINE_EMBED_NAME = 'image';\n\nconst attributeDefs: {\n  text: AttributeDef[];\n  newline: AttributeDef[];\n  inlineEmbed: AttributeDef[];\n  blockEmbed: AttributeDef[];\n} = {\n  text: [\n    { name: 'color', values: ['#ffffff', '#000000', '#ff0000', '#ffff00'] },\n    { name: 'bold', values: [true] },\n    { name: 'code', values: [true] },\n    // @ts-expect-error\n    { name: 'font', values: FontClass.whitelist },\n    // @ts-expect-error\n    { name: 'size', values: SizeClass.whitelist },\n  ],\n  newline: [\n    // @ts-expect-error\n    { name: 'align', values: AlignClass.whitelist },\n    { name: 'header', values: [1, 2, 3, 4, 5] },\n    { name: 'blockquote', values: [true] },\n    { name: 'list', values: ['ordered', 'bullet', 'checked', 'unchecked'] },\n  ],\n  inlineEmbed: [\n    { name: 'width', values: ['100', '200', '300'] },\n    { name: 'height', values: ['100', '200', '300'] },\n  ],\n  blockEmbed: [\n    { name: 'align', values: ['center', 'right'] },\n    { name: 'width', values: ['100', '200', '300'] },\n    { name: 'height', values: ['100', '200', '300'] },\n  ],\n};\n\nconst isLineFinished = (delta: Delta) => {\n  const lastOp = delta.ops[delta.ops.length - 1];\n  if (!lastOp) return false;\n  if (typeof lastOp.insert === 'string') {\n    return lastOp.insert.endsWith('\\n');\n  }\n  if (typeof lastOp.insert === 'object') {\n    const key = Object.keys(lastOp.insert)[0];\n    return key === BLOCK_EMBED_NAME;\n  }\n  throw new Error('invalid op');\n};\n\nconst generateAttributes = (scope: keyof typeof attributeDefs) => {\n  const attributeCount =\n    scope === 'newline'\n      ? // Some block-level formats are exclusive so we only pick one for now for simplicity\n        choose([0, 0, 1])\n      : choose([0, 0, 0, 0, 0, 1, 2, 3, 4]);\n  const attributes: AttributeMap = {};\n  for (let i = 0; i < attributeCount; i += 1) {\n    const def = choose(attributeDefs[scope]);\n    attributes[def.name] = choose(def.values);\n  }\n  return attributes;\n};\n\nconst generateRandomText = () => {\n  return choose([\n    'hi',\n    'world',\n    'Slab',\n    ' ',\n    'this is a long text that contains spaces',\n  ]);\n};\n\ntype SingleInsertValue =\n  | string\n  | { [INLINE_EMBED_NAME]: string }\n  | { [BLOCK_EMBED_NAME]: string };\n\nconst generateSingleInsertDelta = (): Delta['ops'][number] & {\n  insert: SingleInsertValue;\n} => {\n  const operation = choose<keyof typeof attributeDefs>([\n    'text',\n    'text',\n    'text',\n    'newline',\n    'inlineEmbed',\n    'blockEmbed',\n  ]);\n\n  let insert: SingleInsertValue;\n  switch (operation) {\n    case 'text':\n      insert = generateRandomText();\n      break;\n    case 'newline':\n      insert = '\\n';\n      break;\n    case 'inlineEmbed':\n      insert = { [INLINE_EMBED_NAME]: 'https://example.com' };\n      break;\n    case 'blockEmbed': {\n      insert = { [BLOCK_EMBED_NAME]: 'https://example.com' };\n      break;\n    }\n  }\n\n  const attributes = generateAttributes(operation);\n  const op: Op & { insert: SingleInsertValue } = { insert };\n  if (Object.keys(attributes).length) {\n    op.attributes = attributes;\n  }\n  return op;\n};\n\nconst safePushInsert = (delta: Delta, isDoc: boolean) => {\n  const op = generateSingleInsertDelta();\n  if (\n    typeof op.insert === 'object' &&\n    op.insert[BLOCK_EMBED_NAME] &&\n    (!isDoc || (delta.ops.length && !isLineFinished(delta)))\n  ) {\n    delta.insert('\\n');\n  }\n  delta.push(op);\n};\n\nconst generateDocument = () => {\n  const delta = new Delta();\n  const operationCount = 2 + randomInt(20);\n  for (let i = 0; i < operationCount; i += 1) {\n    safePushInsert(delta, true);\n  }\n  if (!isLineFinished(delta)) {\n    delta.insert('\\n');\n  }\n  return delta;\n};\n\nconst generateChange = (\n  doc: Delta,\n  changeCount: number,\n  allowedActions = ['insert', 'delete', 'retain'],\n): Delta => {\n  const docLength = doc.length();\n  const skipLength = allowedActions.includes('retain')\n    ? randomInt(docLength)\n    : 0;\n  let change = new Delta().retain(skipLength);\n  const action = choose(allowedActions);\n  const nextOp = doc.slice(skipLength).ops[0];\n  if (!nextOp) throw new Error('nextOp expected');\n  const needNewline = !isLineFinished(doc.slice(0, skipLength));\n  switch (action) {\n    case 'insert': {\n      const delta = new Delta();\n      const operationCount = randomInt(5) + 1;\n      for (let i = 0; i < operationCount; i += 1) {\n        safePushInsert(delta, false);\n      }\n      if (\n        needNewline ||\n        (typeof nextOp.insert === 'object' && !!nextOp.insert[BLOCK_EMBED_NAME])\n      ) {\n        delta.insert('\\n');\n      }\n      change = change.concat(delta);\n      break;\n    }\n    case 'delete': {\n      const lengthToDelete = randomInt(docLength - skipLength - 1) + 1;\n      const nextOpAfterDelete = doc.slice(skipLength + lengthToDelete).ops[0];\n      if (\n        needNewline &&\n        (!nextOpAfterDelete ||\n          (typeof nextOpAfterDelete.insert === 'object' &&\n            !!nextOpAfterDelete.insert[BLOCK_EMBED_NAME]))\n      ) {\n        change.insert('\\n');\n      }\n      change.delete(lengthToDelete);\n      break;\n    }\n    case 'retain': {\n      const retainLength =\n        typeof nextOp.insert === 'string'\n          ? randomInt(nextOp.insert.length - 1) + 1\n          : 1;\n      if (typeof nextOp.insert === 'string') {\n        if (\n          nextOp.insert.includes('\\n') &&\n          nextOp.insert.replace(/\\n/g, '').length\n        ) {\n          break;\n        }\n        if (nextOp.insert.includes('\\n')) {\n          change.retain(\n            retainLength,\n            AttributeMap.diff(nextOp.attributes, generateAttributes('newline')),\n          );\n        } else {\n          change.retain(\n            retainLength,\n            AttributeMap.diff(nextOp.attributes, generateAttributes('text')),\n          );\n        }\n        break;\n      }\n      break;\n    }\n  }\n  changeCount -= 1;\n  return changeCount <= 0\n    ? change\n    : change.compose(\n        generateChange(doc.compose(change), changeCount, allowedActions),\n      );\n};\n\ndescribe('editor', () => {\n  test('setContents()', () => {\n    runFuzz(() => {\n      const quill = new Quill(document.createElement('div'));\n      const delta = generateDocument();\n\n      expect(delta.concat(new Delta().delete(1))).toEqual(\n        quill.setContents(delta),\n      );\n    });\n  });\n\n  test('updateContents()', () => {\n    runFuzz(() => {\n      const quill = new Quill(document.createElement('div'));\n      const delta = generateDocument();\n      quill.setContents(delta);\n\n      for (let i = 0; i < 800; i += 1) {\n        const doc = quill.getContents();\n        const change = generateChange(doc, randomInt(4) + 1);\n        const diff = quill.updateContents(change);\n        expect(change).toEqual(diff);\n      }\n    });\n  });\n\n  test('insertContents() vs applyDelta()', () => {\n    const quill1 = new Quill(document.createElement('div'));\n    const quill2 = new Quill(document.createElement('div'));\n\n    runFuzz(() => {\n      const delta = generateDocument();\n      quill1.setContents(delta);\n      quill2.setContents(delta);\n\n      const retain = randomInt(delta.length());\n      const change = generateChange(delta, randomInt(20) + 1, ['insert']);\n\n      quill1.editor.insertContents(retain, change);\n      quill2.editor.applyDelta(new Delta().retain(retain).concat(change));\n\n      const contents1 = quill1.getContents().ops;\n      const contents2 = quill2.getContents().ops;\n\n      expect(contents1).toEqual(contents2);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/fuzz/tableEmbed.spec.ts",
    "content": "import type { AttributeMap } from 'quill-delta';\nimport Delta from 'quill-delta';\nimport TableEmbed from '../../src/modules/tableEmbed.js';\nimport type {\n  CellData,\n  TableData,\n  TableRowColumnOp,\n} from '../../src/modules/tableEmbed.js';\nimport { choose, randomInt } from './__helpers__/utils.js';\nimport { beforeAll, describe, expect, test } from 'vitest';\n\nconst getRandomRowColumnId = () => {\n  const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';\n  return new Array(8)\n    .fill(0)\n    .map(() => characters.charAt(Math.floor(Math.random() * characters.length)))\n    .join('');\n};\n\nconst attachAttributes = <T extends object>(\n  obj: T,\n): T & { attributes: Record<string, unknown> } => {\n  const getRandomAttributes = () => {\n    const attributeCount = choose([1, 4, 8]);\n    const allowedAttributes = ['align', 'background', 'color', 'font'];\n    const allowedValues = ['center', 'red', 'left', 'uppercase'];\n    const attributes: AttributeMap = {};\n    new Array(attributeCount).fill(0).forEach(() => {\n      attributes[choose(allowedAttributes)] = choose(allowedValues);\n    });\n    return attributes;\n  };\n  if (choose([true, false])) {\n    // @ts-expect-error\n    obj.attributes = getRandomAttributes();\n  }\n  // @ts-expect-error\n  return obj;\n};\n\nconst getRandomCellContent = () => {\n  const opCount = choose([1, 2, 3]);\n  const delta = new Delta();\n  new Array(opCount).fill(0).forEach(() => {\n    delta.push(\n      attachAttributes({\n        insert: new Array(randomInt(10) + 1)\n          .fill(0)\n          .map(() => choose(['a', 'b', 'c', 'c', 'e', 'f', 'g']))\n          .join(''),\n      }),\n    );\n  });\n  return delta.ops;\n};\n\nconst getRandomChange = (base: Delta) => {\n  const table: TableData = {};\n  const dimension = {\n    rows: new Delta(\n      (base.ops[0].insert as any)['table-embed'].rows || [],\n    ).length(),\n    columns: new Delta(\n      (base.ops[0].insert as any)['table-embed'].columns || [],\n    ).length(),\n  };\n  (['rows', 'columns'] as const).forEach((field) => {\n    const baseLength = dimension[field];\n    const action = choose(['insert', 'delete', 'retain']);\n    const delta = new Delta();\n    switch (action) {\n      case 'insert':\n        delta.retain(randomInt(baseLength + 1));\n        delta.push(\n          attachAttributes({ insert: { id: getRandomRowColumnId() } }),\n        );\n        break;\n      case 'delete':\n        if (baseLength >= 1) {\n          delta.retain(randomInt(baseLength));\n          delta.delete(1);\n        }\n        break;\n      case 'retain':\n        if (baseLength >= 1) {\n          delta.retain(randomInt(baseLength));\n          delta.push(attachAttributes({ retain: 1 }));\n        }\n        break;\n      default:\n        break;\n    }\n    if (delta.length() > 0) {\n      table[field] = delta.ops;\n    }\n  });\n\n  const updateCellCount = choose([0, 1, 2, 3]);\n  new Array(updateCellCount).fill(0).forEach(() => {\n    const row = randomInt(dimension.rows);\n    const column = randomInt(dimension.columns);\n    const cellIdentityToModify = `${row + 1}:${column + 1}`;\n    table.cells = {\n      [cellIdentityToModify]: attachAttributes({\n        content: getRandomCellContent(),\n      }),\n    };\n  });\n  return new Delta([attachAttributes({ retain: { 'table-embed': table } })]);\n};\n\nconst getRandomRowColumnInsert = (count: number): TableRowColumnOp[] => {\n  return new Array(count)\n    .fill(0)\n    .map<TableRowColumnOp>(() =>\n      attachAttributes({ insert: { id: getRandomRowColumnId() } }),\n    );\n};\n\nconst getRandomBase = () => {\n  const rowCount = choose([0, 1, 2, 3]);\n  const columnCount = choose([0, 1, 2]);\n  const cellCount = choose([0, 1, 2, 3, 4, 5]);\n\n  const table: TableData = {};\n  if (rowCount) table.rows = getRandomRowColumnInsert(rowCount);\n  if (columnCount) table.columns = getRandomRowColumnInsert(columnCount);\n  if (cellCount) {\n    const cells: Record<string, CellData> = {};\n    new Array(cellCount).fill(0).forEach(() => {\n      const row = randomInt(rowCount);\n      const column = randomInt(columnCount);\n      const identity = `${row + 1}:${column + 1}`;\n      const cell: CellData = attachAttributes({});\n      if (choose([true, false])) {\n        cell.content = getRandomCellContent();\n      }\n      if (Object.keys(cell).length) {\n        cells[identity] = cell;\n      }\n    });\n    if (Object.keys(cells).length) table.cells = cells;\n  }\n  return new Delta([{ insert: { 'table-embed': table } }]);\n};\n\nconst runTestCase = () => {\n  const base = getRandomBase();\n  const change = getRandomChange(base);\n  expect(base).toEqual(base.compose(change).compose(change.invert(base)));\n\n  const anotherChange = getRandomChange(base);\n  expect(change.compose(change.transform(anotherChange, true))).toEqual(\n    anotherChange.compose(anotherChange.transform(change)),\n  );\n};\n\ndescribe('tableEmbed', () => {\n  beforeAll(() => {\n    TableEmbed.register();\n  });\n\n  test('delta', () => {\n    for (let i = 0; i < 20; i += 1) {\n      for (let j = 0; j < 1000; j += 1) {\n        runTestCase();\n      }\n    }\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/fuzz/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  resolve: {\n    extensions: ['.ts', '.js'],\n  },\n  test: {\n    include: ['test/fuzz/**/*.spec.ts'],\n    environment: 'jsdom',\n    testTimeout: 40000,\n    pool: 'threads',\n  },\n});\n"
  },
  {
    "path": "packages/quill/test/types/quill.test-d.ts",
    "content": "import { assertType, expectTypeOf } from 'vitest';\nimport Quill, { Delta } from '../../src/quill.js';\nimport type { EmitterSource, Parchment, Range } from '../../src/quill.js';\nimport type { default as Block, BlockEmbed } from '../../src/blots/block.js';\nimport SnowTheme from '../../src/themes/snow.js';\nimport { LeafBlot } from 'parchment';\n\n{\n  const Counter = (quill: Quill, options: { unit: string }) => {\n    console.log(quill, options);\n  };\n  Quill.register('modules/counter', Counter);\n  Quill.register('themes/snow', SnowTheme);\n  Quill.register('themes/snow', SnowTheme, true);\n\n  class MyBlot extends LeafBlot {}\n\n  Quill.register(MyBlot);\n  Quill.register(MyBlot, true);\n  // @ts-expect-error\n  Quill.register(SnowTheme);\n  Quill.register({\n    'modules/counter': Counter,\n    'themes/snow': SnowTheme,\n    'formats/my-blot': MyBlot,\n  });\n  Quill.register(\n    {\n      'modules/counter': Counter,\n      'themes/snow': SnowTheme,\n      'formats/my-blot': MyBlot,\n    },\n    true,\n  );\n}\n\nconst quill = new Quill('#editor');\n\n{\n  quill.deleteText(0, 1);\n  quill.deleteText(0, 1, 'api');\n  quill.deleteText({ index: 0, length: 1 });\n  quill.deleteText({ index: 0, length: 1 }, 'api');\n}\n\n{\n  assertType<Delta>(quill.getContents());\n  assertType<Delta>(quill.getContents(1));\n  assertType<Delta>(quill.getContents(1, 2));\n}\n\n{\n  assertType<number>(quill.getLength());\n}\n\n{\n  assertType<string>(quill.getSemanticHTML());\n  assertType<string>(quill.getSemanticHTML(1));\n  assertType<string>(quill.getSemanticHTML(1, 2));\n}\n\n{\n  assertType<Delta>(\n    quill.insertEmbed(10, 'image', 'https://example.com/logo.png'),\n  );\n  assertType<Delta>(\n    quill.insertEmbed(10, 'image', 'https://example.com/logo.png', 'api'),\n  );\n}\n\n{\n  quill.insertText(0, 'Hello');\n  quill.insertText(0, 'Hello', 'api');\n  quill.insertText(0, 'Hello', 'bold', true);\n  quill.insertText(0, 'Hello', 'bold', true, 'api');\n  quill.insertText(5, 'Quill', {\n    color: '#ffff00',\n    italic: true,\n  });\n  quill.insertText(\n    5,\n    'Quill',\n    {\n      color: '#ffff00',\n      italic: true,\n    },\n    'api',\n  );\n}\n\n{\n  quill.enable();\n  quill.enable(true);\n}\n\n{\n  quill.disable();\n}\n\n{\n  assertType<boolean>(quill.editReadOnly(() => true));\n  assertType<string>(quill.editReadOnly(() => 'success'));\n}\n\n{\n  quill.setText('Hello World!');\n  quill.setText('Hello World!', 'api');\n}\n\n{\n  assertType<Delta>(quill.updateContents([{ insert: 'Hello World!' }]));\n  assertType<Delta>(quill.updateContents([{ insert: 'Hello World!' }], 'api'));\n  assertType<Delta>(quill.updateContents(new Delta().insert('Hello World!')));\n  assertType<Delta>(\n    quill.updateContents(new Delta().insert('Hello World!'), 'api'),\n  );\n}\n\n{\n  assertType<Delta>(quill.setContents([{ insert: 'Hello World!\\n' }]));\n  assertType<Delta>(quill.setContents([{ insert: 'Hello World!\\n' }], 'api'));\n  assertType<Delta>(quill.setContents(new Delta().insert('Hello World!\\n')));\n  assertType<Delta>(\n    quill.setContents(new Delta().insert('Hello World!\\n'), 'api'),\n  );\n}\n\n{\n  assertType<Delta>(quill.format('bold', true));\n  assertType<Delta>(quill.format('bold', true, 'api'));\n}\n\n{\n  quill.formatText(0, 1, 'bold', true);\n  quill.formatText(0, 1, 'bold', true, 'api');\n  quill.formatText(0, 5, {\n    bold: false,\n    color: 'rgb(0, 0, 255)',\n  });\n  quill.formatText(\n    0,\n    5,\n    {\n      bold: false,\n      color: 'rgb(0, 0, 255)',\n    },\n    'api',\n  );\n}\n\n{\n  quill.formatLine(0, 1, 'bold', true);\n  quill.formatLine(0, 1, 'bold', true, 'api');\n  quill.formatLine(0, 5, {\n    bold: false,\n    color: 'rgb(0, 0, 255)',\n  });\n  quill.formatLine(\n    0,\n    5,\n    {\n      bold: false,\n      color: 'rgb(0, 0, 255)',\n    },\n    'api',\n  );\n}\n\n{\n  quill.getFormat();\n  quill.getFormat(1);\n  quill.getFormat(1, 10);\n  quill.getFormat({ index: 1, length: 1 });\n}\n\n{\n  assertType<Delta>(quill.removeFormat(3, 2));\n  assertType<Delta>(quill.removeFormat(3, 2, 'user'));\n}\n\n{\n  quill.getBounds(3, 2);\n}\n\n{\n  quill.getSelection();\n  quill.getSelection(true);\n}\n\n{\n  quill.setSelection(1, 2);\n  quill.setSelection(1, 2, 'api');\n  quill.setSelection({ index: 1, length: 2 });\n  quill.setSelection({ index: 1, length: 2 }, 'api');\n}\n\n{\n  quill.scrollSelectionIntoView();\n  quill.scrollSelectionIntoView({ smooth: true });\n}\n\n{\n  quill.blur();\n}\n\n{\n  quill.focus();\n}\n\n{\n  assertType<boolean>(quill.hasFocus());\n}\n\n{\n  quill.update();\n  quill.update('user');\n}\n\n{\n  quill.scrollRectIntoView({ left: 0, right: 0, top: 0, bottom: 0 });\n  quill.scrollRectIntoView(\n    document.createElement('div').getBoundingClientRect(),\n  );\n  quill.scrollRectIntoView(\n    document.createElement('div').getBoundingClientRect(),\n    { smooth: true },\n  );\n}\n\n{\n  quill.on('text-change', (delta, oldDelta, source) => {\n    expectTypeOf<Delta>(delta);\n    expectTypeOf<Delta>(oldDelta);\n    expectTypeOf<EmitterSource>(source);\n  });\n}\n\n{\n  quill.on('selection-change', (range, oldRange, source) => {\n    expectTypeOf<Range>(range);\n    expectTypeOf<Range>(oldRange);\n    expectTypeOf<EmitterSource>(source);\n  });\n}\n\n{\n  assertType<[Parchment.LeafBlot | null, number]>(quill.getLeaf(0));\n}\n\n{\n  assertType<[BlockEmbed | Block | null, number]>(quill.getLine(0));\n}\n\n{\n  assertType<(BlockEmbed | Block)[]>(quill.getLines(0));\n  assertType<(BlockEmbed | Block)[]>(quill.getLines(0, 10));\n}\n"
  },
  {
    "path": "packages/quill/test/unit/__helpers__/cleanup.ts",
    "content": "import { beforeEach } from 'vitest';\n\nbeforeEach(() => {\n  document.body.innerHTML = '';\n});\n"
  },
  {
    "path": "packages/quill/test/unit/__helpers__/expect.ts",
    "content": "import { expect } from 'vitest';\nimport { normalizeHTML } from './utils.js';\n\nconst sortAttributes = (element: HTMLElement) => {\n  const attributes = Array.from(element.attributes);\n  const sortedAttributes = attributes.sort((a, b) =>\n    a.name.localeCompare(b.name),\n  );\n\n  while (element.attributes.length > 0) {\n    element.removeAttribute(element.attributes[0].name);\n  }\n\n  for (const attr of sortedAttributes) {\n    element.setAttribute(attr.name, attr.value);\n  }\n\n  element.childNodes.forEach((child) => {\n    if (child instanceof HTMLElement) {\n      sortAttributes(child);\n    }\n  });\n};\n\nexpect.extend({\n  toEqualHTML(received, expected, options: { ignoreAttrs?: string[] } = {}) {\n    const ignoreAttrs = options?.ignoreAttrs ?? [];\n    const receivedDOM = document.createElement('div');\n    const expectedDOM = document.createElement('div');\n    receivedDOM.innerHTML = normalizeHTML(\n      typeof received === 'string' ? received : received.innerHTML,\n    );\n    expectedDOM.innerHTML = normalizeHTML(expected);\n\n    const doms = [receivedDOM, expectedDOM];\n\n    doms.forEach((dom) => {\n      Array.from(dom.querySelectorAll('.ql-ui')).forEach((node) => {\n        node.remove();\n      });\n\n      ignoreAttrs.forEach((attr) => {\n        Array.from(dom.querySelectorAll(`[${attr}]`)).forEach((node) => {\n          node.removeAttribute(attr);\n        });\n      });\n\n      sortAttributes(dom);\n    });\n\n    if (this.equals(receivedDOM.innerHTML, expectedDOM.innerHTML)) {\n      return { pass: true, message: () => '' };\n    }\n    return {\n      pass: false,\n      message: () =>\n        `HTMLs don't match.\\n${this.utils.diff(\n          this.utils.stringify(receivedDOM),\n          this.utils.stringify(expectedDOM),\n        )}`,\n    };\n  },\n});\n"
  },
  {
    "path": "packages/quill/test/unit/__helpers__/factory.ts",
    "content": "import { Registry } from 'parchment';\nimport type { Attributor } from 'parchment';\n\nimport Block from '../../../src/blots/block.js';\nimport Break from '../../../src/blots/break.js';\nimport Cursor from '../../../src/blots/cursor.js';\nimport Scroll from '../../../src/blots/scroll.js';\nimport TextBlot from '../../../src/blots/text.js';\nimport ListItem, { ListContainer } from '../../../src/formats/list.js';\nimport Inline from '../../../src/blots/inline.js';\nimport Emitter from '../../../src/core/emitter.js';\nimport { normalizeHTML } from './utils.js';\n\nexport const createRegistry = (formats: unknown[] = []) => {\n  const registry = new Registry();\n\n  formats.forEach((format) => {\n    registry.register(format as Attributor);\n  });\n  registry.register(Block);\n  registry.register(Break);\n  registry.register(Cursor);\n  registry.register(Inline);\n  registry.register(Scroll);\n  registry.register(TextBlot);\n  registry.register(ListContainer);\n  registry.register(ListItem);\n\n  return registry;\n};\n\nexport const createScroll = (\n  html: string | { html: string },\n  registry = createRegistry(),\n  container = document.body,\n) => {\n  const emitter = new Emitter();\n  const root = container.appendChild(document.createElement('div'));\n  root.innerHTML = normalizeHTML(html);\n  const scroll = new Scroll(registry, root, {\n    emitter,\n  });\n  return scroll;\n};\n"
  },
  {
    "path": "packages/quill/test/unit/__helpers__/utils.ts",
    "content": "export const sleep = (ms: number) =>\n  new Promise<void>((r) => {\n    setTimeout(() => {\n      r();\n    }, ms);\n  });\n\nexport const normalizeHTML = (html: string | { html: string }) =>\n  typeof html === 'object' ? html.html : html.replace(/\\n\\s*/g, '');\n"
  },
  {
    "path": "packages/quill/test/unit/__helpers__/vitest.d.ts",
    "content": "import type { Assertion, AsymmetricMatchersContaining } from 'vitest';\n\ninterface CustomMatchers<R = unknown> {\n  toEqualHTML(html: string, options?: { ignoreAttrs?: string[] }): R;\n}\n\ndeclare module 'vitest' {\n  interface Assertion<T = any> extends CustomMatchers<T> {}\n  interface AsymmetricMatchersContaining extends CustomMatchers {}\n}\n"
  },
  {
    "path": "packages/quill/test/unit/blots/block-embed.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport Video from '../../../src/formats/video.js';\nimport Image from '../../../src/formats/image.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([Video, Image]));\n\ndescribe('Block Embed', () => {\n  test('insert', () => {\n    const scroll = createScroll('<p>0123</p>');\n    scroll.insertAt(2, 'video', '#');\n    expect(scroll.domNode).toEqualHTML(`\n      <p>01</p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <p>23</p>\n    `);\n  });\n\n  test('split newline', () => {\n    const scroll = createScroll('<p>0123</p>');\n    scroll.insertAt(4, 'video', '#');\n    expect(scroll.domNode).toEqualHTML(`\n      <p>0123</p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <p><br></p>\n    `);\n  });\n\n  test('insert end of document', () => {\n    const scroll = createScroll('<p>0123</p>');\n    scroll.insertAt(5, 'video', '#');\n    expect(scroll.domNode).toEqualHTML(`\n      <p>0123</p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert text before', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(0, 'Test');\n    expect(scroll.domNode).toEqualHTML(`\n      <p>Test</p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert text after', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(1, 'Test');\n    expect(scroll.domNode).toEqualHTML(`\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <p>Test</p>\n    `);\n  });\n\n  test('insert inline embed before', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(0, 'image', '/assets/favicon.png');\n    expect(scroll.domNode).toEqualHTML(`\n      <p><img src=\"/assets/favicon.png\"></p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert inline embed after', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(1, 'image', '/assets/favicon.png');\n    expect(scroll.domNode).toEqualHTML(`\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <p><img src=\"/assets/favicon.png\"></p>\n    `);\n  });\n\n  test('insert block embed before', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(0, 'video', '#1');\n    expect(scroll.domNode).toEqualHTML(`\n      <iframe src=\"#1\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert block embed after', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(1, 'video', '#1');\n    expect(scroll.domNode).toEqualHTML(`\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <iframe src=\"#1\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert newline before', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(0, '\\n');\n    scroll.optimize();\n    expect(scroll.domNode).toEqualHTML(`\n      <p><br></p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert multiple newlines before', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(0, '\\n\\n\\n');\n    scroll.optimize();\n    expect(scroll.domNode).toEqualHTML(`\n      <p><br></p>\n      <p><br></p>\n      <p><br></p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n\n  test('insert newline after', () => {\n    const scroll = createScroll(\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.insertAt(1, '\\n');\n    scroll.optimize();\n    expect(scroll.domNode).toEqualHTML(`\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n      <p><br></p>\n    `);\n  });\n\n  test('delete preceding newline', () => {\n    const scroll = createScroll(\n      '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n    );\n    scroll.deleteAt(4, 1);\n    expect(scroll.domNode).toEqualHTML(`\n      <p>0123</p>\n      <iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/blots/block.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport Header from '../../../src/formats/header.js';\nimport Bold from '../../../src/formats/bold.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([Header, Bold]));\n\ndescribe('Block', () => {\n  test('childless', () => {\n    const scroll = createScroll('');\n    const block = scroll.create('block');\n    // @ts-expect-error\n    block.optimize();\n    expect(block.domNode).toEqualHTML('<br>');\n  });\n\n  test('insert into empty', () => {\n    const scroll = createScroll('');\n    const block = scroll.create('block');\n    block.insertAt(0, 'Test');\n    expect(block.domNode).toEqualHTML('Test');\n  });\n\n  test('insert newlines', () => {\n    const scroll = createScroll('<p><br></p>');\n    scroll.insertAt(0, '\\n\\n\\n');\n    expect(scroll.domNode).toEqualHTML(\n      '<p><br></p><p><br></p><p><br></p><p><br></p>',\n    );\n  });\n\n  test('insert multiline', () => {\n    const scroll = createScroll('<p>Hello World!</p>');\n    scroll.insertAt(6, 'pardon\\nthis\\n\\ninterruption\\n');\n    expect(scroll.domNode).toEqualHTML(`\n      <p>Hello pardon</p>\n      <p>this</p>\n      <p><br></p>\n      <p>interruption</p>\n      <p>World!</p>\n    `);\n  });\n\n  test('insert into formatted', () => {\n    const scroll = createScroll('<h1>Welcome</h1>');\n    scroll.insertAt(3, 'l\\n');\n    // @ts-expect-error\n    expect(scroll.domNode.firstChild?.outerHTML).toEqualHTML('<h1>Well</h1>');\n    // @ts-expect-error\n    expect(scroll.domNode.childNodes[1]?.outerHTML).toEqualHTML(\n      '<h1>come</h1>',\n    );\n  });\n\n  test('delete line contents', () => {\n    const scroll = createScroll('<p>Hello</p><p>World!</p>');\n    scroll.deleteAt(0, 5);\n    expect(scroll.domNode).toEqualHTML('<p><br></p><p>World!</p>');\n  });\n\n  test('join lines', () => {\n    const scroll = createScroll('<h1>Hello</h1><h2>World!</h2>');\n    scroll.deleteAt(5, 1);\n    expect(scroll.domNode).toEqualHTML('<h2>HelloWorld!</h2>');\n  });\n\n  test('join line with empty', () => {\n    const scroll = createScroll(\n      '<p>Hello<strong>World</strong></p><p><br></p>',\n    );\n    scroll.deleteAt(10, 1);\n    expect(scroll.domNode).toEqualHTML('<p>Hello<strong>World</strong></p>');\n  });\n\n  test('join empty lines', () => {\n    const scroll = createScroll('<h1><br></h1><p><br></p>');\n    scroll.deleteAt(1, 1);\n    expect(scroll.domNode).toEqualHTML('<h1><br></h1>');\n  });\n\n  test('format empty', () => {\n    const scroll = createScroll('<p><br></p>');\n    scroll.formatAt(0, 1, 'header', 1);\n    expect(scroll.domNode).toEqualHTML('<h1><br></h1>');\n  });\n\n  test('format newline', () => {\n    const scroll = createScroll('<h1>Hello</h1>');\n    scroll.formatAt(5, 1, 'header', 2);\n    expect(scroll.domNode).toEqualHTML('<h2>Hello</h2>');\n  });\n\n  test('remove unnecessary break', () => {\n    const scroll = createScroll('<p>Test</p>');\n    scroll.children.head?.domNode.appendChild(document.createElement('br'));\n    scroll.update();\n    expect(scroll.domNode).toEqualHTML('<p>Test</p>');\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/blots/inline.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport { createRegistry, createScroll } from '../__helpers__/factory.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Italic from '../../../src/formats/italic.js';\n\ndescribe('Inline', () => {\n  test('format order', () => {\n    const scroll = createScroll(\n      '<p>Hello World!</p>',\n      createRegistry([Bold, Italic]),\n    );\n    scroll.formatAt(0, 1, 'bold', true);\n    scroll.formatAt(0, 1, 'italic', true);\n    scroll.formatAt(2, 1, 'italic', true);\n    scroll.formatAt(2, 1, 'bold', true);\n    expect(scroll.domNode).toEqualHTML(\n      '<p><strong><em>H</em></strong>e<strong><em>l</em></strong>lo World!</p>',\n    );\n  });\n\n  test('reorder', () => {\n    const scroll = createScroll(\n      '<p>0<strong>12</strong>3</p>',\n      createRegistry([Bold, Italic]),\n    );\n    const p = scroll.domNode.firstChild as HTMLParagraphElement;\n    const em = document.createElement('em');\n    Array.from(p.childNodes).forEach((node) => {\n      em.appendChild(node);\n    });\n    p.appendChild(em);\n    expect(scroll.domNode).toEqualHTML('<p><em>0<strong>12</strong>3</em></p>');\n    scroll.update();\n    expect(scroll.domNode).toEqualHTML(\n      '<p><em>0</em><strong><em>12</em></strong><em>3</em></p>',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/blots/scroll.spec.ts",
    "content": "import { describe, expect, test, vitest } from 'vitest';\nimport Emitter from '../../../src/core/emitter.js';\nimport Selection, { Range } from '../../../src/core/selection.js';\nimport Cursor from '../../../src/blots/cursor.js';\nimport Scroll from '../../../src/blots/scroll.js';\nimport Delta from 'quill-delta';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport { normalizeHTML, sleep } from '../__helpers__/utils.js';\nimport Underline from '../../../src/formats/underline.js';\nimport Strike from '../../../src/formats/strike.js';\n\nconst createScroll = (html: string) => {\n  const emitter = new Emitter();\n  const registry = createRegistry([Underline, Strike]);\n  const container = document.body.appendChild(document.createElement('div'));\n  container.innerHTML = normalizeHTML(html);\n  return new Scroll(registry, container, { emitter });\n};\n\ndescribe('Scroll', () => {\n  test('initialize empty document', () => {\n    const scroll = createScroll('');\n    expect(scroll.domNode).toEqualHTML('<p><br></p>');\n  });\n\n  test('api change', () => {\n    const scroll = createScroll('<p>Hello World!</p>');\n    vitest.spyOn(scroll.emitter, 'emit');\n    scroll.insertAt(5, '!');\n    expect(scroll.emitter.emit).toHaveBeenCalledWith(\n      Emitter.events.SCROLL_OPTIMIZE,\n      expect.any(Array),\n      expect.any(Object),\n    );\n  });\n\n  test('user change', async () => {\n    const scroll = createScroll('<p>Hello World!</p>');\n    vitest.spyOn(scroll.emitter, 'emit');\n    scroll.domNode.firstChild?.appendChild(document.createTextNode('!'));\n    await sleep(1);\n    expect(scroll.emitter.emit).toHaveBeenCalledWith(\n      Emitter.events.SCROLL_OPTIMIZE,\n      expect.any(Array),\n      expect.any(Object),\n    );\n    expect(scroll.emitter.emit).toHaveBeenCalledWith(\n      Emitter.events.SCROLL_UPDATE,\n      Emitter.sources.USER,\n      expect.any(Array),\n    );\n  });\n\n  test('prevent dragstart', () => {\n    const scroll = createScroll('<p>Hello World!</p>');\n    const dragstart = new Event('dragstart');\n    vitest.spyOn(dragstart, 'preventDefault');\n    scroll.domNode.dispatchEvent(dragstart);\n    expect(dragstart.preventDefault).toHaveBeenCalled();\n  });\n\n  describe('leaf()', () => {\n    test('text', () => {\n      const scroll = createScroll('<p>Tests</p>');\n      const [leaf, offset] = scroll.leaf(2);\n      expect(leaf?.value()).toEqual('Tests');\n      expect(offset).toEqual(2);\n    });\n\n    test('precise', () => {\n      const scroll = createScroll(\n        '<p><u>0</u><s>1</s><u>2</u><s>3</s><u>4</u></p>',\n      );\n      const [leaf, offset] = scroll.leaf(3);\n      expect(leaf?.value()).toEqual('2');\n      expect(offset).toEqual(1);\n    });\n\n    test('newline', () => {\n      const scroll = createScroll('<p>0123</p><p>5678</p>');\n      const [leaf, offset] = scroll.leaf(4);\n      expect(leaf?.value()).toEqual('0123');\n      expect(offset).toEqual(4);\n    });\n\n    test('cursor', () => {\n      const scroll = createScroll('<p><u>0</u>1<u>2</u></p>');\n      const selection = new Selection(scroll, scroll.emitter);\n      selection.setRange(new Range(2));\n      selection.format('strike', true);\n      const [leaf, offset] = selection.scroll.leaf(2);\n      expect(leaf instanceof Cursor).toBe(true);\n      expect(offset).toEqual(0);\n    });\n\n    test('beyond document', () => {\n      const scroll = createScroll('<p>Test</p>');\n      const [leaf, offset] = scroll.leaf(10);\n      expect(leaf).toEqual(null);\n      expect(offset).toEqual(-1);\n    });\n  });\n\n  describe('insertContents()', () => {\n    test('does not mutate the input', () => {\n      const scroll = createScroll('<p>Test</p>');\n      const delta = new Delta().insert('\\n');\n      const clonedDelta = new Delta(structuredClone(delta.ops));\n      scroll.insertContents(0, delta);\n      expect(delta.ops).toEqual(clonedDelta.ops);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/core/composition.spec.ts",
    "content": "import Emitter from '../../../src/core/emitter.js';\nimport Composition from '../../../src/core/composition.js';\nimport Scroll from '../../../src/blots/scroll.js';\nimport { describe, expect, test, vitest } from 'vitest';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport Quill from '../../../src/core.js';\n\ndescribe('Composition', () => {\n  test('triggers events on compositionstart', async () => {\n    const emitter = new Emitter();\n    const scroll = new Scroll(createRegistry(), document.createElement('div'), {\n      emitter,\n    });\n    new Composition(scroll, emitter);\n\n    vitest.spyOn(emitter, 'emit');\n\n    const event = new CompositionEvent('compositionstart');\n    scroll.domNode.dispatchEvent(event);\n    expect(emitter.emit).toHaveBeenCalledWith(\n      Quill.events.COMPOSITION_BEFORE_START,\n      event,\n    );\n    expect(emitter.emit).toHaveBeenCalledWith(\n      Quill.events.COMPOSITION_START,\n      event,\n    );\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/core/editor.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport Block from '../../../src/blots/block.js';\nimport { Range } from '../../../src/core/selection.js';\nimport Scroll from '../../../src/blots/scroll.js';\nimport { Registry } from 'parchment';\nimport Text from '../../../src/blots/text.js';\nimport Emitter from '../../../src/core/emitter.js';\nimport Break from '../../../src/blots/break.js';\nimport { describe, expect, test } from 'vitest';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport List, { ListContainer } from '../../../src/formats/list.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Image from '../../../src/formats/image.js';\nimport Link from '../../../src/formats/link.js';\nimport { FontClass } from '../../../src/formats/font.js';\nimport Header from '../../../src/formats/header.js';\nimport Italic from '../../../src/formats/italic.js';\nimport { AlignClass } from '../../../src/formats/align.js';\nimport Video from '../../../src/formats/video.js';\nimport Strike from '../../../src/formats/strike.js';\nimport Underline from '../../../src/formats/underline.js';\nimport CodeBlock, { CodeBlockContainer } from '../../../src/formats/code.js';\nimport { SizeClass } from '../../../src/formats/size.js';\nimport Blockquote from '../../../src/formats/blockquote.js';\nimport IndentClass from '../../../src/formats/indent.js';\nimport { ColorClass } from '../../../src/formats/color.js';\nimport Quill from '../../../src/core.js';\nimport { normalizeHTML } from '../__helpers__/utils.js';\n\nconst createEditor = (htmlOrContents: string | Delta) => {\n  const container = document.createElement('div');\n  if (typeof htmlOrContents === 'string') {\n    container.innerHTML = normalizeHTML(htmlOrContents);\n  }\n  document.body.appendChild(container);\n  const quill = new Quill(container, {\n    registry: createRegistry([\n      ListContainer,\n      List,\n      IndentClass,\n      Bold,\n      Image,\n      ColorClass,\n      Link,\n      FontClass,\n      Header,\n      Italic,\n      AlignClass,\n      Video,\n      Strike,\n      Underline,\n      CodeBlock,\n      CodeBlockContainer,\n      Blockquote,\n      SizeClass,\n    ]),\n  });\n  if (typeof htmlOrContents !== 'string') {\n    quill.setContents(htmlOrContents);\n  }\n  return quill.editor;\n};\n\ndescribe('Editor', () => {\n  describe('insert', () => {\n    test('text', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertText(2, '!!');\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('01!!23', { bold: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><strong>01!!23</strong></p>',\n      );\n    });\n\n    test('embed', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertEmbed(2, 'image', '/assets/favicon.png');\n      expect(editor.getDelta()).toEqual(\n        new Delta()\n          .insert('01', { bold: true })\n          .insert({ image: '/assets/favicon.png' }, { bold: true })\n          .insert('23', { bold: true })\n          .insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><strong>01<img src=\"/assets/favicon.png\">23</strong></p>',\n      );\n    });\n\n    test('on empty line', () => {\n      const editor = createEditor('<p>0</p><p><br></p><p>3</p>');\n      editor.insertText(2, '!');\n      expect(editor.getDelta()).toEqual(new Delta().insert('0\\n!\\n3\\n'));\n      expect(editor.scroll.domNode).toEqualHTML('<p>0</p><p>!</p><p>3</p>');\n    });\n\n    test('end of document', () => {\n      const editor = createEditor('<p>Hello</p>');\n      editor.insertText(6, 'World!');\n      expect(editor.getDelta()).toEqual(new Delta().insert('Hello\\nWorld!\\n'));\n      expect(editor.scroll.domNode).toEqualHTML('<p>Hello</p><p>World!</p>');\n    });\n\n    test('end of document with newline', () => {\n      const editor = createEditor('<p>Hello</p>');\n      editor.insertText(6, 'World!\\n');\n      expect(editor.getDelta()).toEqual(new Delta().insert('Hello\\nWorld!\\n'));\n      expect(editor.scroll.domNode).toEqualHTML('<p>Hello</p><p>World!</p>');\n    });\n\n    test('embed at end of document with newline', () => {\n      const editor = createEditor('<p>Hello</p>');\n      editor.insertEmbed(6, 'image', '/assets/favicon.png');\n      expect(editor.getDelta()).toEqual(\n        new Delta()\n          .insert('Hello\\n')\n          .insert({ image: '/assets/favicon.png' })\n          .insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>Hello</p><p><img src=\"/assets/favicon.png\"></p>',\n      );\n    });\n\n    test('newline splitting', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertText(2, '\\n');\n      expect(editor.getDelta()).toEqual(\n        new Delta()\n          .insert('01', { bold: true })\n          .insert('\\n')\n          .insert('23', { bold: true })\n          .insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p><strong>01</strong></p>\n        <p><strong>23</strong></p>`);\n    });\n\n    test('prepend newline', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertText(0, '\\n');\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('\\n').insert('0123', { bold: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p><br></p>\n        <p><strong>0123</strong></p>`);\n    });\n\n    test('append newline', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertText(4, '\\n');\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('0123', { bold: true }).insert('\\n\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p><strong>0123</strong></p>\n        <p><br></p>`);\n    });\n\n    test('multiline text', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertText(2, '\\n!!\\n!!\\n');\n      expect(editor.getDelta()).toEqual(\n        new Delta()\n          .insert('01', { bold: true })\n          .insert('\\n')\n          .insert('!!', { bold: true })\n          .insert('\\n')\n          .insert('!!', { bold: true })\n          .insert('\\n')\n          .insert('23', { bold: true })\n          .insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p><strong>01</strong></p>\n        <p><strong>!!</strong></p>\n        <p><strong>!!</strong></p>\n        <p><strong>23</strong></p>`);\n    });\n\n    test('multiple newlines', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.insertText(2, '\\n\\n');\n      expect(editor.getDelta()).toEqual(\n        new Delta()\n          .insert('01', { bold: true })\n          .insert('\\n\\n')\n          .insert('23', { bold: true })\n          .insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p><strong>01</strong></p>\n        <p><br></p>\n        <p><strong>23</strong></p>`);\n    });\n\n    test('text removing formatting', () => {\n      const editor = createEditor('<p><s>01</s></p>');\n      editor.insertText(2, '23', { bold: false, strike: false });\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('01', { strike: true }).insert('23\\n'),\n      );\n    });\n  });\n\n  describe('delete', () => {\n    test('inner node', () => {\n      const editor = createEditor('<p><strong><em>0123</em></strong></p>');\n      editor.deleteText(1, 2);\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('03', { bold: true, italic: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><strong><em>03</em></strong></p>',\n      );\n    });\n\n    test('parts of multiple lines', () => {\n      const editor = createEditor('<p><em>0123</em></p><p><em>5678</em></p>');\n      editor.deleteText(2, 5);\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('0178', { italic: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p><em>0178</em></p>');\n    });\n\n    test('entire line keeping newline', () => {\n      const editor = createEditor('<p><strong><em>0123</em></strong></p>');\n      editor.deleteText(0, 4);\n      expect(editor.getDelta()).toEqual(new Delta().insert('\\n'));\n      expect(editor.scroll.domNode).toEqualHTML('<p><br></p>');\n    });\n\n    test('newline', () => {\n      const editor = createEditor('<p><em>0123</em></p><p><em>5678</em></p>');\n      editor.deleteText(4, 1);\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('01235678', { italic: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p><em>01235678</em></p>');\n    });\n\n    test('entire document', () => {\n      const editor = createEditor('<p><strong><em>0123</em></strong></p>');\n      editor.deleteText(0, 5);\n      expect(editor.getDelta()).toEqual(new Delta().insert('\\n'));\n      expect(editor.scroll.domNode).toEqualHTML('<p><br></p>');\n    });\n\n    test('multiple complete lines', () => {\n      const editor = createEditor(\n        '<p><em>012</em></p><p><em>456</em></p><p><em>890</em></p>',\n      );\n      editor.deleteText(0, 8);\n      expect(editor.getDelta()).toEqual(\n        new Delta().insert('890', { italic: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p><em>890</em></p>');\n    });\n  });\n\n  describe('format', () => {\n    test('line', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.formatLine(1, 1, { header: 1 });\n      expect(editor.scroll.domNode).toEqualHTML('<h1>0123</h1>');\n    });\n  });\n\n  describe('removeFormat', () => {\n    test('unwrap', () => {\n      const editor = createEditor('<p>0<em>12</em>3</p>');\n      editor.removeFormat(1, 2);\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123</p>');\n    });\n\n    test('split inline', () => {\n      const editor = createEditor('<p>0<strong><em>12</em></strong>3</p>');\n      editor.removeFormat(1, 1);\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>01<strong><em>2</em></strong>3</p>',\n      );\n    });\n\n    test('partial line', () => {\n      const editor = createEditor(\n        '<h1>01</h1><ol><li data-list=\"ordered\">34</li></ol>',\n      );\n      editor.removeFormat(1, 3);\n      expect(editor.scroll.domNode).toEqualHTML('<p>01</p><p>34</p>');\n    });\n\n    test('remove embed', () => {\n      const editor = createEditor('<p>0<img src=\"/assets/favicon.png\">2</p>');\n      editor.removeFormat(1, 1);\n      expect(editor.scroll.domNode).toEqualHTML('<p>02</p>');\n    });\n\n    test('combined', () => {\n      const editor = createEditor(\n        `\n        <h1>01<img src=\"/assets/favicon.png\">3</h1>\n        <ol>\n          <li data-list=\"ordered\">5<strong>6<em>78</em>9</strong>0</li>\n        </ol>\n      `,\n      );\n      editor.removeFormat(1, 7);\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p>013</p>\n        <p>567<strong><em>8</em>9</strong>0</p>\n      `);\n    });\n\n    test('end of document', () => {\n      const editor = createEditor(\n        `\n        <ol>\n          <li data-list=\"ordered\">0123</li>\n          <li data-list=\"ordered\">5678</li>\n        </ol>\n      `,\n      );\n      editor.removeFormat(0, 12);\n      expect(editor.scroll.domNode).toEqualHTML(`\n        <p>0123</p>\n        <p>5678</p>\n      `);\n    });\n  });\n\n  describe('applyDelta', () => {\n    test('insert', () => {\n      const editor = createEditor('<p></p>');\n      editor.applyDelta(new Delta().insert('01'));\n      expect(editor.scroll.domNode).toEqualHTML('<p>01</p>');\n    });\n\n    test('attributed insert', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(new Delta().retain(2).insert('|', { bold: true }));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>01<strong>|</strong>23</p>',\n      );\n    });\n\n    test('format', () => {\n      const editor = createEditor('<p>01</p>');\n      editor.applyDelta(new Delta().retain(2, { bold: true }));\n      expect(editor.scroll.domNode).toEqualHTML('<p><strong>01</strong></p>');\n    });\n\n    test('discontinuous formats', () => {\n      const editor = createEditor('');\n      const delta = new Delta()\n        .insert('ab', { bold: true })\n        .insert('23\\n45')\n        .insert('cd', { bold: true });\n      editor.applyDelta(delta);\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><strong>ab</strong>23</p><p>45<strong>cd</strong></p>',\n      );\n    });\n\n    test('unformatted insert', () => {\n      const editor = createEditor('<p><em>01</em></p>');\n      editor.applyDelta(new Delta().retain(1).insert('|'));\n      expect(editor.scroll.domNode).toEqualHTML('<p><em>0</em>|<em>1</em></p>');\n    });\n\n    test('insert at format boundary', () => {\n      const editor = createEditor('<p><em>0</em><u>1</u></p>');\n      editor.applyDelta(new Delta().retain(1).insert('|', { strike: true }));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><em>0</em><s>|</s><u>1</u></p>',\n      );\n    });\n\n    test('unformatted newline', () => {\n      const editor = createEditor('<h1>01</h1>');\n      editor.applyDelta(new Delta().retain(2).insert('\\n'));\n      expect(editor.scroll.domNode).toEqualHTML('<p>01</p><h1><br></h1>');\n    });\n\n    test('formatted embed', () => {\n      const editor = createEditor('');\n      editor.applyDelta(\n        new Delta().insert({ image: '/assets/favicon.png' }, { italic: true }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><em><img src=\"/assets/favicon.png\"></em></p>',\n      );\n    });\n\n    test('insert text before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(new Delta().retain(5).insert('5678'));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p>5678</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n    });\n\n    test('insert attributed text before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(new Delta().retain(5).insert('5678', { bold: true }));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p><strong>5678</strong></p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n    });\n\n    test('insert text with newline before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(new Delta().retain(5).insert('5678\\n'));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p>5678</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n    });\n\n    test('insert formatted lines before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta().retain(5).insert('a\\nb').insert('\\n', { header: 1 }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p>a</p><h1>b</h1><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n    });\n\n    test('insert attributed text with newline before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta().retain(5).insert('5678', { bold: true }).insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p><strong>5678</strong></p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n    });\n\n    test('multiple inserts and deletes', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(\n        new Delta()\n          .retain(1)\n          .insert('a')\n          .delete(2)\n          .insert('cd')\n          .delete(1)\n          .insert('efg'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p>0acdefg</p>');\n    });\n\n    test('insert text with delete in existing block', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta()\n          .retain(4)\n          .insert('abc')\n          // Retain newline at end of block being inserted into.\n          .retain(1)\n          .delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123abc</p>');\n    });\n\n    test('insert text with delete before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta()\n          .retain(5)\n          // Explicit newline required to maintain correct index calculation for the delete.\n          .insert('abc\\n')\n          .delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123</p><p>abc</p>');\n    });\n\n    test('insert inline embed with delete in existing block', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta()\n          .retain(4)\n          .insert({ image: '/assets/favicon.png' })\n          // Retain newline at end of block being inserted into.\n          .retain(1)\n          .delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123<img src=\"/assets/favicon.png\"></p>',\n      );\n    });\n\n    test('insert inline embed with delete before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta()\n          .retain(5)\n          .insert({ image: '/assets/favicon.png' })\n          // Explicit newline required to maintain correct index calculation for the delete.\n          .insert('\\n')\n          .delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p><img src=\"/assets/favicon.png\"></p>',\n      );\n    });\n\n    test('insert inline embed with delete before block embed using delete op first', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta()\n          .retain(5)\n          .delete(1)\n          .insert({ image: '/assets/favicon.png' })\n          // Explicit newline required to maintain correct index calculation for the delete.\n          .insert('\\n'),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p><img src=\"/assets/favicon.png\"></p>',\n      );\n    });\n\n    test('insert inline embed and text with delete before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta()\n          .retain(5)\n          .insert({ image: '/assets/favicon.png' })\n          // Explicit newline required to maintain correct index calculation for the delete.\n          .insert('abc\\n')\n          .delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><p><img src=\"/assets/favicon.png\">abc</p>',\n      );\n    });\n\n    test('insert inline embed to the middle of formatted content', () => {\n      const editor = createEditor('<p><strong>0123</strong></p>');\n      editor.applyDelta(\n        new Delta().retain(2).insert({ image: '/assets/favicon.png' }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><strong>01</strong><img src=\"/assets/favicon.png\"><strong>23</strong></p>',\n      );\n    });\n\n    test('insert inline embed between plain text and formatted content', () => {\n      const editor = createEditor('<p>a<strong>b</strong></p>');\n      editor.applyDelta(new Delta().retain(1).insert({ image: '#' }));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>a<img src=\"#\"><strong>b</strong></p>',\n      );\n    });\n\n    test('prepend inline embed to another inline embed with same attributes', () => {\n      const editor = createEditor('<p><img src=\"#\" alt=\"hi\"/></p>');\n      editor.applyDelta(new Delta().insert({ image: '#' }, { alt: 'hi' }));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><img src=\"#\" alt=\"hi\"><img src=\"#\" alt=\"hi\"></p>',\n      );\n    });\n\n    test('insert block embed with delete before block embed', () => {\n      const editor = createEditor(\n        '<p>0123</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n      editor.applyDelta(\n        new Delta().retain(5).insert({ video: '#changed' }).delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><iframe src=\"#changed\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>',\n      );\n    });\n\n    test('deletes block embed and appends text', () => {\n      const editor = createEditor(\n        `<p><br></p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p>b</p>`,\n      );\n      editor.applyDelta(new Delta().retain(1).insert('a').delete(1));\n      expect(editor.scroll.domNode).toEqualHTML('<p><br></p><p>ab</p>');\n    });\n\n    test('multiple delete block embed and append texts', () => {\n      const editor = createEditor(\n        `<p><br></p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p>b</p>`,\n      );\n      editor.applyDelta(\n        new Delta().retain(1).insert('a').delete(1).insert('!').delete(1),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p><br></p><p>a!b</p>');\n    });\n\n    test('multiple nonconsecutive delete block embed and append texts', () => {\n      const editor = createEditor(\n        `<p><br></p>\n         <iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe>\n         <p>a</p>\n         <iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe>\n         <p>bb</p>\n         <iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe>\n         <p>ccc</p>\n         <iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe>\n         <p>dddd</p>`,\n      );\n      const old = editor.getDelta();\n      const delta = new Delta()\n        .retain(1)\n        .insert('1')\n        .delete(1)\n        .retain(2)\n        .insert('2')\n        .delete(1)\n        .retain(3)\n        .insert('3')\n        .delete(1)\n        .retain(4)\n        .insert('4')\n        .delete(1);\n      editor.applyDelta(delta);\n      expect(editor.getDelta()).toEqual(old.compose(delta));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><br></p><p>1a</p><p>2bb</p><p>3ccc</p><p>4dddd</p>',\n      );\n    });\n\n    describe('block embed', () => {\n      test('improper block embed insert', () => {\n        const editor = createEditor('<p>0123</p>');\n        editor.applyDelta(new Delta().retain(2).insert({ video: '#' }));\n        expect(editor.scroll.domNode).toEqualHTML(\n          '<p>01</p><iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe><p>23</p>',\n        );\n      });\n\n      describe('insert and delete', () => {\n        test('prepend', () => {\n          const editor = createEditor('<p>0123</p>');\n          editor.applyDelta(new Delta().insert({ video: '#' }).delete(2));\n          expect(editor.scroll.domNode).toEqualHTML(\n            '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe><p>23</p>',\n          );\n        });\n\n        test('insert to the middle of text', () => {\n          const editor = createEditor(`<p>abc</p>`);\n          editor.applyDelta(\n            new Delta().retain(1).insert({ video: '#' }).delete(2),\n          );\n          expect(editor.scroll.domNode).toEqualHTML(\n            '<p>a</p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p><br></p>',\n          );\n        });\n\n        test('insert after \\\\n', () => {\n          const editor = createEditor(`<p>a</p><p>cda</p>`);\n          editor.applyDelta(\n            new Delta().retain(2).insert({ video: '#' }).delete(2),\n          );\n          expect(editor.scroll.domNode).toEqualHTML(\n            '<p>a</p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p>a</p>',\n          );\n        });\n\n        test('insert after an inline embed', () => {\n          const editor = createEditor(\n            `<p><img src=\"/assets/favicon.png\"></p><p>abc</p>`,\n          );\n          editor.applyDelta(\n            new Delta().retain(1).insert({ video: '#' }).delete(2),\n          );\n          expect(editor.scroll.domNode).toEqualHTML(\n            '<p><img src=\"/assets/favicon.png\"></p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p>bc</p>',\n          );\n        });\n\n        test('insert after a block embed', () => {\n          const editor = createEditor(\n            `<iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p>abc</p>`,\n          );\n          editor.applyDelta(\n            new Delta().retain(1).insert({ video: '#' }).delete(2),\n          );\n          expect(editor.scroll.domNode).toEqualHTML(\n            '<iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"#\"></iframe><p>c</p>',\n          );\n        });\n      });\n\n      test('append formatted block embed', () => {\n        const editor = createEditor('<p>0123</p><p><br></p>');\n        editor.applyDelta(\n          new Delta().retain(5).insert({ video: '#' }, { align: 'right' }),\n        );\n        expect(editor.scroll.domNode).toEqualHTML(\n          '<p>0123</p><iframe src=\"#\" class=\"ql-video ql-align-right\" frameborder=\"0\" allowfullscreen=\"true\"></iframe><p><br></p>',\n        );\n      });\n    });\n\n    test('append', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(new Delta().retain(5).insert('5678'));\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123</p><p>5678</p>');\n    });\n\n    test('append newline', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(new Delta().retain(5).insert('\\n', { header: 2 }));\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123</p><h2><br></h2>');\n    });\n\n    test('append text with newline', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(\n        new Delta().retain(5).insert('5678').insert('\\n', { header: 2 }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123</p><h2>5678</h2>');\n    });\n\n    test('append non-isolated newline', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(new Delta().retain(5).insert('5678\\n', { header: 2 }));\n      expect(editor.scroll.domNode).toEqualHTML('<p>0123</p><h2>5678</h2>');\n    });\n\n    test('eventual append', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(\n        new Delta()\n          .retain(2)\n          .insert('ab\\n', { header: 1 })\n          .retain(3)\n          .insert('cd\\n', { header: 2 }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<h1>01ab</h1><p>23</p><h2>cd</h2>',\n      );\n    });\n\n    test('append text, embed and newline', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(\n        new Delta()\n          .retain(5)\n          .insert('5678')\n          .insert({ image: '/assets/favicon.png' })\n          .insert('\\n', { header: 2 }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><h2>5678<img src=\"/assets/favicon.png\"></h2>',\n      );\n    });\n\n    test('append multiple lines', () => {\n      const editor = createEditor('<p>0123</p>');\n      editor.applyDelta(\n        new Delta()\n          .retain(5)\n          .insert('56')\n          .insert('\\n', { header: 1 })\n          .insert('89')\n          .insert('\\n', { header: 2 }),\n      );\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p>0123</p><h1>56</h1><h2>89</h2>',\n      );\n    });\n\n    test('code block', () => {\n      const editor = createEditor(\n        '<p>0</p><div class=\"ql-code-block-container\"><div class=\"ql-code-block\">1</div><div class=\"ql-code-block\">23</div></div><p><br></p>',\n      );\n      editor.applyDelta(new Delta().delete(4).retain(1).delete(2));\n      expect(editor.scroll.domNode.innerHTML).toEqual('<p>2</p>');\n    });\n\n    test('prepending bold with a newline and unformatted text', () => {\n      const editor = createEditor('<p><strong>a</strong></p>');\n      editor.applyDelta(new Delta().insert('\\n1'));\n      expect(editor.scroll.domNode).toEqualHTML(\n        '<p><br></p><p>1<strong>a</strong></p>',\n      );\n    });\n  });\n\n  describe('insertContents', () => {\n    const video =\n      '<iframe src=\"#\" class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\"></iframe>';\n\n    test('ignores empty delta', () => {\n      const editor = createEditor('<p>1</p>');\n      editor.insertContents(0, new Delta());\n      expect(editor.getDelta().ops).toEqual([{ insert: '1\\n' }]);\n\n      editor.insertContents(0, new Delta().retain(100));\n      expect(editor.getDelta().ops).toEqual([{ insert: '1\\n' }]);\n    });\n\n    test('prepend to paragraph', () => {\n      const editor = createEditor('<p>2</p>');\n      editor.insertContents(0, new Delta().insert('1'));\n      expect(editor.getDelta().ops).toEqual([{ insert: '12\\n' }]);\n\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert('a', { bold: true })\n          .insert('\\n', { header: 1 })\n          .insert('b', { bold: true }),\n      );\n\n      expect(editor.getDelta().ops).toEqual([\n        { insert: 'a', attributes: { bold: true } },\n        { insert: '\\n', attributes: { header: 1 } },\n        { insert: 'b', attributes: { bold: true } },\n        { insert: '12\\n' },\n      ]);\n    });\n\n    test('prepend to list item', () => {\n      const editor = createEditor('<ol><li data-list=\"bullet\">2</li></ol>');\n      editor.insertContents(0, new Delta().insert('1'));\n      expect(editor.getDelta().ops).toEqual([\n        { insert: '12' },\n        { insert: '\\n', attributes: { list: 'bullet' } },\n      ]);\n\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert('a', { bold: true })\n          .insert('\\n', { header: 1 })\n          .insert('b', { bold: true }),\n      );\n\n      expect(editor.getDelta().ops).toEqual([\n        { insert: 'a', attributes: { bold: true } },\n        { insert: '\\n', attributes: { header: 1 } },\n        { insert: 'b', attributes: { bold: true } },\n        { insert: '12' },\n        { insert: '\\n', attributes: { list: 'bullet' } },\n      ]);\n    });\n\n    test('insert before formatting', () => {\n      class MyBlot extends Block {\n        static className = 'my-blot';\n        static blotName = 'my-blot';\n\n        formatAt(index: number, length: number, name: string, value: string) {\n          super.formatAt(index, length, name, value);\n          if (name === 'test-style' && !!this.prev) {\n            this.domNode.setAttribute('test-style', value);\n          }\n        }\n      }\n\n      const registry = new Registry();\n      registry.register(MyBlot, Block, Break, Text);\n      const editor = new Editor(\n        new Scroll(registry, document.createElement('div'), {\n          emitter: new Emitter(),\n        }),\n      );\n\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert('\\n')\n          .insert('hi')\n          .insert('\\n', { 'my-blot': true, 'test-style': 'random' }),\n      );\n      expect(editor.scroll.domNode.innerHTML).toContain('test-style=\"random\"');\n    });\n\n    describe('prepend to block embed', () => {\n      test('without ending with \\\\n', () => {\n        const editor = createEditor(`${video}`);\n        editor.insertContents(0, new Delta().insert('a'));\n        expect(editor.getDelta().ops).toEqual([\n          { insert: 'a\\n' },\n          { insert: { video: '#' } },\n        ]);\n      });\n\n      test('empty first line', () => {\n        const editor = createEditor(`<p></p>${video}`);\n        editor.insertContents(1, new Delta().insert('\\nworld\\n'));\n        expect(editor.getDelta().ops).toEqual([\n          { insert: '\\n\\nworld\\n' },\n          { insert: { video: '#' } },\n        ]);\n      });\n\n      test('multiple lines', () => {\n        const editor = createEditor(`${video}`);\n        editor.insertContents(\n          0,\n          new Delta().insert('a').insert('\\n', { header: 1 }),\n        );\n        expect(editor.getDelta().ops).toEqual([\n          { insert: 'a' },\n          { insert: '\\n', attributes: { header: 1 } },\n          { insert: { video: '#' } },\n        ]);\n      });\n    });\n\n    describe('append', () => {\n      test('appends to editor', () => {\n        const editor = createEditor('<p>1</p>');\n        editor.insertContents(2, new Delta().insert('a'));\n        expect(editor.getDelta().ops).toEqual([{ insert: '1\\na\\n' }]);\n        editor.insertContents(\n          4,\n          new Delta().insert('b').insert('\\n', { header: 1 }),\n        );\n        expect(editor.getDelta().ops).toEqual([\n          { insert: '1\\na\\nb' },\n          { insert: '\\n', attributes: { header: 1 } },\n        ]);\n      });\n\n      test('appends to paragraph', () => {\n        const editor = createEditor('<p>1</p><p>2</p>');\n        editor.insertContents(2, new Delta().insert('a'));\n        expect(editor.getDelta().ops).toEqual([{ insert: '1\\na2\\n' }]);\n        editor.insertContents(\n          2,\n          new Delta().insert('b').insert('\\n', { header: 1 }),\n        );\n        expect(editor.getDelta().ops).toEqual([\n          { insert: '1\\nb' },\n          { insert: '\\n', attributes: { header: 1 } },\n          { insert: 'a2\\n' },\n        ]);\n      });\n\n      test('appends to block embed', () => {\n        const editor = createEditor(`${video}<p>2</p>`);\n        editor.insertContents(1, new Delta().insert('1'));\n        expect(editor.getDelta().ops).toEqual([\n          { insert: { video: '#' } },\n          { insert: '12\\n' },\n        ]);\n        editor.insertContents(\n          1,\n          new Delta().insert('b').insert('\\n', { header: 1 }),\n        );\n        expect(editor.getDelta().ops).toEqual([\n          { insert: { video: '#' } },\n          { insert: 'b' },\n          { insert: '\\n', attributes: { header: 1 } },\n          { insert: '12\\n' },\n        ]);\n      });\n    });\n\n    test('prepends a formatted block embed', () => {\n      const editor = createEditor(`<p></p>`);\n      editor.insertContents(\n        0,\n        new Delta().insert({ video: '#' }, { width: '300' }),\n      );\n      expect(editor.getDelta().ops).toEqual([\n        { insert: { video: '#' }, attributes: { width: '300' } },\n        { insert: '\\n' },\n      ]);\n    });\n\n    test('prepends two formatted block embeds', () => {\n      const editor = createEditor(`<p></p>`);\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert({ video: '#' }, { width: '300' })\n          .insert({ video: '#' }, { width: '600' }),\n      );\n      expect(editor.getDelta().ops).toEqual([\n        { insert: { video: '#' }, attributes: { width: '300' } },\n        { insert: { video: '#' }, attributes: { width: '600' } },\n        { insert: '\\n' },\n      ]);\n    });\n\n    test('inserts formatted block embeds (styles)', () => {\n      const editor = createEditor(`<p></p>`);\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert('a\\n')\n          .insert({ video: '#' }, { width: '300' })\n          .insert({ video: '#' }, { width: '300' })\n          .insert('\\nd'),\n      );\n      expect(editor.getDelta().ops).toEqual([\n        { insert: 'a\\n' },\n        { insert: { video: '#' }, attributes: { width: '300' } },\n        { insert: { video: '#' }, attributes: { width: '300' } },\n        { insert: '\\nd\\n' },\n      ]);\n    });\n\n    test('inserts formatted block embeds (attributor)', () => {\n      const editor = createEditor(`<p></p>`);\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert('a\\n')\n          .insert({ video: '#' }, { align: 'center' })\n          .insert({ video: '#' }, { align: 'center' })\n          .insert('\\nd'),\n      );\n      expect(editor.getDelta().ops).toEqual([\n        { insert: 'a\\n' },\n        { insert: { video: '#' }, attributes: { align: 'center' } },\n        { insert: { video: '#' }, attributes: { align: 'center' } },\n        { insert: '\\nd\\n' },\n      ]);\n    });\n\n    test('inserts inline embeds to bold text', () => {\n      const editor = createEditor(`<p><strong>ab</strong></p>`);\n      editor.insertContents(1, new Delta().insert({ image: '#' }));\n      expect(editor.getDelta().ops).toEqual([\n        { insert: 'a', attributes: { bold: true } },\n        { insert: { image: '#' } },\n        { insert: 'b', attributes: { bold: true } },\n        { insert: '\\n' },\n      ]);\n    });\n\n    test('inserts multiple lines to a container', () => {\n      const editor = createEditor(`<ol><li data-list=\"ordered\"></li></ol>`);\n      editor.insertContents(\n        0,\n        new Delta()\n          .insert('world', { font: 'monospace' })\n          .insert('\\n', { list: 'bullet' })\n          .insert('\\n'),\n      );\n      expect(editor.getDelta().ops).toEqual([\n        { insert: 'world', attributes: { font: 'monospace' } },\n        { insert: '\\n', attributes: { list: 'bullet' } },\n        { insert: '\\n' },\n        { insert: '\\n', attributes: { list: 'ordered' } },\n      ]);\n    });\n\n    describe('invalid delta', () => {\n      const getEditorDelta = (modify: (editor: Editor) => void) => {\n        const editor = createEditor(`<p></p>`);\n        modify(editor);\n        return editor.getDelta().ops;\n      };\n\n      test('conflict block formats', () => {\n        const change = new Delta()\n          .insert('a')\n          .insert('\\n', { header: 1, list: 'bullet' })\n          .insert('b')\n          .insert('\\n', { header: 1, list: 'bullet' });\n\n        expect(\n          getEditorDelta((editor) => editor.insertContents(0, change)),\n        ).toEqual(getEditorDelta((editor) => editor.applyDelta(change)));\n      });\n\n      test('block embeds with line formats', () => {\n        const change = new Delta()\n          .insert('a\\n')\n          .insert({ video: '#' }, { header: 1 })\n          .insert({ video: '#' }, { header: 1 })\n          .insert('\\n', { header: 1 });\n\n        expect(\n          getEditorDelta((editor) => editor.insertContents(0, change)),\n        ).toEqual(getEditorDelta((editor) => editor.applyDelta(change)));\n      });\n\n      test('missing \\\\n before block embeds', () => {\n        const change = new Delta()\n          .insert('a')\n          .insert({ video: '#' })\n          .insert('b\\n');\n\n        expect(\n          getEditorDelta((editor) => editor.insertContents(0, change)),\n        ).toEqual(getEditorDelta((editor) => editor.applyDelta(change)));\n      });\n    });\n  });\n\n  describe('getFormat()', () => {\n    test('unformatted', () => {\n      const editor = createEditor('<p>0123</p>');\n      expect(editor.getFormat(1)).toEqual({});\n    });\n\n    test('formatted', () => {\n      const editor = createEditor('<h1><em>0123</em></h1>');\n      expect(editor.getFormat(1)).toEqual({ header: 1, italic: true });\n    });\n\n    test('cursor', () => {\n      const editor = createEditor(\n        '<h1><strong><em>0123</em></strong></h1><h2><u>5678</u></h2>',\n      );\n      expect(editor.getFormat(2)).toEqual({\n        bold: true,\n        italic: true,\n        header: 1,\n      });\n    });\n\n    test('cursor with preformat', () => {\n      const editor = createEditor('<h1><strong><em>0123</em></strong></h1>');\n      const quill = Quill.find(editor.scroll.domNode.parentElement!) as Quill;\n      quill.selection.setRange(new Range(2));\n      quill.selection.format('underline', true);\n      quill.selection.format('color', 'red');\n      expect(editor.getFormat(2)).toEqual({\n        bold: true,\n        italic: true,\n        header: 1,\n        color: 'red',\n        underline: true,\n      });\n    });\n\n    test('across leaves', () => {\n      const editor = createEditor(\n        `\n        <h1>\n          <strong class=\"ql-size-small\"><em>01</em></strong>\n          <em class=\"ql-size-large\"><u>23</u></em>\n          <em class=\"ql-size-huge\"><u>45</u></em>\n        </h1>\n      `,\n      );\n      expect(editor.getFormat(1, 4)).toEqual({\n        italic: true,\n        header: 1,\n        size: ['small', 'large', 'huge'],\n      });\n    });\n\n    test('across leaves repeated', () => {\n      const editor = createEditor(\n        `\n        <h1>\n          <strong class=\"ql-size-small\"><em>01</em></strong>\n          <em class=\"ql-size-large\"><u>23</u></em>\n          <em class=\"ql-size-huge\"><u>45</u></em>\n          <em class=\"ql-size-small\"><u>45</u></em>\n        </h1>\n      `,\n      );\n      expect(editor.getFormat(1, 4)).toEqual({\n        italic: true,\n        header: 1,\n        size: ['small', 'large', 'huge'],\n      });\n    });\n\n    test('across lines repeated', () => {\n      const editor = createEditor(\n        `\n        <h1 class=\"ql-align-right\"><em>01</em></h1>\n        <h1 class=\"ql-align-center\"><em>34</em></h1>\n        <h1 class=\"ql-align-right\"><em>36</em></h1>\n        <h1 class=\"ql-align-center\"><em>33</em></h1>\n      `,\n      );\n      expect(editor.getFormat(1, 3)).toEqual({\n        italic: true,\n        header: 1,\n        align: ['right', 'center'],\n      });\n    });\n    test('across lines', () => {\n      const editor = createEditor(\n        `\n        <h1 class=\"ql-align-right\"><em>01</em></h1>\n        <h1 class=\"ql-align-center\"><em>34</em></h1>\n      `,\n      );\n      expect(editor.getFormat(1, 3)).toEqual({\n        italic: true,\n        header: 1,\n        align: ['right', 'center'],\n      });\n    });\n  });\n\n  describe('getHTML', () => {\n    test('inline', () => {\n      expect(\n        createEditor('<blockquote>Test</blockquote>').getHTML(1, 2),\n      ).toEqual('es');\n\n      expect(\n        createEditor('<blockquote>Test</blockquote>').getHTML(0, 4),\n      ).toEqual('Test');\n    });\n\n    test('entire line', () => {\n      const editor = createEditor('<blockquote>Test</blockquote>');\n      expect(editor.getHTML(0, 5)).toEqual('<blockquote>Test</blockquote>');\n    });\n\n    test('entire list item', () => {\n      const editor = createEditor('<ul><li>Test</li></ul>');\n      expect(editor.getHTML(0, 5)).toEqual('<ul><li>Test</li></ul>');\n    });\n\n    test('across lines', () => {\n      const editor = createEditor(\n        '<h1 class=\"ql-align-center\">Header</h1><p>Text</p><blockquote>Quote</blockquote>',\n      );\n      expect(editor.getHTML(1, 14)).toEqual(\n        '<h1 class=\"ql-align-center\">eader</h1><p>Text</p><blockquote>Quo</blockquote>',\n      );\n    });\n\n    test('collapsible spaces', () => {\n      expect(\n        createEditor('<p><strong>123 </strong>123<em> 123</em></p>').getHTML(\n          0,\n          11,\n        ),\n      ).toEqual('<strong>123&nbsp;</strong>123<em>&nbsp;123</em>');\n\n      expect(createEditor(new Delta().insert('1   2\\n')).getHTML(0, 5)).toEqual(\n        '1&nbsp;&nbsp;&nbsp;2',\n      );\n\n      expect(\n        createEditor(\n          new Delta().insert('  123', { bold: true }).insert('\\n'),\n        ).getHTML(0, 5),\n      ).toEqual('<strong>&nbsp;&nbsp;123</strong>');\n    });\n\n    test('mixed list', () => {\n      const editor = createEditor(\n        `\n          <ol>\n            <li data-list=\"ordered\">One</li>\n            <li data-list=\"ordered\">Two</li>\n            <li data-list=\"bullet\">Foo</li>\n            <li data-list=\"bullet\">Bar</li>\n          </ol>\n        `,\n      );\n      expect(editor.getHTML(2, 12)).toEqualHTML(`\n        <ol>\n          <li>e</li>\n          <li>Two</li>\n        </ol>\n        <ul>\n          <li>Foo</li>\n          <li>Ba</li>\n        </ul>\n      `);\n    });\n\n    test('nested list', () => {\n      const editor = createEditor(\n        `\n          <ol>\n            <li data-list=\"ordered\">One</li>\n            <li data-list=\"ordered\">Two</li>\n            <li data-list=\"bullet\" class=\"ql-indent-1\">Alpha</li>\n            <li data-list=\"ordered\" class=\"ql-indent-2\">I</li>\n            <li data-list=\"ordered\" class=\"ql-indent-2\">II</li>\n            <li data-list=\"ordered\">Three</li>\n          </ol>\n        `,\n      );\n      expect(editor.getHTML(2, 20)).toEqualHTML(`\n        <ol>\n          <li>e</li>\n          <li>Two\n            <ul>\n              <li>Alpha\n                <ol>\n                  <li>I</li>\n                  <li>II</li>\n                </ol>\n              </li>\n            </ul>\n          </li>\n          <li>Thr</li>\n        </ol>\n      `);\n    });\n\n    test('nested checklist', () => {\n      const editor = createEditor(\n        `\n          <ol>\n            <li data-list=\"checked\">One</li>\n            <li data-list=\"checked\">Two</li>\n            <li data-list=\"unchecked\" class=\"ql-indent-1\">Alpha</li>\n            <li data-list=\"checked\" class=\"ql-indent-2\">I</li>\n            <li data-list=\"checked\" class=\"ql-indent-2\">II</li>\n            <li data-list=\"checked\">Three</li>\n          </ol>\n        `,\n      );\n      expect(editor.getHTML(2, 20)).toEqualHTML(`\n        <ul>\n          <li data-list=\"checked\">e</li>\n          <li data-list=\"checked\">Two\n            <ul>\n              <li data-list=\"unchecked\">Alpha\n                <ul>\n                  <li data-list=\"checked\">I</li>\n                  <li data-list=\"checked\">II</li>\n                </ul>\n              </li>\n            </ul>\n          </li>\n          <li data-list=\"checked\">Thr</li>\n        </ul>\n      `);\n    });\n\n    test('partial list', () => {\n      const editor = createEditor(\n        `\n        <ol>\n          <li data-list=\"ordered\">1111</li>\n          <li data-list=\"ordered\" class=\"ql-indent-1\">AAAA</li>\n          <li data-list=\"ordered\" class=\"ql-indent-2\">IIII</li>\n          <li data-list=\"ordered\" class=\"ql-indent-1\">BBBB</li>\n          <li data-list=\"ordered\">2222</li>\n        </ol>\n        `,\n      );\n      expect(editor.getHTML(12, 12)).toEqualHTML(`\n        <ol>\n          <li>\n            <ol>\n              <li>\n                <ol>\n                  <li>II</li>\n                </ol>\n              </li>\n              <li>BBBB</li>\n            </ol>\n          </li>\n          <li>2222</li>\n        </ol>\n      `);\n    });\n\n    test('text within tag', () => {\n      const editor = createEditor('<p><strong>a</strong></p>');\n      expect(editor.getHTML(0, 1)).toEqual('<strong>a</strong>');\n    });\n\n    test('escape html', () => {\n      const editor = createEditor('<p><br></p>');\n      editor.insertText(0, '<b>Test</b>');\n      expect(editor.getHTML(0, 11)).toEqual('&lt;b&gt;Test&lt;/b&gt;');\n    });\n\n    test('multiline code', () => {\n      const editor = createEditor(\n        '<p><br></p><p>0123</p><p><br></p><p><br></p><p>4567</p><p><br></p>',\n      );\n      const length = editor.scroll.length();\n      editor.formatLine(0, length, { 'code-block': 'javascript' });\n\n      expect(editor.getHTML(0, length)).toEqual(\n        '<pre>\\n\\n0123\\n\\n\\n4567\\n\\n</pre>',\n      );\n      expect(editor.getHTML(1, 7)).toEqual('<pre>\\n0123\\n\\n\\n\\n</pre>');\n      expect(editor.getHTML(2, 7)).toEqual('<pre>\\n123\\n\\n\\n4\\n</pre>');\n      expect(editor.getHTML(5, 7)).toEqual('<pre>\\n\\n\\n\\n4567\\n</pre>');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/core/emitter.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport Emitter from '../../../src/core/emitter.js';\nimport Quill from '../../../src/core.js';\n\ndescribe('emitter', () => {\n  test('emit and on', () => {\n    const emitter = new Emitter();\n\n    let received: unknown;\n    emitter.on('abc', (data) => {\n      received = data;\n    });\n    emitter.emit('abc', { hello: 'world' });\n\n    expect(received).toEqual({ hello: 'world' });\n  });\n\n  test('listenDOM', () => {\n    const quill = new Quill(document.createElement('div'));\n    document.body.appendChild(quill.container);\n\n    let calls = 0;\n    quill.emitter.listenDOM('click', document.body, () => {\n      calls += 1;\n    });\n\n    document.body.click();\n    expect(calls).toEqual(1);\n\n    quill.container.remove();\n    document.body.click();\n    expect(calls).toEqual(1);\n\n    document.body.appendChild(quill.container);\n    document.body.click();\n    expect(calls).toEqual(2);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/core/quill.spec.ts",
    "content": "import '../../../src/quill.js';\nimport Delta from 'quill-delta';\nimport { LeafBlot, Registry } from 'parchment';\nimport {\n  afterEach,\n  beforeEach,\n  describe,\n  expect,\n  test,\n  vitest,\n  vi,\n} from 'vitest';\nimport type { MockedFunction } from 'vitest';\nimport Emitter from '../../../src/core/emitter.js';\nimport Theme from '../../../src/core/theme.js';\nimport Toolbar from '../../../src/modules/toolbar.js';\nimport Quill, {\n  expandConfig,\n  globalRegistry,\n  overload,\n} from '../../../src/core/quill.js';\nimport { Range } from '../../../src/core/selection.js';\nimport Snow from '../../../src/themes/snow.js';\nimport { normalizeHTML } from '../__helpers__/utils.js';\n\nconst createContainer = (html: string | { html: string } = '') => {\n  const container = document.createElement('div');\n  container.innerHTML = normalizeHTML(html);\n  document.body.appendChild(container);\n  return container;\n};\n\ndescribe('Quill', () => {\n  test('imports', () => {\n    Object.keys(Quill.imports).forEach((path) => {\n      expect(Quill.import(path)).toBeTruthy();\n    });\n  });\n\n  describe('register', () => {\n    const imports = { ...Quill.imports };\n    afterEach(() => {\n      Quill.imports = imports;\n    });\n\n    test('register(path, target)', () => {\n      class Counter {}\n      Quill.register('modules/counter', Counter);\n\n      expect(Quill.imports).toHaveProperty('modules/counter', Counter);\n      expect(Quill.import('modules/counter')).toEqual(Counter);\n    });\n\n    test('register(formats)', () => {\n      class MyCounterBlot extends LeafBlot {\n        static blotName = 'my-counter';\n        static className = 'ql-my-counter';\n      }\n      Quill.register(MyCounterBlot);\n\n      expect(Quill.imports).toHaveProperty('formats/my-counter', MyCounterBlot);\n      expect(Quill.import('formats/my-counter')).toEqual(MyCounterBlot);\n    });\n\n    test('register(targets)', () => {\n      class ABlot extends LeafBlot {\n        static blotName = 'a-blot';\n        static className = 'ql-a-blot';\n      }\n      class AModule {}\n      Quill.register({\n        'formats/a-blot': ABlot,\n        'modules/a-module': AModule,\n      });\n\n      expect(Quill.import('formats/a-blot')).toEqual(ABlot);\n      expect(Quill.import('modules/a-module')).toEqual(AModule);\n    });\n  });\n\n  describe('construction', () => {\n    test('empty', () => {\n      const quill = new Quill(createContainer());\n      expect(quill.getContents()).toEqual(new Delta().insert('\\n'));\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p><br></p>\"');\n    });\n\n    test('text', () => {\n      const quill = new Quill(createContainer('0123'));\n      expect(quill.getContents()).toEqual(new Delta().insert('0123\\n'));\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p>0123</p>\"');\n    });\n\n    test('newlines', () => {\n      const quill = new Quill(\n        createContainer('<p><br></p><p><br></p><p><br></p>'),\n      );\n      expect(quill.getContents()).toEqual(new Delta().insert('\\n\\n\\n'));\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p><br></p><p><br></p><p><br></p>\"',\n      );\n    });\n\n    test('formatted ending', () => {\n      const quill = new Quill(\n        createContainer('<p class=\"ql-align-center\">Test</p>'),\n      );\n      expect(quill.getContents()).toEqual(\n        new Delta().insert('Test').insert('\\n', { align: 'center' }),\n      );\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p class=\"ql-align-center\">Test</p>\"',\n      );\n    });\n  });\n\n  describe('api', () => {\n    const setup = () => {\n      const quill = new Quill(createContainer('<p>0123<em>45</em>67</p>'));\n      const oldDelta = quill.getContents();\n      vitest.spyOn(quill.emitter, 'emit');\n      return { quill, oldDelta };\n    };\n\n    test('deleteText()', () => {\n      const { quill, oldDelta } = setup();\n      quill.deleteText(3, 2);\n      const change = new Delta().retain(3).delete(2);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>012<em>5</em>67</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        change,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    test('format()', () => {\n      const { quill, oldDelta } = setup();\n      quill.setSelection(3, 2);\n      quill.format('bold', true);\n      const change = new Delta().retain(3).retain(2, { bold: true });\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>012<strong>3<em>4</em></strong><em>5</em>67</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        change,\n        oldDelta,\n        Emitter.sources.API,\n      );\n      expect(quill.getSelection()).toEqual(new Range(3, 2));\n    });\n\n    test('formatLine()', () => {\n      const { quill, oldDelta } = setup();\n      quill.formatLine(1, 1, 'header', 2);\n      const change = new Delta().retain(8).retain(1, { header: 2 });\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<h2>0123<em>45</em>67</h2>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        change,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    describe('formatText()', () => {\n      test('single format', () => {\n        const { quill, oldDelta } = setup();\n        quill.formatText(3, 2, 'bold', true);\n        const change = new Delta().retain(3).retain(2, { bold: true });\n        expect(quill.root.innerHTML).toMatchInlineSnapshot(\n          '\"<p>012<strong>3<em>4</em></strong><em>5</em>67</p>\"',\n        );\n        expect(quill.emitter.emit).toHaveBeenCalledWith(\n          Emitter.events.TEXT_CHANGE,\n          change,\n          oldDelta,\n          Emitter.sources.API,\n        );\n      });\n\n      test('format object', () => {\n        const { quill, oldDelta } = setup();\n        quill.formatText(3, 2, { bold: true });\n        const change = new Delta().retain(3).retain(2, { bold: true });\n        expect(quill.root.innerHTML).toMatchInlineSnapshot(\n          '\"<p>012<strong>3<em>4</em></strong><em>5</em>67</p>\"',\n        );\n        expect(quill.emitter.emit).toHaveBeenCalledWith(\n          Emitter.events.TEXT_CHANGE,\n          change,\n          oldDelta,\n          Emitter.sources.API,\n        );\n      });\n    });\n\n    test('insertEmbed()', () => {\n      const { quill, oldDelta } = setup();\n      quill.insertEmbed(5, 'image', '/assets/favicon.png');\n      const change = new Delta()\n        .retain(5)\n        .insert({ image: '/assets/favicon.png' }, { italic: true });\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>0123<em>4<img src=\"/assets/favicon.png\">5</em>67</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        change,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    test('insertText()', () => {\n      const { quill, oldDelta } = setup();\n      quill.insertText(5, '|', 'bold', true);\n      const change = new Delta()\n        .retain(5)\n        .insert('|', { bold: true, italic: true });\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>0123<em>4</em><strong><em>|</em></strong><em>5</em>67</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        change,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    test('enable/disable', () => {\n      const { quill } = setup();\n      quill.disable();\n      expect(quill.root.getAttribute('contenteditable')).toEqual('false');\n      quill.enable();\n      expect(quill.root.getAttribute('contenteditable')).toBeTruthy();\n    });\n\n    test('getBounds() index', () => {\n      const { quill } = setup();\n      expect(quill.getBounds(1)).toBeTruthy();\n    });\n\n    test('getBounds() range', () => {\n      const { quill } = setup();\n      expect(quill.getBounds(new Range(3, 4))).toBeTruthy();\n    });\n\n    test('getFormat()', () => {\n      const { quill } = setup();\n      const formats = quill.getFormat(5);\n      expect(formats).toEqual({ italic: true });\n    });\n\n    test('getSelection()', () => {\n      const { quill } = setup();\n      expect(quill.getSelection()).toEqual(null);\n      const range = new Range(1, 2);\n      quill.setSelection(range);\n      expect(quill.getSelection()).toEqual(range);\n    });\n\n    test('removeFormat()', () => {\n      const { quill, oldDelta } = setup();\n      quill.removeFormat(5, 1);\n      const change = new Delta().retain(5).retain(1, { italic: null });\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>0123<em>4</em>567</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        change,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    test('updateContents() delta', () => {\n      const { quill, oldDelta } = setup();\n      const delta = new Delta().retain(5).insert('|');\n      quill.updateContents(delta);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>0123<em>4</em>|<em>5</em>67</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        delta,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    test('updateContents() ops array', () => {\n      const { quill, oldDelta } = setup();\n      const delta = new Delta().retain(5).insert('|');\n      quill.updateContents(delta.ops);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>0123<em>4</em>|<em>5</em>67</p>\"',\n      );\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        delta,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n  });\n\n  describe('events', () => {\n    const setup = () => {\n      const quill = new Quill(createContainer('<p>0123</p>'));\n      quill.update();\n      vitest.spyOn(quill.emitter, 'emit');\n      const oldDelta = quill.getContents();\n      return { quill, oldDelta };\n    };\n\n    test('api text insert', () => {\n      const { quill, oldDelta } = setup();\n      quill.insertText(2, '!');\n      const delta = new Delta().retain(2).insert('!');\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        delta,\n        oldDelta,\n        Emitter.sources.API,\n      );\n    });\n\n    test('user text insert', async () => {\n      const { quill, oldDelta } = setup();\n      (quill.root.firstChild?.firstChild as Text).data = '01!23';\n      const delta = new Delta().retain(2).insert('!');\n\n      await new Promise((r) => setTimeout(r, 1));\n      expect(quill.emitter.emit).toHaveBeenCalledWith(\n        Emitter.events.TEXT_CHANGE,\n        delta,\n        oldDelta,\n        Emitter.sources.USER,\n      );\n    });\n\n    const editTest = (\n      oldText: string,\n      oldSelection: number | Range,\n      newText: string,\n      newSelection: number | Range,\n      expectedDelta: Delta,\n    ) => {\n      return async () => {\n        const { quill } = setup();\n        quill.setText(`${oldText}\\n`);\n        // @ts-expect-error\n        quill.setSelection(oldSelection);\n        quill.update();\n        const oldContents = quill.getContents();\n        const textNode = quill.root.firstChild?.firstChild as Text;\n        textNode.data = newText;\n        if (typeof newSelection === 'number') {\n          quill.selection.setNativeRange(textNode, newSelection);\n        } else {\n          quill.selection.setNativeRange(\n            textNode,\n            newSelection.index,\n            textNode,\n            newSelection.index + newSelection.length,\n          );\n        }\n        await new Promise((r) => setTimeout(r, 1));\n        const calls = (\n          quill.emitter.emit as MockedFunction<typeof quill.emitter.emit>\n        ).mock.calls;\n        if (calls[calls.length - 1][1] === Emitter.events.SELECTION_CHANGE) {\n          calls.pop();\n        }\n        const args = calls.pop();\n        expect(args).toEqual([\n          Emitter.events.TEXT_CHANGE,\n          expectedDelta,\n          oldContents,\n          Emitter.sources.USER,\n        ]);\n      };\n    };\n\n    describe('insert a in aaaa', () => {\n      test(\n        'at index 0',\n        editTest('aaaa', 0, 'aaaaa', 1, new Delta().insert('a')),\n      );\n      test(\n        'at index 1',\n        editTest('aaaa', 1, 'aaaaa', 2, new Delta().retain(1).insert('a')),\n      );\n      test(\n        'at index 2',\n        editTest('aaaa', 2, 'aaaaa', 3, new Delta().retain(2).insert('a')),\n      );\n      test(\n        'at index 3',\n        editTest('aaaa', 3, 'aaaaa', 4, new Delta().retain(3).insert('a')),\n      );\n    });\n\n    describe('insert a in xaa', () => {\n      test(\n        'at index 1',\n        editTest('xaa', 1, 'xaaa', 2, new Delta().retain(1).insert('a')),\n      );\n      test(\n        'at index 2',\n        editTest('xaa', 2, 'xaaa', 3, new Delta().retain(2).insert('a')),\n      );\n      test(\n        'at index 3',\n        editTest('xaa', 3, 'xaaa', 4, new Delta().retain(3).insert('a')),\n      );\n    });\n\n    describe('insert aa in ax', () => {\n      test(\n        'at index 0',\n        editTest('ax', 0, 'aaax', 2, new Delta().insert('aa')),\n      );\n      test(\n        'at index 1',\n        editTest('ax', 1, 'aaax', 3, new Delta().retain(1).insert('aa')),\n      );\n    });\n\n    describe('delete a in xaa', () => {\n      test(\n        'at index 1',\n        editTest('xaa', 2, 'xa', 1, new Delta().retain(1).delete(1)),\n      );\n      test(\n        'at index 2',\n        editTest('xaa', 3, 'xa', 2, new Delta().retain(2).delete(1)),\n      );\n    });\n\n    describe('forward-delete a in xaa', () => {\n      test(\n        'at index 1',\n        editTest('xaa', 1, 'xa', 1, new Delta().retain(1).delete(1)),\n      );\n      test(\n        'at index 2',\n        editTest('xaa', 2, 'xa', 2, new Delta().retain(2).delete(1)),\n      );\n    });\n\n    test(\n      'replace yay with y',\n      editTest(\n        'yay',\n        new Range(0, 3),\n        'y',\n        1,\n        new Delta().insert('y').delete(3),\n      ),\n    );\n  });\n\n  describe('setContents()', () => {\n    test('empty', () => {\n      const quill = new Quill(createContainer(''));\n      const delta = new Delta().insert('\\n');\n      quill.setContents(delta);\n      expect(quill.getContents()).toEqual(delta);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p><br></p>\"');\n    });\n\n    test('single line', () => {\n      const quill = new Quill(createContainer(''));\n      const delta = new Delta().insert('Hello World!\\n');\n      quill.setContents(delta);\n      expect(quill.getContents()).toEqual(delta);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>Hello World!</p>\"',\n      );\n    });\n\n    test('multiple lines', () => {\n      const quill = new Quill(createContainer(''));\n      const delta = new Delta().insert('Hello\\n\\nWorld!\\n');\n      quill.setContents(delta);\n      expect(quill.getContents()).toEqual(delta);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p>Hello</p><p><br></p><p>World!</p>\"',\n      );\n    });\n\n    test('basic formats', () => {\n      const quill = new Quill(createContainer(''));\n      const delta = new Delta()\n        .insert('Welcome')\n        .insert('\\n', { header: 1 })\n        .insert('Hello\\n')\n        .insert('World')\n        .insert('!', { bold: true })\n        .insert('\\n');\n      quill.setContents(delta);\n      expect(quill.getContents()).toEqual(delta);\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<h1>Welcome</h1><p>Hello</p><p>World<strong>!</strong></p>\"',\n      );\n    });\n\n    test('array of operations', () => {\n      const quill = new Quill(createContainer(''));\n      const delta = new Delta()\n        .insert('test')\n        .insert('123', { bold: true })\n        .insert('\\n');\n      quill.setContents(delta.ops);\n      expect(quill.getContents()).toEqual(delta);\n    });\n\n    test('json', () => {\n      const quill = new Quill(createContainer(''));\n      const delta = new Delta().insert('test\\n');\n      quill.setContents(delta);\n      expect(quill.getContents()).toEqual(new Delta(delta));\n    });\n\n    test('no trailing newline', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      quill.setContents(new Delta().insert('0123'));\n      expect(quill.getContents()).toEqual(new Delta().insert('0123\\n'));\n    });\n\n    test('inline formatting', () => {\n      const quill = new Quill(\n        createContainer('<p><strong>Bold</strong></p><p>Not bold</p>'),\n      );\n      const contents = quill.getContents();\n      const delta = quill.setContents(contents);\n      expect(quill.getContents()).toEqual(contents);\n      expect(delta).toEqual(contents.delete(contents.length()));\n    });\n\n    test('block embed', () => {\n      const quill = new Quill(createContainer('<p>Hello World!</p>'));\n      const contents = new Delta().insert({ video: '#' });\n      quill.setContents(contents);\n      expect(quill.getContents()).toEqual(contents);\n    });\n  });\n\n  describe('getText()', () => {\n    test('return all text by default', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getText()).toMatchInlineSnapshot(`\n        \"Welcome\n        \"\n      `);\n    });\n\n    test('works when only provide index', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getText(2)).toMatchInlineSnapshot(`\n        \"lcome\n        \"\n      `);\n    });\n\n    test('works when provide index and length', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getText(2, 3)).toMatchInlineSnapshot(`\n        \"lco\"\n      `);\n    });\n\n    test('works with range', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getText({ index: 1, length: 2 })).toMatchInlineSnapshot(\n        '\"el\"',\n      );\n    });\n  });\n\n  describe('getSemanticHTML()', () => {\n    test('return all html by default', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getSemanticHTML()).toMatchInlineSnapshot(`\n        \"<h1>Welcome</h1>\"\n      `);\n    });\n\n    test('works when only provide index', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getSemanticHTML(2)).toMatchInlineSnapshot(`\n        \"lcome\"\n      `);\n    });\n\n    test('works when provide index and length', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getSemanticHTML(2, 3)).toMatchInlineSnapshot(`\n        \"lco\"\n      `);\n    });\n\n    test('works with range', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      expect(quill.getText({ index: 1, length: 2 })).toMatchInlineSnapshot(\n        '\"el\"',\n      );\n    });\n  });\n\n  describe('setText()', () => {\n    test('overwrite', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      quill.setText('abc');\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p>abc</p>\"');\n    });\n\n    test('set to newline', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      quill.setText('\\n');\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p><br></p>\"');\n    });\n\n    test('multiple newlines', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      quill.setText('\\n\\n');\n      expect(quill.root.innerHTML).toMatchInlineSnapshot(\n        '\"<p><br></p><p><br></p>\"',\n      );\n    });\n\n    test('content with trailing newline', () => {\n      const quill = new Quill(createContainer('<h1>Welcome</h1>'));\n      quill.setText('abc\\n');\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p>abc</p>\"');\n    });\n\n    test('return carriage', () => {\n      const quill = new Quill(createContainer('<p>Test</p>'));\n      quill.setText('\\r');\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p><br></p>\"');\n    });\n\n    test('return carriage newline', () => {\n      const quill = new Quill(createContainer('<p>Test</p>'));\n      quill.setText('\\r\\n');\n      expect(quill.root.innerHTML).toMatchInlineSnapshot('\"<p><br></p>\"');\n    });\n  });\n\n  describe('expandConfig', () => {\n    const testContainerId = 'testContainer';\n    beforeEach(() => {\n      const testContainer = document.createElement('div');\n      testContainer.id = testContainerId;\n      document.body.appendChild(testContainer);\n    });\n\n    test('user overwrite quill', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        placeholder: 'Test',\n        readOnly: true,\n      });\n      expect(config.placeholder).toEqual('Test');\n      expect(config.readOnly).toEqual(true);\n    });\n\n    test('convert css selectors', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        bounds: `#${testContainerId}`,\n      });\n      expect(config.bounds).toEqual(\n        document.querySelector(`#${testContainerId}`),\n      );\n      expect(config.container).toEqual(\n        document.querySelector(`#${testContainerId}`),\n      );\n    });\n\n    test('convert module true to {}', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          syntax: true,\n        },\n      });\n      expect(config.modules.syntax).toMatchObject({\n        interval: 1000,\n      });\n    });\n\n    describe('theme defaults', () => {\n      test('for Snow', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          modules: {\n            toolbar: true,\n          },\n          theme: 'snow',\n        });\n        expect(config.theme).toEqual(Snow);\n        // @ts-expect-error\n        expect(config.modules.toolbar.handlers.image).toEqual(\n          Snow.DEFAULTS.modules.toolbar?.handlers?.image,\n        );\n      });\n\n      test('for false', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          // @ts-expect-error\n          theme: false,\n        });\n        expect(config.theme).toEqual(Theme);\n      });\n\n      test('for undefined', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          theme: undefined,\n        });\n        expect(config.theme).toEqual(Theme);\n      });\n\n      test('for null', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          // @ts-expect-error\n          theme: null,\n        });\n        expect(config.theme).toEqual(Theme);\n      });\n    });\n\n    test('quill < module < theme < user', () => {\n      const oldTheme = Theme.DEFAULTS.modules;\n      const oldToolbar = Toolbar.DEFAULTS;\n      Toolbar.DEFAULTS = {\n        option: 2,\n        module: true,\n      };\n      Theme.DEFAULTS.modules = {\n        toolbar: {\n          option: 1,\n          theme: true,\n        },\n      };\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: {\n            option: 0,\n            user: true,\n          },\n        },\n      });\n      expect(config.modules.toolbar).toEqual({\n        option: 0,\n        module: true,\n        theme: true,\n        user: true,\n      });\n      Theme.DEFAULTS.modules = oldTheme;\n      Toolbar.DEFAULTS = oldToolbar;\n    });\n\n    test('toolbar default', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: true,\n        },\n      });\n      expect(config.modules.toolbar).toEqual(Toolbar.DEFAULTS);\n    });\n\n    test('toolbar disabled', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: false,\n        },\n        theme: 'snow',\n      });\n      expect(config.modules.toolbar).toBe(undefined);\n    });\n\n    test('toolbar selector', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: {\n            container: `#${testContainerId}`,\n          },\n        },\n      });\n      expect(config.modules.toolbar).toEqual({\n        container: `#${testContainerId}`,\n        handlers: Toolbar.DEFAULTS.handlers,\n      });\n    });\n\n    test('toolbar container shorthand', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: document.querySelector(`#${testContainerId}`),\n        },\n      });\n      expect(config.modules.toolbar).toEqual({\n        container: document.querySelector(`#${testContainerId}`),\n        handlers: Toolbar.DEFAULTS.handlers,\n      });\n    });\n\n    test('toolbar container shorthand with theme options', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: document.querySelector(`#${testContainerId}`),\n        },\n        theme: 'snow',\n      });\n      for (const [format, handler] of Object.entries(\n        Snow.DEFAULTS.modules.toolbar!.handlers ?? {},\n      )) {\n        // @ts-expect-error\n        expect(config.modules.toolbar.handlers[format]).toBe(handler);\n      }\n    });\n\n    test('toolbar format array', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: ['bold'],\n        },\n      });\n      expect(config.modules.toolbar).toEqual({\n        container: ['bold'],\n        handlers: Toolbar.DEFAULTS.handlers,\n      });\n    });\n\n    test('toolbar custom handler, default container', () => {\n      const handler = () => {}; // eslint-disable-line func-style\n      const config = expandConfig(`#${testContainerId}`, {\n        modules: {\n          toolbar: {\n            handlers: {\n              bold: handler,\n            },\n          },\n        },\n      });\n      // @ts-expect-error\n      expect(config.modules.toolbar.container).toEqual(null);\n      // @ts-expect-error\n      expect(config.modules.toolbar.handlers.bold).toEqual(handler);\n      // @ts-expect-error\n      expect(config.modules.toolbar.handlers.clean).toEqual(\n        // @ts-expect-error\n        Toolbar.DEFAULTS.handlers.clean,\n      );\n    });\n\n    test('registry defaults to globalRegistry', () => {\n      const config = expandConfig(`#${testContainerId}`, {});\n      expect(config.registry).toBe(globalRegistry);\n    });\n\n    test('registry with undefined values', () => {\n      const config = expandConfig(`#${testContainerId}`, {\n        registry: undefined,\n      });\n      expect(config.registry).toBe(globalRegistry);\n    });\n\n    describe('formats', () => {\n      test('null value allows all formats', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          formats: null,\n        });\n\n        expect(config.registry.query('cursor')).toBeTruthy();\n        expect(config.registry.query('bold')).toBeTruthy();\n      });\n\n      test('undefined value allows all formats', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          formats: undefined,\n        });\n\n        expect(config.registry.query('cursor')).toBeTruthy();\n        expect(config.registry.query('bold')).toBeTruthy();\n      });\n\n      test('always allows core formats', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          formats: ['bold'],\n        });\n\n        expect(config.registry.query('cursor')).toBeTruthy();\n        expect(config.registry.query('break')).toBeTruthy();\n      });\n\n      test('limits allowed formats', () => {\n        const config = expandConfig(`#${testContainerId}`, {\n          formats: ['bold'],\n        });\n\n        expect(config.registry.query('italic')).toBeFalsy();\n        expect(config.registry.query('bold')).toBeTruthy();\n      });\n\n      test('ignores unknown formats', () => {\n        const name = 'my-unregistered-format';\n        const config = expandConfig(`#${testContainerId}`, {\n          formats: [name],\n        });\n\n        expect(config.registry.query(name)).toBeFalsy();\n      });\n\n      test('registers list container when there is a list', () => {\n        expect(\n          expandConfig(`#${testContainerId}`, {\n            formats: ['bold'],\n          }).registry.query('list-container'),\n        ).toBeFalsy();\n\n        expect(\n          expandConfig(`#${testContainerId}`, {\n            formats: ['list'],\n          }).registry.query('list-container'),\n        ).toBeTruthy();\n      });\n\n      test('provides both registry and formats', () => {\n        const registry = new Registry();\n        const config = expandConfig(`#${testContainerId}`, {\n          registry,\n          formats: ['bold'],\n        });\n\n        expect(config.registry).toBe(registry);\n        expect(config.registry.query('bold')).toBeFalsy();\n      });\n    });\n  });\n\n  describe('overload', () => {\n    test('(index:number, length:number)', () => {\n      const [index, length, formats, source] = overload(0, 1);\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(index:number, length:number, format:string, value:boolean, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        0,\n        1,\n        'bold',\n        true,\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(index:number, length:number, format:string, value:string, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        0,\n        1,\n        'color',\n        Quill.sources.USER,\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ color: Quill.sources.USER });\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(index:number, length:number, format:string, value:string)', () => {\n      const [index, length, formats, source] = overload(\n        0,\n        1,\n        'color',\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ color: Quill.sources.USER });\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(index:number, length:number, format:object)', () => {\n      const [index, length, formats, source] = overload(0, 1, { bold: true });\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(index:number, length:number, format:object, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        0,\n        1,\n        { bold: true },\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(index:number, length:number, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        0,\n        1,\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(index:number, source:string)', () => {\n      const [index, length, formats, source] = overload(0, Quill.sources.USER);\n      expect(index).toBe(0);\n      expect(length).toBe(0);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(range:range)', () => {\n      const [index, length, formats, source] = overload(new Range(0, 1));\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(range:range, format:string, value:boolean, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        new Range(0, 1),\n        'bold',\n        true,\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(range:range, format:string, value:string, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        new Range(0, 1),\n        'color',\n        Quill.sources.API,\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ color: Quill.sources.API });\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(range:range, format:string, value:string)', () => {\n      const [index, length, formats, source] = overload(\n        new Range(0, 1),\n        'color',\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ color: Quill.sources.USER });\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(range:range, format:object)', () => {\n      const [index, length, formats, source] = overload(new Range(0, 1), {\n        bold: true,\n      });\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(range:range, format:object, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        new Range(0, 1),\n        { bold: true },\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(range:range, source:string)', () => {\n      const [index, length, formats, source] = overload(\n        new Range(0, 1),\n        Quill.sources.USER,\n      );\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.USER);\n    });\n\n    test('(range:range)', () => {\n      const [index, length, formats, source] = overload(new Range(0, 1));\n      expect(index).toBe(0);\n      expect(length).toBe(1);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(range:range, dummy:number)', () => {\n      // @ts-expect-error\n      const [index, length, formats, source] = overload(new Range(10, 1), 0);\n      expect(index).toBe(10);\n      expect(length).toBe(1);\n      expect(formats).toEqual({});\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(range:range, dummy:number, format:string, value:boolean)', () => {\n      // @ts-expect-error\n      const [index, length, formats, source] = overload(\n        new Range(10, 1),\n        0,\n        'bold',\n        true,\n      );\n      expect(index).toBe(10);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.API);\n    });\n\n    test('(range:range, dummy:number, format:object, source:string)', () => {\n      // @ts-expect-error\n      const [index, length, formats, source] = overload(\n        new Range(10, 1),\n        0,\n        { bold: true },\n        Quill.sources.USER,\n      );\n      expect(index).toBe(10);\n      expect(length).toBe(1);\n      expect(formats).toEqual({ bold: true });\n      expect(source).toBe(Quill.sources.USER);\n    });\n  });\n\n  describe('placeholder', () => {\n    const setup = () => {\n      const container = createContainer('<p></p>');\n      const quill = new Quill(container, {\n        placeholder: 'a great day to be a placeholder',\n      });\n      return { quill };\n    };\n\n    test('blank editor', () => {\n      const { quill } = setup();\n      expect(quill.root.dataset.placeholder).toEqual(\n        'a great day to be a placeholder',\n      );\n      expect([...quill.root.classList]).toContain('ql-blank');\n    });\n\n    test('with text', () => {\n      const { quill } = setup();\n      quill.setText('test');\n      expect([...quill.root.classList]).not.toContain('ql-blank');\n    });\n\n    test('formatted line', () => {\n      const { quill } = setup();\n      quill.formatLine(0, 1, 'list', 'ordered');\n      expect([...quill.root.classList]).not.toContain('ql-blank');\n    });\n  });\n\n  describe('scrollSelectionIntoView', () => {\n    const createContents = (separator: string) =>\n      new Array(200)\n        .fill(0)\n        .map((_, i) => `text ${i + 1}`)\n        .join(separator);\n\n    const viewportRatio = (element: Element): Promise<number> => {\n      return new Promise((resolve) => {\n        const observer = new IntersectionObserver((entries) => {\n          resolve(entries[0].intersectionRatio);\n          observer.disconnect();\n        });\n        observer.observe(element);\n        // Firefox doesn't call IntersectionObserver callback unless\n        // there are rafs.\n        requestAnimationFrame(() => {});\n      });\n    };\n\n    test('scroll upward', async () => {\n      document.body.style.height = '500px';\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n\n      Object.assign(container.style, {\n        height: '100px',\n        overflow: 'scroll',\n      });\n\n      const editorContainer = container.appendChild(\n        document.createElement('div'),\n      );\n      Object.assign(editorContainer.style, {\n        height: '100px',\n        overflow: 'scroll',\n        border: '10px solid red',\n      });\n\n      const space = container.appendChild(document.createElement('div'));\n      space.style.height = '800px';\n\n      const quill = new Quill(editorContainer);\n\n      const text = createContents('\\n');\n      quill.setContents(new Delta().insert(text));\n      quill.setSelection({ index: text.indexOf('text 10'), length: 4 }, 'user');\n\n      container.scrollTop = -500;\n\n      expect(\n        await viewportRatio(\n          editorContainer.querySelector('p:nth-child(10)') as HTMLElement,\n        ),\n      ).toBeGreaterThan(0.9);\n      expect(\n        await viewportRatio(\n          editorContainer.querySelector('p:nth-child(11)') as HTMLElement,\n        ),\n      ).toEqual(0);\n    });\n\n    test('scroll downward', async () => {\n      document.body.style.height = '500px';\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n\n      Object.assign(container.style, {\n        height: '100px',\n        overflow: 'scroll',\n      });\n\n      const space = container.appendChild(document.createElement('div'));\n      space.style.height = '80px';\n\n      const editorContainer = container.appendChild(\n        document.createElement('div'),\n      );\n      Object.assign(editorContainer.style, {\n        height: '100px',\n        overflow: 'scroll',\n        border: '10px solid red',\n      });\n\n      const quill = new Quill(editorContainer);\n\n      const text = createContents('\\n');\n      quill.setContents(new Delta().insert(text));\n      quill.setSelection(\n        { index: text.indexOf('text 100'), length: 4 },\n        'user',\n      );\n\n      expect(\n        await viewportRatio(\n          editorContainer.querySelector('p:nth-child(100)') as HTMLElement,\n        ),\n      ).toBeGreaterThan(0.9);\n      expect(\n        await viewportRatio(\n          editorContainer.querySelector('p:nth-child(101)') as HTMLElement,\n        ),\n      ).toEqual(0);\n    });\n\n    test('scroll-padding', async () => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n      const quill = new Quill(container);\n      Object.assign(quill.root.style, {\n        scrollPaddingBottom: '50px',\n        height: '200px',\n        overflow: 'auto',\n      });\n      const text = createContents('\\n');\n      quill.setContents(new Delta().insert(text));\n      quill.setSelection({ index: text.indexOf('text 10'), length: 4 }, 'user');\n      expect(\n        await viewportRatio(\n          container.querySelector('p:nth-child(10)') as HTMLElement,\n        ),\n      ).toBeGreaterThan(0.9);\n      expect(\n        await viewportRatio(\n          container.querySelector('p:nth-child(11)') as HTMLElement,\n        ),\n      ).toBeGreaterThan(0.9);\n      quill.root.style.scrollPaddingBottom = '0';\n      quill.setSelection(1, 'user');\n      quill.setSelection({ index: text.indexOf('text 10'), length: 4 }, 'user');\n      expect(\n        await viewportRatio(\n          container.querySelector('p:nth-child(11)') as HTMLElement,\n        ),\n      ).toBe(0);\n    });\n\n    test('inline scroll', async () => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n\n      Object.assign(container.style, {\n        width: '200px',\n        display: 'flex',\n        overflow: 'scroll',\n      });\n\n      const space = container.appendChild(document.createElement('div'));\n      space.style.width = '80px';\n\n      const editorContainer = container.appendChild(\n        document.createElement('div'),\n      );\n      Object.assign(editorContainer.style, {\n        width: '100px',\n        overflow: 'scroll',\n        border: '10px solid red',\n      });\n\n      const quill = new Quill(editorContainer);\n\n      Object.assign(quill.root.style, {\n        overflow: 'scroll',\n        whiteSpace: 'nowrap',\n      });\n\n      const text = createContents(' ');\n      const text100Index = text.indexOf('text 100');\n      const delta = new Delta()\n        .insert(text)\n        .compose(new Delta().retain(text100Index).retain(8, { bold: true }));\n      quill.setContents(delta);\n      quill.setSelection({ index: text100Index, length: 8 }, 'user');\n\n      expect(\n        await viewportRatio(\n          editorContainer.querySelector('strong') as HTMLElement,\n        ),\n      ).toBeGreaterThan(0.9);\n\n      quill.setSelection(0, 'user');\n      expect(\n        await viewportRatio(\n          editorContainer.querySelector('strong') as HTMLElement,\n        ),\n      ).toEqual(0);\n    });\n\n    test('scroll smoothly', async () => {\n      document.body.style.height = '500px';\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n\n      Object.assign(container.style, {\n        height: '100px',\n        overflow: 'scroll',\n      });\n\n      const space = container.appendChild(document.createElement('div'));\n      space.style.height = '80px';\n\n      const editorContainer = container.appendChild(\n        document.createElement('div'),\n      );\n      Object.assign(editorContainer.style, {\n        height: '100px',\n        overflow: 'scroll',\n        border: '10px solid red',\n      });\n\n      const quill = new Quill(editorContainer);\n\n      const text = createContents('\\n');\n      quill.setContents(new Delta().insert(text));\n      quill.setSelection(\n        { index: text.indexOf('text 100'), length: 4 },\n        'silent',\n      );\n      quill.scrollSelectionIntoView({ smooth: true });\n\n      await vi.waitFor(async () => {\n        expect(\n          await viewportRatio(\n            editorContainer.querySelector('p:nth-child(100)') as HTMLElement,\n          ),\n        ).toBeGreaterThan(0.9);\n        expect(\n          await viewportRatio(\n            editorContainer.querySelector('p:nth-child(101)') as HTMLElement,\n          ),\n        ).toEqual(0);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/core/selection.spec.ts",
    "content": "import Selection, { Range } from '../../../src/core/selection.js';\nimport Cursor from '../../../src/blots/cursor.js';\nimport Emitter from '../../../src/core/emitter.js';\nimport { expect, describe, test } from 'vitest';\nimport { createRegistry, createScroll } from '../__helpers__/factory.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Underline from '../../../src/formats/underline.js';\nimport Image from '../../../src/formats/image.js';\nimport Link from '../../../src/formats/link.js';\nimport Italic from '../../../src/formats/italic.js';\nimport Strike from '../../../src/formats/strike.js';\nimport { ColorStyle } from '../../../src/formats/color.js';\nimport { BackgroundStyle } from '../../../src/formats/background.js';\nimport { SizeClass } from '../../../src/formats/size.js';\n\nconst createSelection = (html: string, container = document.body) => {\n  const scroll = createScroll(\n    html,\n    createRegistry([\n      Bold,\n      Underline,\n      Italic,\n      Strike,\n      Image,\n      Link,\n      ColorStyle,\n      BackgroundStyle,\n      SizeClass,\n    ]),\n    container,\n  );\n  return new Selection(scroll, scroll.emitter);\n};\n\ndescribe('Selection', () => {\n  describe('focus()', () => {\n    const setupTest = () => {\n      const container = document.createElement('div');\n      const textarea = container.appendChild(\n        document.createElement('textarea'),\n      );\n      const selection = createSelection('<p>0123</p>', container);\n\n      document.body.appendChild(container);\n      textarea.focus();\n      textarea.select();\n      return { selection, textarea };\n    };\n\n    test('initial focus', () => {\n      const { selection } = setupTest();\n      expect(selection.hasFocus()).toBe(false);\n      selection.focus();\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('restore last range', () => {\n      const { selection, textarea } = setupTest();\n      const range = new Range(1, 2);\n      selection.setRange(range);\n      textarea.focus();\n      textarea.select();\n      expect(selection.hasFocus()).toBe(false);\n      selection.focus();\n      expect(selection.hasFocus()).toBe(true);\n      expect(selection.getRange()[0]).toEqual(range);\n    });\n  });\n\n  describe('getRange()', () => {\n    test('empty document', () => {\n      const selection = createSelection('');\n      selection.setNativeRange(selection.root.querySelector('br'), 0);\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(0);\n      expect(range?.length).toEqual(0);\n    });\n\n    test('empty line', () => {\n      const selection = createSelection('<p>0</p><p><br></p><p>3</p>');\n      selection.setNativeRange(selection.root.querySelector('br'), 0);\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(2);\n      expect(range?.length).toEqual(0);\n    });\n\n    test('end of line', () => {\n      const selection = createSelection('<p>0</p>');\n      selection.setNativeRange(\n        selection.root.firstChild?.firstChild as Node,\n        1,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(1);\n      expect(range?.length).toEqual(0);\n    });\n\n    test('text node', () => {\n      const selection = createSelection('<p>0123</p>');\n      selection.setNativeRange(\n        selection.root.firstChild?.firstChild as Node,\n        1,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(1);\n      expect(range?.length).toEqual(0);\n    });\n\n    test('line boundaries', () => {\n      const selection = createSelection('<p><br></p><p>12</p>');\n      selection.setNativeRange(\n        selection.root.firstChild,\n        0,\n        selection.root.lastChild?.lastChild as Node,\n        2,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(0);\n      expect(range?.length).toEqual(3);\n    });\n\n    test('nested text node', () => {\n      const selection = createSelection(\n        `<p><strong><em>01</em></strong></p>\n        <ol>\n          <li data-list=\"bullet\"><em><u>34</u></em></li>\n        </ol>`,\n      );\n      selection.setNativeRange(\n        selection.root.querySelector('em')?.firstChild as Node,\n        1,\n        selection.root.querySelector('u')?.firstChild as Node,\n        1,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(1);\n      expect(range?.length).toEqual(3);\n    });\n\n    test('between embed across lines', () => {\n      const selection = createSelection(\n        `\n        <p>\n          <img src=\"/assets/favicon.png\">\n          <img src=\"/assets/favicon.png\">\n        </p>\n        <p>\n          <img src=\"/assets/favicon.png\">\n          <img src=\"/assets/favicon.png\">\n        </p>`,\n      );\n      selection.setNativeRange(\n        selection.root.firstChild,\n        1,\n        selection.root.lastChild,\n        1,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(1);\n      expect(range?.length).toEqual(3);\n    });\n\n    test('between embed across list', () => {\n      const selection = createSelection(\n        `\n        <p>\n          <img src=\"/assets/favicon.png\">\n          <img src=\"/assets/favicon.png\">\n        </p>\n        <ol>\n          <li data-list=\"bullet\">\n            <img src=\"/assets/favicon.png\">\n            <img src=\"/assets/favicon.png\">\n          </li>\n        </ol>`,\n      );\n      selection.setNativeRange(\n        selection.root.firstChild,\n        1,\n        selection.root.lastChild?.firstChild,\n        2,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(1);\n      expect(range?.length).toEqual(3);\n    });\n\n    test('between inlines', () => {\n      const selection = createSelection('<p><em>01</em><s>23</s><u>45</u></p>');\n      selection.setNativeRange(\n        selection.root.firstChild,\n        1,\n        selection.root.firstChild,\n        2,\n      );\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(2);\n      expect(range?.length).toEqual(2);\n    });\n\n    test('between blocks', () => {\n      const selection = createSelection(\n        `\n        <p>01</p>\n        <p><br></p>\n        <ol>\n          <li data-list=\"bullet\">45</li>\n          <li data-list=\"bullet\">78</li>\n        </ol>`,\n      );\n      selection.setNativeRange(selection.root, 1, selection.root.lastChild, 1);\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(3);\n      expect(range?.length).toEqual(4);\n    });\n\n    test('wrong input', () => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n      const textarea = container.appendChild(\n        document.createElement('textarea'),\n      );\n      const selection = createSelection('<p>0123</p>', container);\n      textarea.select();\n      const [range] = selection.getRange();\n      expect(range).toEqual(null);\n    });\n  });\n\n  describe('setRange()', () => {\n    test('empty document', () => {\n      const selection = createSelection('');\n      const expected = new Range(0);\n      selection.setRange(expected);\n      const [range] = selection.getRange();\n      expect(range).toEqual(expected);\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('empty lines', () => {\n      const selection = createSelection(\n        `\n        <p><br></p>\n        <ol>\n          <li data-list=\"bullet\"><br></li>\n        </ol>`,\n      );\n      const expected = new Range(0, 1);\n      selection.setRange(expected);\n      const [range] = selection.getRange();\n      expect(range).toEqual(range);\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('nested text node', () => {\n      const selection = createSelection(\n        `\n        <p><strong><em>01</em></strong></p>\n        <ol>\n          <li data-list=\"bullet\"><em><u>34</u></em></li>\n        </ol>`,\n      );\n      const expected = new Range(1, 3);\n      selection.setRange(expected);\n      const [range] = selection.getRange();\n      expect(range).toEqual(expected);\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('between inlines', () => {\n      const selection = createSelection('<p><em>01</em><s>23</s><u>45</u></p>');\n      const expected = new Range(2, 2);\n      selection.setRange(expected);\n      const [range] = selection.getRange();\n      expect(range).toEqual(expected);\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('single embed', () => {\n      const selection = createSelection(\n        `<p><img src=\"/assets/favicon.png\"></p>`,\n      );\n      const expected = new Range(1, 0);\n      selection.setRange(expected);\n      const [range] = selection.getRange();\n      expect(range).toEqual(expected);\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('between embeds', () => {\n      const selection = createSelection(\n        `\n        <p>\n          <img src=\"/assets/favicon.png\">\n          <img src=\"/assets/favicon.png\">\n        </p>\n        <ol>\n          <li data-list=\"bullet\">\n            <img src=\"/assets/favicon.png\">\n            <img src=\"/assets/favicon.png\">\n          </li>\n        </ol>`,\n      );\n      const expected = new Range(1, 3);\n      selection.setRange(expected);\n      const [range] = selection.getRange();\n      expect(range).toEqual(expected);\n      expect(selection.hasFocus()).toBe(true);\n    });\n\n    test('null', () => {\n      const selection = createSelection('<p>0123</p>');\n      selection.setRange(new Range(1));\n      let [range] = selection.getRange();\n      expect(range).not.toEqual(null);\n      selection.setRange(null);\n      [range] = selection.getRange();\n      expect(range).toEqual(null);\n      expect(selection.hasFocus()).toBe(false);\n    });\n\n    test('after format', async () => {\n      const selection = createSelection('<p>0123 567 9012</p>');\n      selection.setRange(new Range(5));\n      selection.format('bold', true);\n      selection.format('bold', false);\n      selection.setRange(new Range(8));\n\n      await new Promise<void>((resolve) => {\n        selection.emitter.once(Emitter.events.SCROLL_OPTIMIZE, () => {\n          resolve();\n        });\n      });\n      const [range] = selection.getRange();\n      expect(range?.index).toEqual(8);\n    });\n  });\n\n  describe('format()', () => {\n    test('trailing', () => {\n      const selection = createSelection(`<p>0123</p>`);\n      selection.setRange(new Range(4));\n      selection.format('bold', true);\n      expect(selection.getRange()[0]?.index).toEqual(4);\n      expect(selection.root).toEqualHTML(`\n        <p>0123<strong><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></strong></p>\n      `);\n    });\n\n    test('split nodes', () => {\n      const selection = createSelection(`<p><em>0123</em></p>`);\n      selection.setRange(new Range(2));\n\n      selection.format('bold', true);\n      expect(selection.getRange()[0]?.index).toEqual(2);\n      expect(selection.root).toEqualHTML(`\n      <p><em>01</em><strong><em><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></em></strong><em>23</em></p>\n      `);\n    });\n\n    test('between characters', () => {\n      const selection = createSelection(`<p><em>0</em><strong>1</strong></p>`);\n      selection.setRange(new Range(1));\n      selection.format('underline', true);\n      expect(selection.getRange()[0]?.index).toEqual(1);\n      expect(selection.root).toEqualHTML(`\n        <p><em>0<u><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></u></em><strong>1</strong></p>\n      `);\n    });\n\n    test('empty line', () => {\n      const selection = createSelection(`<p><br></p>`);\n      selection.setRange(new Range(0));\n      selection.format('bold', true);\n      expect(selection.getRange()[0]?.index).toEqual(0);\n      expect(selection.root).toEqualHTML(`\n        <p><strong><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></strong></p>\n      `);\n    });\n\n    test('cursor interference', () => {\n      const selection = createSelection(`<p>0123</p>`);\n      selection.setRange(new Range(2));\n      selection.format('underline', true);\n      selection.scroll.update();\n      const native = selection.getNativeRange();\n      expect(native?.start.node).toEqual(selection.cursor.textNode);\n    });\n\n    test('multiple', () => {\n      const selection = createSelection(`<p>0123</p>`);\n      selection.setRange(new Range(2));\n      selection.format('color', 'red');\n      selection.format('italic', true);\n      selection.format('underline', true);\n      selection.format('background', 'blue');\n      expect(selection.getRange()[0]?.index).toEqual(2);\n      expect(selection.root).toEqualHTML(`\n        <p>01<em style=\"color: red; background-color: blue;\"><u><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></u></em>23</p>\n      `);\n    });\n\n    test('remove format', () => {\n      const selection = createSelection(`<p><strong>0123</strong></p>`);\n      selection.setRange(new Range(2));\n      selection.format('italic', true);\n      selection.format('underline', true);\n      selection.format('italic', false);\n      expect(selection.getRange()[0]?.index).toEqual(2);\n      expect(selection.root).toEqualHTML(`\n        <p><strong>01<u><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></u>23</strong></p>\n      `);\n    });\n\n    test('selection change cleanup', () => {\n      const selection = createSelection(`<p>0123</p>`);\n      selection.setRange(new Range(2));\n      selection.format('italic', true);\n      selection.setRange(new Range(0, 0));\n      selection.scroll.update();\n      expect(selection.root).toEqualHTML('<p>0123</p>');\n    });\n\n    test('text change cleanup', () => {\n      const selection = createSelection(`<p>0123</p>`);\n      selection.setRange(new Range(2));\n      selection.format('italic', true);\n      selection.cursor.textNode.data = `${Cursor.CONTENTS}|`;\n      selection.setNativeRange(selection.cursor.textNode, 2);\n      selection.scroll.update();\n      expect(selection.root).toEqualHTML('<p>01<em>|</em>23</p>');\n    });\n\n    test('no cleanup', () => {\n      const selection = createSelection('<p>0123</p><p><br></p>');\n      selection.setRange(new Range(2));\n      selection.format('italic', true);\n      selection.root.removeChild(selection.root.lastChild as Node);\n      selection.scroll.update();\n      expect(selection.getRange()[0]?.index).toEqual(2);\n      expect(selection.root).toEqualHTML(`\n        <p>01<em><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></em>23</p>\n      `);\n    });\n\n    describe('unlink cursor', () => {\n      test('one level', () => {\n        const selection = createSelection(\n          '<p><strong><a href=\"https://example.com\">link</a></strong></p><p><br></p>',\n        );\n        selection.setRange(new Range(4));\n        selection.format('bold', false);\n        expect(selection.root).toEqualHTML(`\n          <p><strong><a href=\"https://example.com\">link</a></strong><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></p>\n          <p><br /></p>\n        `);\n      });\n\n      test('nested formats', () => {\n        const selection = createSelection(\n          '<p><strong><em><a href=\"https://example.com\">bold</a></em></strong></p><p><br></p>',\n        );\n        selection.setRange(new Range(4));\n        selection.format('italic', false);\n        expect(selection.root).toEqualHTML(`\n          <p><strong><em><a href=\"https://example.com\">bold</a></em><span class=\"ql-cursor\">${Cursor.CONTENTS}</span></strong></p>\n          <p><br /></p>\n        `);\n      });\n\n      test('ignore link format', () => {\n        const selection = createSelection(\n          '<p><strong>bold</strong></p><p><br></p>',\n        );\n        selection.setRange(new Range(4));\n        selection.format('link', 'https://example.com');\n        expect(selection.root).toEqualHTML(`\n          <p><strong>bold<span class=\"ql-cursor\">${Cursor.CONTENTS}</span></strong></p>\n          <p><br /></p>\n        `);\n      });\n    });\n  });\n\n  describe('getBounds()', () => {\n    const setup = () => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n      container.classList.add('ql-editor');\n      const style = document.body.appendChild(document.createElement('style'));\n      style.innerText =\n        '.ql-editor p, .ql-editor img { margin: 0; padding: 0; border: 0; }';\n      container.style.fontFamily = 'monospace';\n      container.style.lineHeight = '18px';\n      const div = container.appendChild(document.createElement('div'));\n      div.style.border = '1px solid #777';\n      div.innerHTML = '<p><span>0</span></p>';\n      const span = div.firstChild?.firstChild as HTMLSpanElement;\n      const bounds = span.getBoundingClientRect();\n      const reference = {\n        height: bounds.height,\n        left: bounds.left,\n        lineHeight: span.parentElement?.offsetHeight as number,\n        width: bounds.width,\n        top: bounds.top,\n      };\n      div.remove();\n\n      return { reference, container };\n    };\n\n    test('empty document', () => {\n      const { reference, container } = setup();\n      const selection = createSelection('<p><br></p>', container);\n      const bounds = selection.getBounds(0);\n      expect(bounds?.left).approximately(reference.left, 3);\n      expect(bounds?.height).approximately(reference.height, 3);\n      expect(bounds?.top).approximately(reference.top, 3);\n    });\n\n    test('empty line', () => {\n      const { reference, container } = setup();\n      const selection = createSelection(\n        `\n        <p>0000</p>\n        <p><br></p>\n        <p>0000</p>`,\n        container,\n      );\n      const bounds = selection.getBounds(5);\n      expect(bounds?.left).approximately(reference.left, 3);\n      expect(bounds?.height).approximately(reference.height, 3);\n      expect(bounds?.top).approximately(\n        reference.top + reference.lineHeight,\n        3,\n      );\n    });\n\n    test('plain text', () => {\n      const { reference, container } = setup();\n      const selection = createSelection('<p>0123</p>', container);\n      const bounds = selection.getBounds(2);\n      expect(bounds?.left).approximately(\n        reference.left + reference.width * 2,\n        2,\n      );\n      expect(bounds?.height).approximately(reference.height, 1);\n      expect(bounds?.top).approximately(reference.top, 1);\n    });\n\n    test('multiple characters', () => {\n      const { reference, container } = setup();\n      const selection = createSelection('<p>0123</p>', container);\n      const bounds = selection.getBounds(1, 2);\n      expect(bounds?.left).approximately(reference.left + reference.width, 2);\n      expect(bounds?.height).approximately(reference.height, 1);\n      expect(bounds?.top).approximately(reference.top, 1);\n      expect(bounds?.width).approximately(reference.width * 2, 2);\n    });\n\n    test('start of line', () => {\n      const { reference, container } = setup();\n      const selection = createSelection(\n        `\n        <p>0000</p>\n        <p>0000</p>`,\n        container,\n      );\n      const bounds = selection.getBounds(5);\n      expect(bounds?.left).approximately(reference.left, 1);\n      expect(bounds?.height).approximately(reference.height, 1);\n      expect(bounds?.top).approximately(\n        reference.top + reference.lineHeight,\n        1,\n      );\n    });\n\n    test('end of line', () => {\n      const { reference, container } = setup();\n      const selection = createSelection(\n        `\n        <p>0000</p>\n        <p>0000</p>\n        <p>0000</p>`,\n        container,\n      );\n      const bounds = selection.getBounds(9);\n      expect(bounds?.left).approximately(\n        reference.left + reference.width * 4,\n        4,\n      );\n      expect(bounds?.height).approximately(reference.height, 1);\n      expect(bounds?.top).approximately(\n        reference.top + reference.lineHeight,\n        1,\n      );\n    });\n\n    test('selection starting at end of text node', () => {\n      const { reference, container } = setup();\n      container.style.width = `${reference.width * 4}px`;\n      const selection = createSelection(\n        `\n        <p>\n          0000\n          <b>0000</b>\n          0000\n        </p>`,\n        container,\n      );\n      const bounds = selection.getBounds(4, 1);\n      expect(bounds?.width).approximately(reference.width, 1);\n    });\n\n    test('multiple lines', () => {\n      const { reference, container } = setup();\n      const selection = createSelection(\n        `\n        <p>0000</p>\n        <p>0000</p>\n        <p>0000</p>`,\n        container,\n      );\n      const bounds = selection.getBounds(2, 4);\n      expect(bounds?.left).approximately(reference.left, 1);\n      expect(bounds?.height).approximately(reference.height * 2, 3);\n      expect(bounds?.top).approximately(reference.top, 1);\n      expect(bounds?.width).toBeGreaterThan(3 * reference.width);\n    });\n\n    test('large text', () => {\n      const { reference, container } = setup();\n      const selection = createSelection(\n        '<p><span class=\"ql-size-large\">0000</span></p>',\n        container,\n      );\n      const span = container.querySelector('span') as HTMLSpanElement;\n      const bounds = selection.getBounds(2);\n      expect(bounds?.left).approximately(\n        reference.left + span.offsetWidth / 2,\n        3,\n      );\n      expect(bounds?.height).approximately(span.offsetHeight, 3);\n      expect(bounds?.top).approximately(reference.top, 3);\n    });\n\n    test('image', () => {\n      const { reference, container } = setup();\n      const selection = createSelection(\n        `\n        <p>\n          <img src=\"/assets/favicon.png\" width=\"32px\" height=\"32px\">\n          <img src=\"/assets/favicon.png\" width=\"32px\" height=\"32px\">\n        </p>`,\n        container,\n      );\n      const bounds = selection.getBounds(1);\n      expect(bounds?.left).approximately(reference.left + 32, 1);\n      expect(bounds?.height).approximately(32, 1);\n      expect(bounds?.top).approximately(reference.top, 3);\n    });\n\n    test('beyond document', () => {\n      const selection = createSelection('<p>0123</p>');\n      expect(() => {\n        selection.getBounds(10, 0);\n      }).not.toThrow();\n      expect(() => {\n        selection.getBounds(0, 10);\n      }).not.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts",
    "content": "import '../../../../src/quill.js';\nimport { describe, expect, test, vitest } from 'vitest';\nimport createRegistryWithFormats from '../../../../src/core/utils/createRegistryWithFormats.js';\nimport { globalRegistry } from '../../../../src/core/quill.js';\nimport logger from '../../../../src/core/logger.js';\nimport { Registry } from 'parchment';\nimport Inline from '../../../../src/blots/inline.js';\nimport Container from '../../../../src/blots/container.js';\n\nconst debug = logger('test');\n\ndescribe('createRegistryWithFormats', () => {\n  test('register core formats', () => {\n    const registry = createRegistryWithFormats([], globalRegistry, debug);\n    expect(registry.query('cursor')).toBeTruthy();\n    expect(registry.query('bold')).toBeFalsy();\n  });\n\n  test('register specified formats', () => {\n    const registry = createRegistryWithFormats(['bold'], globalRegistry, debug);\n    expect(registry.query('cursor')).toBeTruthy();\n    expect(registry.query('bold')).toBeTruthy();\n  });\n\n  test('register required container', () => {\n    const sourceRegistry = new Registry();\n\n    class RequiredContainer extends Container {\n      static blotName = 'my-required-container';\n    }\n    class Child extends Inline {\n      static requiredContainer = RequiredContainer;\n      static blotName = 'my-child';\n    }\n\n    sourceRegistry.register(Child);\n\n    const registry = createRegistryWithFormats(\n      ['my-child'],\n      sourceRegistry,\n      debug,\n    );\n\n    expect(registry.query('my-child')).toBeTruthy();\n    expect(registry.query('my-required-container')).toBeTruthy();\n  });\n\n  test('infinite loop', () => {\n    const sourceRegistry = new Registry();\n\n    class InfiniteBlot extends Inline {\n      static requiredContainer = InfiniteBlot;\n      static blotName = 'infinite-blot';\n    }\n\n    sourceRegistry.register(InfiniteBlot);\n\n    const logError = vitest.spyOn(debug, 'error');\n    const registry = createRegistryWithFormats(\n      ['infinite-blot'],\n      sourceRegistry,\n      debug,\n    );\n\n    expect(registry.query('infinite-blot')).toBeTruthy();\n    expect(logError).toHaveBeenCalledWith(\n      expect.stringMatching('Cycle detected'),\n    );\n  });\n\n  test('report missing formats', () => {\n    const logError = vitest.spyOn(debug, 'error');\n    const registry = createRegistryWithFormats(\n      ['my-unknown'],\n      globalRegistry,\n      debug,\n    );\n    expect(registry.query('my-unknown')).toBeFalsy();\n    expect(logError).toHaveBeenCalledWith(expect.stringMatching('my-unknown'));\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/align.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport { describe, test, expect } from 'vitest';\nimport {\n  createRegistry,\n  createScroll as baseCreateScroll,\n} from '../__helpers__/factory.js';\nimport { AlignClass } from '../../../src/formats/align.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([AlignClass]));\n\ndescribe('Align', () => {\n  test('add', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(4, 1, { align: 'center' });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { align: 'center' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p class=\"ql-align-center\">0123</p>',\n    );\n  });\n\n  test('remove', () => {\n    const editor = new Editor(\n      createScroll('<p class=\"ql-align-center\">0123</p>'),\n    );\n    editor.formatText(4, 1, { align: false });\n    expect(editor.getDelta()).toEqual(new Delta().insert('0123\\n'));\n    expect(editor.scroll.domNode).toEqualHTML('<p>0123</p>');\n  });\n\n  test('whitelist', () => {\n    const editor = new Editor(\n      createScroll('<p class=\"ql-align-center\">0123</p>'),\n    );\n    editor.formatText(4, 1, { align: 'middle' });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { align: 'center' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p class=\"ql-align-center\">0123</p>',\n    );\n  });\n\n  test('invalid scope', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(1, 2, { align: 'center' });\n    expect(editor.getDelta()).toEqual(new Delta().insert('0123\\n'));\n    expect(editor.scroll.domNode).toEqualHTML('<p>0123</p>');\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/bold.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport {\n  createRegistry,\n  createScroll as baseCreateScroll,\n} from '../__helpers__/factory.js';\nimport Bold from '../../../src/formats/bold.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([Bold]));\n\ndescribe('Bold', () => {\n  test('optimize and merge', () => {\n    const scroll = createScroll('<p><strong>a</strong>b<strong>c</strong></p>');\n    const bold = document.createElement('b');\n    bold.appendChild(scroll.domNode.firstChild?.childNodes[1] as Node);\n    scroll.domNode.firstChild?.insertBefore(\n      bold,\n      scroll.domNode.firstChild.lastChild,\n    );\n    scroll.update();\n    expect(scroll.domNode).toEqualHTML('<p><strong>abc</strong></p>');\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/code.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport { describe, expect, test } from 'vitest';\nimport CodeBlock, {\n  Code,\n  CodeBlockContainer,\n} from '../../../src/formats/code.js';\nimport Italic from '../../../src/formats/italic.js';\nimport Header from '../../../src/formats/header.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(\n    html,\n    createRegistry([Code, CodeBlock, CodeBlockContainer, Italic, Header]),\n  );\n\ndescribe('Code', () => {\n  test('format newline', () => {\n    const editor = new Editor(createScroll('<p><br></p>'));\n    editor.formatLine(0, 1, { 'code-block': true });\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\"><br /></div>\n      </div>\n    `);\n  });\n\n  test('format lines', () => {\n    const editor = new Editor(createScroll('<p><em>0123</em></p><p>5678</p>'));\n    editor.formatLine(2, 5, { 'code-block': true });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { 'code-block': true })\n        .insert('5678')\n        .insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">0123</div>\n        <div class=\"ql-code-block\">5678</div>\n      </div>\n    `);\n  });\n\n  test('remove format', () => {\n    const editor = new Editor(\n      createScroll(\n        '<div class=\"ql-code-block-container\" spellcheck=\"false\"><div class=\"ql-code-block\">0123</div></div>',\n      ),\n    );\n    editor.formatText(4, 1, { 'code-block': false });\n    expect(editor.getDelta()).toEqual(new Delta().insert('0123\\n'));\n    expect(editor.scroll.domNode).toEqualHTML('<p>0123</p>');\n  });\n\n  test('delete last', () => {\n    const editor = new Editor(\n      createScroll(\n        '<p>0123</p><div class=\"ql-code-block-container\" spellcheck=\"false\"><div class=\"ql-code-block\"><br></div></div><p>5678</p>',\n      ),\n    );\n    editor.deleteText(4, 1);\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { 'code-block': true })\n        .insert('5678\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">0123</div>\n      </div>\n      <p>5678</p>\n    `);\n  });\n\n  test('delete merge before', () => {\n    const editor = new Editor(\n      createScroll(\n        '<h1>0123</h1><div class=\"ql-code-block-container\" spellcheck=\"false\"><div class=\"ql-code-block\">4567</div></div>',\n      ),\n    );\n    editor.deleteText(4, 1);\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('01234567').insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01234567</div>\n      </div>\n    `);\n  });\n\n  test('delete merge after', () => {\n    const editor = new Editor(\n      createScroll(\n        '<div class=\"ql-code-block-container\" spellcheck=\"false\"><div class=\"ql-code-block\">0123</div></div><h1>4567</h1>',\n      ),\n    );\n    editor.deleteText(4, 1);\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('01234567').insert('\\n', { header: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML('<h1>01234567</h1>');\n  });\n\n  test('delete across before partial merge', () => {\n    const editor = new Editor(\n      createScroll(\n        `<div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">34</div>\n        <div class=\"ql-code-block\">67</div>\n      </div>\n      <h1>90</h1>`,\n      ),\n    );\n    editor.deleteText(7, 3);\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('01')\n        .insert('\\n', { 'code-block': true })\n        .insert('34')\n        .insert('\\n', { 'code-block': true })\n        .insert('60')\n        .insert('\\n', { header: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">34</div>\n      </div>\n      <h1>60</h1>\n    `);\n  });\n\n  test('delete across before no merge', () => {\n    const editor = new Editor(\n      createScroll(\n        `<div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">34</div>\n      </div>\n      <h1>6789</h1>`,\n      ),\n    );\n    editor.deleteText(3, 5);\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('01')\n        .insert('\\n', { 'code-block': true })\n        .insert('89')\n        .insert('\\n', { header: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n      </div>\n      <h1>89</h1>\n    `);\n  });\n\n  test('delete across after', () => {\n    const editor = new Editor(\n      createScroll(\n        `<h1>0123</h1>\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">56</div>\n        <div class=\"ql-code-block\">89</div>\n      </div>`,\n      ),\n    );\n    editor.deleteText(2, 4);\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('016')\n        .insert('\\n', { 'code-block': true })\n        .insert('89')\n        .insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">016</div>\n        <div class=\"ql-code-block\">89</div>\n      </div>\n    `);\n  });\n\n  test('replace', () => {\n    const editor = new Editor(\n      createScroll(\n        '<div class=\"ql-code-block-container\" spellcheck=\"false\"><div class=\"ql-code-block\">0123</div></div>',\n      ),\n    );\n    editor.formatText(4, 1, { header: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { header: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML('<h1>0123</h1>');\n  });\n\n  test('replace multiple', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">23</div>\n      </div>\n    `,\n      ),\n    );\n    editor.formatText(0, 6, { header: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('01')\n        .insert('\\n', { header: 1 })\n        .insert('23')\n        .insert('\\n', { header: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML('<h1>01</h1>\\n<h1>23</h1>');\n  });\n\n  test('format imprecise bounds', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">23</div>\n        <div class=\"ql-code-block\">45</div>\n      </div>\n    `,\n      ),\n    );\n    editor.formatText(1, 6, { header: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('01')\n        .insert('\\n', { header: 1 })\n        .insert('23')\n        .insert('\\n', { header: 1 })\n        .insert('45')\n        .insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <h1>01</h1>\n      <h1>23</h1>\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">45</div>\n      </div>\n    `);\n  });\n\n  test('format without newline', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">23</div>\n        <div class=\"ql-code-block\">45</div>\n      </div>\n    `,\n      ),\n    );\n    editor.formatText(3, 1, { header: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('01')\n        .insert('\\n', { 'code-block': true })\n        .insert('23')\n        .insert('\\n', { 'code-block': true })\n        .insert('45')\n        .insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">23</div>\n        <div class=\"ql-code-block\">45</div>\n      </div>\n    `);\n  });\n\n  test('format line', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n        <div class=\"ql-code-block\">23</div>\n        <div class=\"ql-code-block\">45</div>\n      </div>\n    `,\n      ),\n    );\n    editor.formatLine(3, 1, { header: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('01')\n        .insert('\\n', { 'code-block': true })\n        .insert('23')\n        .insert('\\n', { header: 1 })\n        .insert('45')\n        .insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">01</div>\n      </div>\n      <h1>23</h1>\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">45</div>\n      </div>\n    `);\n  });\n\n  test('ignore formatAt', () => {\n    const editor = new Editor(\n      createScroll(\n        '<div class=\"ql-code-block-container\" spellcheck=\"false\"><div class=\"ql-code-block\">0123</div></div>',\n      ),\n    );\n    editor.formatText(1, 1, { bold: true });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { 'code-block': true }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">0123</div>\n      </div>\n    `);\n  });\n\n  test('partial block modification applyDelta', () => {\n    const editor = new Editor(\n      createScroll(\n        `<div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">a</div>\n        <div class=\"ql-code-block\">b</div>\n        <div class=\"ql-code-block\"><br></div>\n      </div>`,\n      ),\n    );\n    const delta = new Delta()\n      .retain(3)\n      .insert('\\n', { 'code-block': true })\n      .delete(1)\n      .retain(1, { 'code-block': null });\n    editor.applyDelta(delta);\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <div class=\"ql-code-block-container\" spellcheck=\"false\">\n        <div class=\"ql-code-block\">a</div>\n        <div class=\"ql-code-block\">b</div>\n      </div>\n      <p><br /></p>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/color.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport { ColorStyle } from '../../../src/formats/color.js';\nimport { describe, expect, test } from 'vitest';\nimport Bold from '../../../src/formats/bold.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([ColorStyle, Bold]));\n\ndescribe('Color', () => {\n  test('add', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(1, 2, { color: 'red' });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0').insert('12', { color: 'red' }).insert('3\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>0<span style=\"color: red;\">12</span>3</p>',\n    );\n  });\n\n  test('remove', () => {\n    const editor = new Editor(\n      createScroll('<p>0<strong style=\"color: red;\">12</strong>3</p>'),\n    );\n    editor.formatText(1, 2, { color: false });\n    const delta = new Delta()\n      .insert('0')\n      .insert('12', { bold: true })\n      .insert('3\\n');\n    expect(editor.getDelta()).toEqual(delta);\n    expect(editor.scroll.domNode).toEqualHTML('<p>0<strong>12</strong>3</p>');\n  });\n\n  test('remove unwrap', () => {\n    const editor = new Editor(\n      createScroll('<p>0<span style=\"color: red;\">12</span>3</p>'),\n    );\n    editor.formatText(1, 2, { color: false });\n    expect(editor.getDelta()).toEqual(new Delta().insert('0123\\n'));\n    expect(editor.scroll.domNode).toEqualHTML('<p>0123</p>');\n  });\n\n  test('invalid scope', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(4, 1, { color: 'red' });\n    expect(editor.getDelta()).toEqual(new Delta().insert('0123\\n'));\n    expect(editor.scroll.domNode).toEqualHTML('<p>0123</p>');\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/header.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport Editor from '../../../src/core/editor.js';\nimport Header from '../../../src/formats/header.js';\nimport Italic from '../../../src/formats/italic.js';\nimport { describe, expect, test } from 'vitest';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([Header, Italic]));\n\ndescribe('Header', () => {\n  test('add', () => {\n    const editor = new Editor(createScroll('<p><em>0123</em></p>'));\n    editor.formatText(4, 1, { header: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123', { italic: true }).insert('\\n', { header: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML('<h1><em>0123</em></h1>');\n  });\n\n  test('remove', () => {\n    const editor = new Editor(createScroll('<h1><em>0123</em></h1>'));\n    editor.formatText(4, 1, { header: false });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123', { italic: true }).insert('\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML('<p><em>0123</em></p>');\n  });\n\n  test('change', () => {\n    const editor = new Editor(createScroll('<h1><em>0123</em></h1>'));\n    editor.formatText(4, 1, { header: 2 });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123', { italic: true }).insert('\\n', { header: 2 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML('<h2><em>0123</em></h2>');\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/indent.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport Editor from '../../../src/core/editor.js';\nimport List, { ListContainer } from '../../../src/formats/list.js';\nimport IndentClass from '../../../src/formats/indent.js';\nimport { describe, expect, test } from 'vitest';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([ListContainer, List, IndentClass]));\n\ndescribe('Indent', () => {\n  test('+1', () => {\n    const editor = new Editor(\n      createScroll('<ol><li data-list=\"bullet\">0123</li></ol>'),\n    );\n    editor.formatText(4, 1, { indent: '+1' });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { list: 'bullet', indent: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li class=\"ql-indent-1\" data-list=\"bullet\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('-1', () => {\n    const editor = new Editor(\n      createScroll(\n        '<ol><li data-list=\"bullet\" class=\"ql-indent-1\">0123</li></ol>',\n      ),\n    );\n    editor.formatText(4, 1, { indent: '-1' });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { list: 'bullet' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"bullet\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('1', () => {\n    const editor = new Editor(createScroll('<p>abc</p>'));\n    editor.formatText(3, 1, { indent: 1 });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('abc').insert('\\n', { indent: 1 }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`<p class=\"ql-indent-1\">abc</p>`);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/link.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport Editor from '../../../src/core/editor.js';\nimport Link from '../../../src/formats/link.js';\nimport { describe, expect, test } from 'vitest';\nimport { SizeClass } from '../../../src/formats/size.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([Link, SizeClass]));\n\ndescribe('Link', () => {\n  test('add', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(1, 2, { link: 'https://quilljs.com' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0')\n        .insert('12', { link: 'https://quilljs.com' })\n        .insert('3\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>0<a href=\"https://quilljs.com\" rel=\"noopener noreferrer\" target=\"_blank\">12</a>3</p>',\n    );\n  });\n\n  test('add invalid', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(1, 2, { link: 'javascript:alert(0);' }); // eslint-disable-line no-script-url\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0')\n        .insert('12', { link: Link.SANITIZED_URL })\n        .insert('3\\n'),\n    );\n  });\n\n  test('add non-whitelisted protocol', () => {\n    const editor = new Editor(createScroll('<p>0123</p>'));\n    editor.formatText(1, 2, { link: 'gopher://quilljs.com' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0')\n        .insert('12', { link: Link.SANITIZED_URL })\n        .insert('3\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>0<a href=\"about:blank\" rel=\"noopener noreferrer\" target=\"_blank\">12</a>3</p>',\n    );\n  });\n\n  test('change', () => {\n    const editor = new Editor(\n      createScroll(\n        '<p>0<a href=\"https://github.com\" target=\"_blank\" rel=\"noopener noreferrer\">12</a>3</p>',\n      ),\n    );\n    editor.formatText(1, 2, { link: 'https://quilljs.com' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0')\n        .insert('12', { link: 'https://quilljs.com' })\n        .insert('3\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>0<a href=\"https://quilljs.com\" rel=\"noopener noreferrer\" target=\"_blank\">12</a>3</p>',\n    );\n  });\n\n  test('remove', () => {\n    const editor = new Editor(\n      createScroll(\n        '<p>0<a class=\"ql-size-large\" href=\"https://quilljs.com\" rel=\"noopener noreferrer\" target=\"_blank\">12</a>3</p>',\n      ),\n    );\n    editor.formatText(1, 2, { link: false });\n    const delta = new Delta()\n      .insert('0')\n      .insert('12', { size: 'large' })\n      .insert('3\\n');\n    expect(editor.getDelta()).toEqual(delta);\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>0<span class=\"ql-size-large\">12</span>3</p>',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/list.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport Editor from '../../../src/core/editor.js';\nimport { describe, expect, test } from 'vitest';\nimport List, { ListContainer } from '../../../src/formats/list.js';\nimport IndentClass from '../../../src/formats/indent.js';\nimport { AlignClass } from '../../../src/formats/align.js';\nimport Video from '../../../src/formats/video.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(\n    html,\n    createRegistry([ListContainer, List, IndentClass, AlignClass, Video]),\n  );\n\ndescribe('List', () => {\n  test('add', () => {\n    const editor = new Editor(\n      createScroll(`\n      <p>0123</p>\n      <p>5678</p>\n      <p>0123</p>`),\n    );\n    editor.formatText(9, 1, { list: 'ordered' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123\\n5678')\n        .insert('\\n', { list: 'ordered' })\n        .insert('0123\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <p>0123</p>\n      <ol>\n        <li data-list=\"ordered\">5678</li>\n      </ol>\n      <p>0123</p>\n    `);\n  });\n\n  test('checklist', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <p>0123</p>\n      <p>5678</p>\n      <p>0123</p>\n    `,\n      ),\n    );\n    editor.scroll.domNode.classList.add('ql-editor');\n    editor.formatText(4, 1, { list: 'checked' });\n    editor.formatText(9, 1, { list: 'unchecked' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { list: 'checked' })\n        .insert('5678')\n        .insert('\\n', { list: 'unchecked' })\n        .insert('0123\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"checked\">0123</li>\n        <li data-list=\"unchecked\">5678</li>\n      </ol>\n      <p>0123</p>\n    `);\n  });\n\n  test('remove', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <p>0123</p>\n      <ol><li data-list=\"ordered\">5678</li></ol>\n      <p>0123</p>\n    `,\n      ),\n    );\n    editor.formatText(9, 1, { list: null });\n    expect(editor.getDelta()).toEqual(new Delta().insert('0123\\n5678\\n0123\\n'));\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <p>0123</p>\n      <p>5678</p>\n      <p>0123</p>\n    `);\n  });\n\n  test('replace', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <p>0123</p>\n      <ol><li data-list=\"ordered\">5678</li></ol>\n      <p>0123</p>\n    `,\n      ),\n    );\n    editor.formatText(9, 1, { list: 'bullet' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123\\n5678')\n        .insert('\\n', { list: 'bullet' })\n        .insert('0123\\n'),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <p>0123</p>\n      <ol>\n        <li data-list=\"bullet\">5678</li>\n      </ol>\n      <p>0123</p>\n    `);\n  });\n\n  test('replace checklist with bullet', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol>\n        <li data-list=\"checked\">0123</li>\n      </ol>\n    `,\n      ),\n    );\n    editor.formatText(4, 1, { list: 'bullet' });\n    expect(editor.getDelta()).toEqual(\n      new Delta().insert('0123').insert('\\n', { list: 'bullet' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"bullet\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('replace with attributes', () => {\n    const editor = new Editor(\n      createScroll(\n        '<ol><li data-list=\"ordered\" class=\"ql-align-center\">0123</li></ol>',\n      ),\n    );\n    editor.formatText(4, 1, { list: 'bullet' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { align: 'center', list: 'bullet' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li class=\"ql-align-center\" data-list=\"bullet\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('format merge', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol><li data-list=\"ordered\">0123</li></ol>\n      <p>5678</p>\n      <ol><li data-list=\"ordered\">0123</li></ol>\n    `,\n      ),\n    );\n    editor.formatText(9, 1, { list: 'ordered' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { list: 'ordered' })\n        .insert('5678')\n        .insert('\\n', { list: 'ordered' })\n        .insert('0123')\n        .insert('\\n', { list: 'ordered' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">0123</li>\n        <li data-list=\"ordered\">5678</li>\n        <li data-list=\"ordered\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('delete merge', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol><li data-list=\"ordered\">0123</li></ol>\n      <p>5678</p>\n      <ol><li data-list=\"ordered\">0123</li></ol>`,\n      ),\n    );\n    editor.deleteText(5, 5);\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { list: 'ordered' })\n        .insert('0123')\n        .insert('\\n', { list: 'ordered' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">0123</li>\n        <li data-list=\"ordered\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('merge checklist', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol><li data-list=\"checked\">0123</li></ol>\n      <p>5678</p>\n      <ol><li data-list=\"checked\">0123</li></ol>\n    `,\n      ),\n    );\n    editor.formatText(9, 1, { list: 'checked' });\n    expect(editor.getDelta()).toEqual(\n      new Delta()\n        .insert('0123')\n        .insert('\\n', { list: 'checked' })\n        .insert('5678')\n        .insert('\\n', { list: 'checked' })\n        .insert('0123')\n        .insert('\\n', { list: 'checked' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"checked\">0123</li>\n        <li data-list=\"checked\">5678</li>\n        <li data-list=\"checked\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('empty line interop', () => {\n    const editor = new Editor(\n      createScroll('<ol><li data-list=\"ordered\"><br></li></ol>'),\n    );\n    editor.insertText(0, 'Test');\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">Test</li>\n      </ol>\n    `);\n    editor.deleteText(0, 4);\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\"><br /></li>\n      </ol>\n    `);\n  });\n\n  test('delete multiple items', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol>\n        <li data-list=\"ordered\">0123</li>\n        <li data-list=\"ordered\">5678</li>\n        <li data-list=\"ordered\">0123</li>\n      </ol>`,\n      ),\n    );\n    editor.deleteText(2, 5);\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">0178</li>\n        <li data-list=\"ordered\">0123</li>\n      </ol>\n    `);\n  });\n\n  test('delete across last item', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol><li data-list=\"ordered\">0123</li></ol>\n      <p>5678</p>`,\n      ),\n    );\n    editor.deleteText(2, 5);\n    expect(editor.scroll.domNode).toEqualHTML('<p>0178</p>');\n  });\n\n  test('delete partial', () => {\n    const editor = new Editor(\n      createScroll('<p>0123</p><ol><li data-list=\"ordered\">5678</li></ol>'),\n    );\n    editor.deleteText(2, 5);\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">0178</li>\n      </ol>\n    `);\n  });\n\n  test('nested list replacement', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <ol>\n        <li data-list=\"bullet\">One</li>\n        <li class=\"ql-indent-1\" data-list=\"bullet\">Alpha</li>\n        <li data-list=\"bullet\">Two</li>\n      </ol>\n    `,\n      ),\n    );\n    editor.formatLine(1, 10, { list: 'bullet' });\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"bullet\">One</li>\n        <li class=\"ql-indent-1\" data-list=\"bullet\">Alpha</li>\n        <li data-list=\"bullet\">Two</li>\n      </ol>\n    `);\n  });\n\n  test('copy atttributes', () => {\n    const editor = new Editor(\n      createScroll('<p class=\"ql-align-center\">Test</p>'),\n    );\n    editor.formatLine(4, 1, { list: 'bullet' });\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li class=\"ql-align-center\" data-list=\"bullet\">Test</li>\n      </ol>\n    `);\n  });\n\n  test('insert block embed', () => {\n    const editor = new Editor(\n      createScroll('<ol><li data-list=\"ordered\">Test</li></ol>'),\n    );\n    editor.insertEmbed(\n      2,\n      'video',\n      'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0',\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">Te</li>\n      </ol>\n      <iframe allowfullscreen=\"true\" class=\"ql-video\" frameborder=\"0\" src=\"https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0\"></iframe>\n      <ol>\n        <li data-list=\"ordered\">st</li>\n      </ol>\n    `);\n  });\n\n  test('insert block embed at beginning', () => {\n    const editor = new Editor(\n      createScroll('<ol><li data-list=\"ordered\">Test</li></ol>'),\n    );\n    editor.insertEmbed(\n      0,\n      'video',\n      'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0',\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <iframe allowfullscreen=\"true\" class=\"ql-video\" frameborder=\"0\" src=\"https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0\"></iframe>\n      <ol>\n        <li data-list=\"ordered\">Test</li>\n      </ol>\n    `);\n  });\n\n  test('insert block embed at end', () => {\n    const editor = new Editor(\n      createScroll('<ol><li data-list=\"ordered\">Test</li></ol>'),\n    );\n    editor.insertEmbed(\n      4,\n      'video',\n      'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0',\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <ol>\n        <li data-list=\"ordered\">Test</li>\n      </ol>\n      <iframe allowfullscreen=\"true\" class=\"ql-video\" frameborder=\"0\" src=\"https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0\"></iframe>\n      <ol>\n        <li data-list=\"ordered\"><br /></li>\n      </ol>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/script.spec.ts",
    "content": "import Editor from '../../../src/core/editor.js';\nimport Script from '../../../src/formats/script.js';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport { describe, expect, test } from 'vitest';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(html, createRegistry([Script]));\n\ndescribe('Script', () => {\n  test('add', () => {\n    const editor = new Editor(\n      createScroll('<p>a<sup>2</sup> + b2 = c<sup>2</sup></p>'),\n    );\n    editor.formatText(6, 1, { script: 'super' });\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>a<sup>2</sup> + b<sup>2</sup> = c<sup>2</sup></p>',\n    );\n  });\n\n  test('remove', () => {\n    const editor = new Editor(\n      createScroll('<p>a<sup>2</sup> + b<sup>2</sup></p>'),\n    );\n    editor.formatText(1, 1, { script: false });\n    expect(editor.scroll.domNode).toEqualHTML('<p>a2 + b<sup>2</sup></p>');\n  });\n\n  test('replace', () => {\n    const editor = new Editor(\n      createScroll('<p>a<sup>2</sup> + b<sup>2</sup></p>'),\n    );\n    editor.formatText(1, 1, { script: 'sub' });\n    expect(editor.scroll.domNode).toEqualHTML(\n      '<p>a<sub>2</sub> + b<sup>2</sup></p>',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/formats/table.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport {\n  createScroll as baseCreateScroll,\n  createRegistry,\n} from '../__helpers__/factory.js';\nimport { describe, expect, test } from 'vitest';\nimport {\n  TableBody,\n  TableCell,\n  TableContainer,\n  TableRow,\n} from '../../../src/formats/table.js';\nimport Header from '../../../src/formats/header.js';\n\nconst createScroll = (html: string) =>\n  baseCreateScroll(\n    html,\n    createRegistry([TableBody, TableCell, TableContainer, TableRow, Header]),\n  );\n\nconst tableDelta = new Delta()\n  .insert('A1')\n  .insert('\\n', { table: 'a' })\n  .insert('A2')\n  .insert('\\n', { table: 'a' })\n  .insert('A3')\n  .insert('\\n', { table: 'a' })\n  .insert('B1')\n  .insert('\\n', { table: 'b' })\n  .insert('B2')\n  .insert('\\n', { table: 'b' })\n  .insert('B3')\n  .insert('\\n', { table: 'b' })\n  .insert('C1')\n  .insert('\\n', { table: 'c' })\n  .insert('C2')\n  .insert('\\n', { table: 'c' })\n  .insert('C3')\n  .insert('\\n', { table: 'c' });\n\nconst tableHTML = `\n  <table>\n    <tbody>\n      <tr>\n        <td data-row=\"a\">A1</td>\n        <td data-row=\"a\">A2</td>\n        <td data-row=\"a\">A3</td>\n      </tr>\n      <tr>\n        <td data-row=\"b\">B1</td>\n        <td data-row=\"b\">B2</td>\n        <td data-row=\"b\">B3</td>\n      </tr>\n      <tr>\n        <td data-row=\"c\">C1</td>\n        <td data-row=\"c\">C2</td>\n        <td data-row=\"c\">C3</td>\n      </tr>\n    </tbody>\n  </table>\n  `;\n\ndescribe('Table', () => {\n  test('initialize', () => {\n    const editor = new Editor(createScroll(tableHTML));\n    expect(editor.getDelta()).toEqual(tableDelta);\n    expect(editor.scroll.domNode).toEqualHTML(tableHTML);\n  });\n\n  test('add', () => {\n    const editor = new Editor(createScroll(''));\n    editor.applyDelta(new Delta([...tableDelta.ops]).delete(1));\n    expect(editor.scroll.domNode).toEqualHTML(tableHTML);\n  });\n\n  test('add format plaintext', () => {\n    const editor = new Editor(createScroll('<p>Test</p>'));\n    editor.formatLine(0, 5, { table: 'a' });\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">Test</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('add format replace', () => {\n    const editor = new Editor(createScroll('<h1>Test</h1>'));\n    editor.formatLine(0, 5, { table: 'a' });\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">Test</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('remove format plaintext', () => {\n    const editor = new Editor(\n      createScroll('<table><tr><td data-row=\"a\">Test</td></tr></table>'),\n    );\n    editor.formatLine(0, 5, { table: null });\n    expect(editor.scroll.domNode).toEqualHTML('<p>Test</p>');\n  });\n\n  test('remove format replace', () => {\n    const editor = new Editor(\n      createScroll('<table><tr><td data-row=\"a\">Test</td></tr></table>'),\n    );\n    editor.formatLine(0, 5, { header: 1 });\n    expect(editor.scroll.domNode).toEqualHTML('<h1>Test</h1>');\n  });\n\n  test('group rows', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <table>\n        <tbody>\n          <tr><td data-row=\"a\">A</td></tr>\n          <tr><td data-row=\"a\">B</td></tr>\n        </tbody>\n      </table>\n    `,\n      ),\n    );\n    // @ts-expect-error\n    editor.scroll.children.head.children.head.children.head.optimize();\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">A</td>\n            <td data-row=\"a\">B</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('split rows', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <table>\n        <tbody>\n          <tr><td data-row=\"a\">A</td><td data-row=\"b\">B</td></tr>\n        </tbody>\n      </table>\n    `,\n      ),\n    );\n    // @ts-expect-error\n    editor.scroll.children.head.children.head.children.head.optimize();\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">A</td>\n          </tr>\n          <tr>\n            <td data-row=\"b\">B</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('group and split rows', () => {\n    const editor = new Editor(\n      createScroll(`\n      <table>\n        <tbody>\n          <tr><td data-row=\"a\">A</td><td data-row=\"b\">B1</td></tr>\n          <tr><td data-row=\"b\">B2</td></tr>\n        </tbody>\n      </table>\n    `),\n    );\n    // @ts-expect-error\n    editor.scroll.children.head.children.head.children.head.optimize();\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">A</td>\n          </tr>\n          <tr>\n            <td data-row=\"b\">B1</td>\n            <td data-row=\"b\">B2</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('balance cells', () => {\n    const editor = new Editor(\n      createScroll(\n        `<table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">A1</td>\n          </tr>\n          <tr>\n            <td data-row=\"b\">B1</td>\n            <td data-row=\"b\">B2</td>\n          </tr>\n          <tr>\n            <td data-row=\"c\">C1</td>\n            <td data-row=\"c\">C2</td>\n            <td data-row=\"c\">C3</td>\n          </tr>\n        </tbody>\n      </table>`,\n      ),\n    );\n    // @ts-expect-error\n    editor.scroll.children.head.balanceCells();\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">A1</td>\n            <td data-row=\"a\"><br /></td>\n            <td data-row=\"a\"><br /></td>\n          </tr>\n          <tr>\n            <td data-row=\"b\">B1</td>\n            <td data-row=\"b\">B2</td>\n            <td data-row=\"b\"><br /></td>\n          </tr>\n          <tr>\n            <td data-row=\"c\">C1</td>\n            <td data-row=\"c\">C2</td>\n            <td data-row=\"c\">C3</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('format', () => {\n    const editor = new Editor(createScroll('<p>a</p><p>b</p><p>1</p><p>2</p>'));\n    editor.formatLine(0, 4, { table: 'a' });\n    editor.formatLine(4, 4, { table: 'b' });\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\">a</td>\n            <td data-row=\"a\">b</td>\n          </tr>\n          <tr>\n            <td data-row=\"b\">1</td>\n            <td data-row=\"b\">2</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n\n  test('applyDelta', () => {\n    const editor = new Editor(createScroll('<p><br /></p>'));\n    editor.applyDelta(\n      new Delta().insert('\\n\\n', { table: 'a' }).insert('\\n\\n', { table: 'b' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"a\"><br /></td>\n            <td data-row=\"a\"><br /></td>\n          </tr>\n          <tr>\n            <td data-row=\"b\"><br /></td>\n            <td data-row=\"b\"><br /></td>\n          </tr>\n        </tbody>\n      </table>\n      <p><br /></p>\n    `);\n  });\n\n  test('unbalanced table applyDelta', () => {\n    const editor = new Editor(createScroll('<p><br /></p>'));\n    editor.applyDelta(\n      new Delta()\n        .insert('A1\\nB1\\nC1\\n', { table: '1' })\n        .insert('A2\\nB2\\nC2\\n', { table: '2' })\n        .insert('A3\\nB3\\n', { table: '3' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"1\">A1</td>\n            <td data-row=\"1\">B1</td>\n            <td data-row=\"1\">C1</td>\n          </tr>\n          <tr>\n            <td data-row=\"2\">A2</td>\n            <td data-row=\"2\">B2</td>\n            <td data-row=\"2\">C2</td>\n          </tr>\n          <tr>\n            <td data-row=\"3\">A3</td>\n            <td data-row=\"3\">B3</td>\n          </tr>\n        </tbody>\n      </table>\n      <p><br /></p>\n    `);\n  });\n\n  test('existing table applyDelta', () => {\n    const editor = new Editor(\n      createScroll(\n        `\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"1\">A1</td>\n          </tr>\n          <tr>\n            <td data-row=\"2\"><br /></td>\n            <td data-row=\"2\">B1</td>\n          </tr>\n        </tbody>\n      </table>`,\n      ),\n    );\n    editor.applyDelta(\n      new Delta()\n        .retain(3)\n        .retain(1, { table: '1' })\n        .insert('\\n', { table: '2' }),\n    );\n    expect(editor.scroll.domNode).toEqualHTML(`\n      <table>\n        <tbody>\n          <tr>\n            <td data-row=\"1\">A1</td>\n            <td data-row=\"1\"><br /></td>\n          </tr>\n          <tr>\n            <td data-row=\"2\"><br /></td>\n            <td data-row=\"2\">B1</td>\n          </tr>\n        </tbody>\n      </table>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/clipboard.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport { describe, expect, test, vitest } from 'vitest';\nimport Quill from '../../../src/core.js';\nimport { Range } from '../../../src/core/selection.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Header from '../../../src/formats/header.js';\nimport Image from '../../../src/formats/image.js';\nimport IndentClass from '../../../src/formats/indent.js';\nimport Italic from '../../../src/formats/italic.js';\nimport Link from '../../../src/formats/link.js';\nimport List, { ListContainer } from '../../../src/formats/list.js';\nimport {\n  TableBody,\n  TableCell,\n  TableContainer,\n  TableRow,\n} from '../../../src/formats/table.js';\nimport Video from '../../../src/formats/video.js';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport type { RegistryDefinition } from 'parchment';\nimport {\n  DirectionAttribute,\n  DirectionClass,\n  DirectionStyle,\n} from '../../../src/formats/direction.js';\nimport CodeBlock from '../../../src/formats/code.js';\nimport { ColorClass, ColorStyle } from '../../../src/formats/color.js';\n\ndescribe('Clipboard', () => {\n  describe('events', () => {\n    const createQuill = () => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n      container.innerHTML = '<h1>0123</h1><p>5<em>67</em>8</p>';\n      const registry = createRegistry([Bold, Italic, Header]);\n      const quill = new Quill(container, { registry });\n      quill.setSelection(2, 5);\n      return quill;\n    };\n\n    describe('paste', () => {\n      const clipboardEvent = {\n        clipboardData: {\n          getData: (type: string) =>\n            type === 'text/html' ? '<strong>|</strong>' : '|',\n        },\n        preventDefault: () => {},\n      } as ClipboardEvent;\n\n      test('pastes html data', async () => {\n        const quill = createQuill();\n        quill.clipboard.onCapturePaste(clipboardEvent);\n        expect(quill.root).toEqualHTML(\n          '<p>01<strong>|</strong><em>7</em>8</p>',\n        );\n        expect(quill.getSelection()).toEqual(new Range(3));\n      });\n\n      test('pastes with \"paste and match style\"', async () => {\n        const quill = createQuill();\n        quill.setContents([\n          { insert: 'abc', attributes: { bold: true } },\n          { insert: '\\n' },\n        ]);\n        quill.setSelection(3, 0);\n        quill.clipboard.onCapturePaste({\n          clipboardData: {\n            getData: (type: string) =>\n              type === 'text/plain' ? 'def' : undefined,\n          },\n          preventDefault: () => {},\n        } as ClipboardEvent);\n        expect(quill.getContents().ops).toEqual([\n          { insert: 'abcdef', attributes: { bold: true } },\n          { insert: '\\n' },\n        ]);\n      });\n\n      test('pastes links from iOS share sheets', async () => {\n        const quill = createQuill();\n        quill.setContents(new Delta().insert('\\n'));\n        quill.clipboard.onCapturePaste({\n          clipboardData: {\n            getData: (type: string) =>\n              type === 'text/uri-list' ? 'https://example.com' : undefined,\n          },\n          preventDefault: () => {},\n        } as ClipboardEvent);\n        expect(quill.getContents().ops).toEqual([\n          { insert: 'https://example.com\\n' },\n        ]);\n\n        // Ignore comments\n        quill.setContents(new Delta().insert('\\n'));\n        quill.clipboard.onCapturePaste({\n          clipboardData: {\n            getData: (type: string) =>\n              type === 'text/uri-list'\n                ? 'https://example.com\\r\\n# Comment\\r\\nhttps://example.com/a'\n                : undefined,\n          },\n          preventDefault: () => {},\n        } as ClipboardEvent);\n        expect(quill.getContents().ops).toEqual([\n          { insert: 'https://example.com\\nhttps://example.com/a\\n' },\n        ]);\n      });\n\n      // Copying from Word includes both html and files\n      test('pastes html data if present with file', async () => {\n        const quill = createQuill();\n        const upload = vitest.spyOn(quill.uploader, 'upload');\n        quill.clipboard.onCapturePaste({\n          ...clipboardEvent,\n          clipboardData: {\n            ...clipboardEvent.clipboardData,\n            // @ts-expect-error\n            files: ['file'],\n          },\n        });\n        expect(upload).not.toHaveBeenCalled();\n        expect(quill.root).toEqualHTML(\n          '<p>01<strong>|</strong><em>7</em>8</p>',\n        );\n        expect(quill.getSelection()).toEqual(new Range(3));\n      });\n\n      test('pastes image file if present with image only html', async () => {\n        const quill = createQuill();\n        const upload = vitest.spyOn(quill.uploader, 'upload');\n        quill.clipboard.onCapturePaste({\n          ...clipboardEvent,\n          clipboardData: {\n            getData: (type) =>\n              type === 'text/html'\n                ? `<meta charset='utf-8'><img src=\"/assets/favicon.png\"/>`\n                : '|',\n            // @ts-expect-error\n            files: ['file'],\n          },\n        });\n        expect(upload).toHaveBeenCalled();\n      });\n\n      test('does not fire selection-change', async () => {\n        const quill = createQuill();\n        const change = vitest.fn();\n        quill.on('selection-change', change);\n        quill.clipboard.onCapturePaste(clipboardEvent);\n        expect(change).not.toHaveBeenCalled();\n      });\n    });\n\n    describe('cut', () => {\n      const setup = () => {\n        const clipboardData: Record<string, string> = {};\n        const clipboardEvent = {\n          clipboardData: {\n            setData: (type, data) => {\n              clipboardData[type] = data;\n            },\n          },\n          preventDefault: () => {},\n        } as ClipboardEvent;\n        return { clipboardData, clipboardEvent };\n      };\n\n      test('keeps formats of first line', async () => {\n        const quill = createQuill();\n        const { clipboardData, clipboardEvent } = setup();\n        quill.clipboard.onCaptureCopy(clipboardEvent, true);\n        expect(quill.root).toEqualHTML('<h1>01<em>7</em>8</h1>');\n        expect(quill.getSelection()).toEqual(new Range(2));\n        expect(clipboardData['text/plain']).toEqual('23\\n56');\n        expect(clipboardData['text/html']).toEqual(\n          '<h1>23</h1><p>5<em>6</em></p>',\n        );\n      });\n    });\n\n    test('dangerouslyPasteHTML(html)', () => {\n      const quill = createQuill();\n      quill.clipboard.dangerouslyPasteHTML('<i>ab</i><b>cd</b>');\n      expect(quill.root).toEqualHTML('<p><em>ab</em><strong>cd</strong></p>');\n    });\n\n    test('dangerouslyPasteHTML(index, html)', () => {\n      const quill = createQuill();\n      quill.clipboard.dangerouslyPasteHTML(2, '<b>ab</b>');\n      expect(quill.root).toEqualHTML(`\n        <h1>01<strong>ab</strong>23</h1>\n        <p>5<em>67</em>8</p>\n      `);\n    });\n  });\n\n  describe('convert', () => {\n    const createClipboard = (extraFormats: RegistryDefinition[] = []) => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n      const registry = createRegistry([\n        Bold,\n        Italic,\n        Header,\n        TableBody,\n        TableContainer,\n        TableCell,\n        TableRow,\n        ListContainer,\n        List,\n        IndentClass,\n        Image,\n        Video,\n        Link,\n        ...extraFormats,\n      ]);\n      const quill = new Quill(container, { registry });\n      quill.setSelection(2, 5);\n      return quill.clipboard;\n    };\n\n    test('text with adjacent spaces', () => {\n      const delta = createClipboard().convert({ text: 'simple  text' });\n      expect(delta).toEqual(new Delta().insert('simple  text'));\n    });\n\n    test('text with newlines', () => {\n      const delta = createClipboard().convert({ text: 'simple\\ntext' });\n      expect(delta).toEqual(new Delta().insert('simple\\ntext'));\n    });\n\n    test('only text in html', () => {\n      const delta = createClipboard().convert({ html: 'simple plain text' });\n      expect(delta).toEqual(new Delta().insert('simple plain text'));\n    });\n\n    test('whitespace', () => {\n      const html =\n        '<div> 0 </div><div> <div> 1 2 <span> 3 </span> 4 </div> </div>' +\n        '<div><span>5 </span><span>6 </span><span> 7</span><span> 8</span></div>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('0\\n1 2  3  4\\n5 6  7 8'));\n    });\n\n    test('multiple whitespaces', () => {\n      const html = '<div>1   2    3</div>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('1 2 3'));\n    });\n\n    test('inline whitespace', () => {\n      const html = '<p>0 <strong>1</strong> 2</p>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(\n        new Delta().insert('0 ').insert('1', { bold: true }).insert(' 2'),\n      );\n    });\n\n    test('intentional whitespace', () => {\n      const html = '<span>0&nbsp;<strong>1</strong>&nbsp;2</span>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(\n        new Delta().insert('0 ').insert('1', { bold: true }).insert(' 2'),\n      );\n    });\n\n    test('consecutive intentional whitespace', () => {\n      const html = '<strong>&nbsp;&nbsp;1&nbsp;&nbsp;</strong>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('  1  ', { bold: true }));\n    });\n\n    test('intentional whitespace at line start/end', () => {\n      expect(\n        createClipboard().convert({ html: '<p>0 &nbsp;</p><p>&nbsp; 2</p>' }),\n      ).toEqual(new Delta().insert('0  \\n  2'));\n      expect(\n        createClipboard().convert({ html: '<p>0&nbsp; </p><p> &nbsp;2</p>' }),\n      ).toEqual(new Delta().insert('0 \\n 2'));\n    });\n\n    test('newlines between inline elements', () => {\n      const html = '<span>foo</span>\\n<span>bar</span>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('foo bar'));\n    });\n\n    test('multiple newlines between inline elements', () => {\n      const html = '<span>foo</span>\\n\\n\\n\\n<span>bar</span>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('foo bar'));\n    });\n\n    test('newlines between block elements', () => {\n      const html = '<p>foo</p>\\n<p>bar</p>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('foo\\nbar'));\n    });\n\n    test('multiple newlines between block elements', () => {\n      const html = '<p>foo</p>\\n\\n\\n\\n<p>bar</p>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('foo\\nbar'));\n    });\n\n    test('space between empty paragraphs', () => {\n      const html = '<p></p> <p></p>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('\\n'));\n    });\n\n    test('newline between empty paragraphs', () => {\n      const html = '<p></p>\\n<p></p>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('\\n'));\n    });\n\n    test('break', () => {\n      const html =\n        '<div>0<br>1</div><div>2<br></div><div>3</div><div><br>4</div><div><br></div><div>5</div>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('0\\n1\\n2\\n3\\n\\n4\\n\\n5'));\n    });\n\n    test('empty block', () => {\n      const html = '<h1>Test</h1><h2></h2><p>Body</p>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('Test\\n', { header: 1 })\n          .insert('\\n', { header: 2 })\n          .insert('Body'),\n      );\n    });\n\n    test('mixed inline and block', () => {\n      const delta = createClipboard().convert({\n        html: '<div>One<div>Two</div></div>',\n      });\n      expect(delta).toEqual(new Delta().insert('One\\nTwo'));\n    });\n\n    test('alias', () => {\n      const delta = createClipboard().convert({\n        html: '<b>Bold</b><i>Italic</i>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('Bold', { bold: true })\n          .insert('Italic', { italic: true }),\n      );\n    });\n\n    test('pre', () => {\n      const html = '<pre> 01 \\n 23 </pre>';\n      expect(createClipboard([CodeBlock]).convert({ html })).toEqual(\n        new Delta().insert(' 01 \\n 23 \\n', { 'code-block': true }),\n      );\n      expect(createClipboard().convert({ html })).toEqual(\n        new Delta().insert(' 01 \\n 23 '),\n      );\n    });\n\n    test('pre with \\\\n node', () => {\n      const html = '<pre><span> 01 </span>\\n<span> 23 </span></pre>';\n      const delta = createClipboard([CodeBlock]).convert({ html });\n      expect(delta).toEqual(\n        new Delta().insert(' 01 \\n 23 \\n', { 'code-block': true }),\n      );\n    });\n\n    test('nested list', () => {\n      const delta = createClipboard().convert({\n        html: '<ol><li>One</li><li class=\"ql-indent-1\">Alpha</li></ol>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('One\\n', { list: 'ordered' })\n          .insert('Alpha\\n', { list: 'ordered', indent: 1 }),\n      );\n    });\n\n    test('html nested list', () => {\n      const delta = createClipboard().convert({\n        html: '<ol><li>One<ol><li>Alpha</li><li>Beta<ol><li>I</li></ol></li></ol></li></ol>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('One\\n', { list: 'ordered' })\n          .insert('Alpha\\nBeta\\n', { list: 'ordered', indent: 1 })\n          .insert('I\\n', { list: 'ordered', indent: 2 }),\n      );\n    });\n\n    test('html nested bullet', () => {\n      const delta = createClipboard().convert({\n        html: '<ul><li>One<ul><li>Alpha</li><li>Beta<ul><li>I</li></ul></li></ul></li></ul>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('One\\n', { list: 'bullet' })\n          .insert('Alpha\\nBeta\\n', { list: 'bullet', indent: 1 })\n          .insert('I\\n', { list: 'bullet', indent: 2 }),\n      );\n    });\n\n    test('html nested checklist', () => {\n      const delta = createClipboard().convert({\n        html:\n          '<ul><li data-list=\"checked\">One<ul><li data-list=\"checked\">Alpha</li><li data-list=\"checked\">Beta' +\n          '<ul><li data-list=\"checked\">I</li></ul></li></ul></li></ul>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('One\\n', { list: 'checked' })\n          .insert('Alpha\\nBeta\\n', { list: 'checked', indent: 1 })\n          .insert('I\\n', { list: 'checked', indent: 2 }),\n      );\n    });\n\n    test('html partial list', () => {\n      const delta = createClipboard().convert({\n        html: '<ol><li><ol><li><ol><li>iiii</li></ol></li><li>bbbb</li></ol></li><li>2222</li></ol>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('iiii\\n', { list: 'ordered', indent: 2 })\n          .insert('bbbb\\n', { list: 'ordered', indent: 1 })\n          .insert('2222\\n', { list: 'ordered' }),\n      );\n    });\n\n    test('html table', () => {\n      const delta = createClipboard().convert({\n        html:\n          '<table>' +\n          '<thead><tr><td>A1</td><td>A2</td><td>A3</td></tr></thead>' +\n          '<tbody><tr><td>B1</td><td></td><td>B3</td></tr></tbody>' +\n          '</table>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('A1\\nA2\\nA3\\n', { table: 1 })\n          .insert('B1\\n\\nB3\\n', { table: 2 }),\n      );\n    });\n\n    test('embeds', () => {\n      const delta = createClipboard().convert({\n        html: '<div>01<img src=\"/assets/favicon.png\" height=\"200\" width=\"300\">34</div>',\n      });\n      const expected = new Delta()\n        .insert('01')\n        .insert(\n          { image: '/assets/favicon.png' },\n          { height: '200', width: '300' },\n        )\n        .insert('34');\n      expect(delta).toEqual(expected);\n    });\n\n    test('block embed', () => {\n      const delta = createClipboard().convert({\n        html: '<p>01</p><iframe src=\"#\"></iframe><p>34</p>',\n      });\n      expect(delta).toEqual(\n        new Delta().insert('01\\n').insert({ video: '#' }).insert('34'),\n      );\n    });\n\n    test('block embeds within blocks', () => {\n      const delta = createClipboard().convert({\n        html: '<h1>01<iframe src=\"#\"></iframe>34</h1><p>67</p>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('01\\n', { header: 1 })\n          .insert({ video: '#' }, { header: 1 })\n          .insert('34\\n', { header: 1 })\n          .insert('67'),\n      );\n    });\n\n    test('wrapped block embed', () => {\n      const delta = createClipboard().convert({\n        html: '<h1>01<a href=\"/\"><iframe src=\"#\"></iframe></a>34</h1><p>67</p>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('01\\n', { header: 1 })\n          .insert({ video: '#' }, { link: '/', header: 1 })\n          .insert('34\\n', { header: 1 })\n          .insert('67'),\n      );\n    });\n\n    test('wrapped block embed with siblings', () => {\n      const delta = createClipboard().convert({\n        html: '<h1>01<a href=\"/\">a<iframe src=\"#\"></iframe>b</a>34</h1><p>67</p>',\n      });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('01', { header: 1 })\n          .insert('a\\n', { link: '/', header: 1 })\n          .insert({ video: '#' }, { link: '/', header: 1 })\n          .insert('b', { link: '/', header: 1 })\n          .insert('34\\n', { header: 1 })\n          .insert('67'),\n      );\n    });\n\n    test('attributor and style match', () => {\n      const html = '<p style=\"direction:rtl;\">Test</p>';\n      const attributors = [DirectionStyle, DirectionClass, DirectionAttribute];\n      attributors.forEach((attributor) => {\n        expect(createClipboard([attributor]).convert({ html })).toEqual(\n          new Delta().insert('Test\\n', { direction: 'rtl' }),\n        );\n      });\n\n      expect(createClipboard().convert({ html })).toEqual(\n        new Delta().insert('Test'),\n      );\n    });\n\n    test('nested styles', () => {\n      const html =\n        '<span style=\"color: red;\"><span style=\"color: blue;\">Test</span></span>';\n      const attributors = [ColorStyle, ColorClass];\n      attributors.forEach((attributor) => {\n        expect(createClipboard([attributor]).convert({ html })).toEqual(\n          new Delta().insert('Test', { color: 'blue' }),\n        );\n      });\n\n      expect(createClipboard().convert({ html })).toEqual(\n        new Delta().insert('Test'),\n      );\n    });\n\n    test('custom matcher', () => {\n      const clipboard = createClipboard();\n      clipboard.addMatcher(Node.TEXT_NODE, (node, delta) => {\n        let index = 0;\n        const regex = /https?:\\/\\/[^\\s]+/g;\n        let match: RegExpExecArray | null = null;\n        const composer = new Delta();\n        // eslint-disable-next-line no-cond-assign\n        while ((match = regex.exec((node as Text).data))) {\n          composer.retain(match.index - index);\n          index = regex.lastIndex;\n          composer.retain(match[0].length, { link: match[0] });\n        }\n        return delta.compose(composer);\n      });\n      const delta = clipboard.convert({\n        html: 'http://github.com https://quilljs.com',\n      });\n      const expected = new Delta()\n        .insert('http://github.com', { link: 'http://github.com' })\n        .insert(' ')\n        .insert('https://quilljs.com', { link: 'https://quilljs.com' });\n      expect(delta).toEqual(expected);\n    });\n\n    test('does not execute javascript', () => {\n      // @ts-expect-error\n      window.unsafeFunction = vitest.fn();\n      const html =\n        \"<img src='/assets/favicon.png' onload='window.unsafeFunction()'/>\";\n      createClipboard().convert({ html });\n      // @ts-expect-error\n      expect(window.unsafeFunction).not.toHaveBeenCalled();\n      // @ts-expect-error\n      delete window.unsafeFunction;\n    });\n\n    test('xss', () => {\n      const delta = createClipboard().convert({\n        html: '<script>alert(2);</script>',\n      });\n      expect(delta).toEqual(new Delta().insert(''));\n    });\n\n    test('Google Docs', () => {\n      const html = `<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-6f072e08-7fff-e641-0fbc-7fe2846294a4\"><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">text</span></p><br /><ol style=\"margin-top:0;margin-bottom:0;padding-inline-start:48px;\"><li dir=\"ltr\" style=\"list-style-type:decimal;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\" aria-level=\"1\"><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\" role=\"presentation\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">i1</span></p></li><li dir=\"ltr\" style=\"list-style-type:decimal;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\" aria-level=\"1\"><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\" role=\"presentation\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">i2</span></p></li><ol style=\"margin-top:0;margin-bottom:0;padding-inline-start:48px;\"><li dir=\"ltr\" style=\"list-style-type:lower-alpha;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\" aria-level=\"2\"><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\" role=\"presentation\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">i3</span></p></li></ol></ol><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">text</span></p></b><br class=\"Apple-interchange-newline\">`;\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(\n        new Delta()\n          .insert('text\\n')\n          .insert('i1\\ni2\\n', { list: 'ordered' })\n          .insert('i3\\n', { list: 'ordered', indent: 1 })\n          .insert('text', { bold: true })\n          .insert('\\n'),\n      );\n    });\n\n    test('ignore empty elements except paragraphs', () => {\n      const html = '<div>hello<div></div>my<p></p>world</div>';\n      const delta = createClipboard().convert({ html });\n      expect(delta).toEqual(new Delta().insert('hello\\nmy\\n\\nworld'));\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/history.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport { describe, expect, test, vitest } from 'vitest';\nimport Quill from '../../../src/core.js';\nimport { getLastChangeIndex } from '../../../src/modules/history.js';\nimport type { HistoryOptions } from '../../../src/modules/history.js';\nimport { createRegistry, createScroll } from '../__helpers__/factory.js';\nimport { sleep } from '../__helpers__/utils.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Image from '../../../src/formats/image.js';\nimport Link from '../../../src/formats/link.js';\nimport { AlignClass } from '../../../src/formats/align.js';\n\ndescribe('History', () => {\n  const scroll = createScroll(\n    '',\n    createRegistry([Bold, Image, Link, AlignClass]),\n  );\n\n  describe('getLastChangeIndex', () => {\n    test('delete', () => {\n      const delta = new Delta().retain(4).delete(2);\n      expect(getLastChangeIndex(scroll, delta)).toEqual(4);\n    });\n\n    test('delete with inserts', () => {\n      const delta = new Delta().retain(4).insert('test').delete(2);\n      expect(getLastChangeIndex(scroll, delta)).toEqual(8);\n    });\n\n    test('insert text', () => {\n      const delta = new Delta().retain(4).insert('testing');\n      expect(getLastChangeIndex(scroll, delta)).toEqual(11);\n    });\n\n    test('insert embed', () => {\n      const delta = new Delta().retain(4).insert({ image: true });\n      expect(getLastChangeIndex(scroll, delta)).toEqual(5);\n    });\n\n    test('insert with deletes', () => {\n      const delta = new Delta().retain(4).delete(3).insert('!');\n      expect(getLastChangeIndex(scroll, delta)).toEqual(5);\n    });\n\n    test('format', () => {\n      const delta = new Delta().retain(4).retain(3, { bold: true });\n      expect(getLastChangeIndex(scroll, delta)).toEqual(7);\n    });\n\n    test('format newline', () => {\n      const delta = new Delta().retain(4).retain(1, { align: 'left' });\n      expect(getLastChangeIndex(scroll, delta)).toEqual(4);\n    });\n\n    test('format mixed', () => {\n      const delta = new Delta()\n        .retain(4)\n        .retain(1, { align: 'left', bold: true });\n      expect(getLastChangeIndex(scroll, delta)).toEqual(4);\n    });\n\n    test('insert newline', () => {\n      const delta = new Delta().retain(4).insert('a\\n');\n      expect(getLastChangeIndex(scroll, delta)).toEqual(5);\n    });\n\n    test('mutliple newline inserts', () => {\n      const delta = new Delta().retain(4).insert('ab\\n\\n');\n      expect(getLastChangeIndex(scroll, delta)).toEqual(7);\n    });\n  });\n\n  describe('undo/redo', () => {\n    const setup = (options?: Partial<HistoryOptions>) => {\n      const container = document.body.appendChild(\n        document.createElement('div'),\n      );\n      container.innerHTML = '<div><p>The lazy fox</p></div>';\n      const quill = new Quill(container, {\n        modules: {\n          history: { delay: 400, ...options },\n        },\n        registry: scroll.registry,\n      });\n      return { quill, original: quill.getContents() };\n    };\n\n    test('limits undo stack size', () => {\n      const { quill } = setup({ delay: 0, maxStack: 2 });\n      ['A', 'B', 'C'].forEach((text) => {\n        quill.insertText(0, text);\n      });\n      expect(quill.history.stack.undo.length).toEqual(2);\n    });\n\n    test('emits selection changes', () => {\n      const { quill } = setup({ delay: 0 });\n      quill.insertText(0, 'foo');\n      const change = vitest.fn();\n      quill.on('selection-change', change);\n      quill.history.undo();\n\n      expect(change).toHaveBeenCalledOnce();\n      expect(change).toHaveBeenCalledWith(expect.anything(), null, 'user');\n    });\n\n    test('user change', () => {\n      const { quill, original } = setup({ delay: 0 });\n      (quill.root.firstChild as HTMLElement).innerHTML = 'The lazy foxes';\n      quill.update();\n      const changed = quill.getContents();\n      expect(changed).not.toEqual(original);\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(original);\n      quill.history.redo();\n      expect(quill.getContents()).toEqual(changed);\n    });\n\n    test('merge changes', () => {\n      const { quill, original } = setup();\n      expect(quill.history.stack.undo.length).toEqual(0);\n      quill.updateContents(new Delta().retain(12).insert('e'));\n      expect(quill.history.stack.undo.length).toEqual(1);\n      quill.updateContents(new Delta().retain(13).insert('s'));\n      expect(quill.history.stack.undo.length).toEqual(1);\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(original);\n      expect(quill.history.stack.undo.length).toEqual(0);\n    });\n\n    test('dont merge changes', async () => {\n      const { quill } = setup();\n      expect(quill.history.stack.undo.length).toEqual(0);\n      quill.updateContents(new Delta().retain(12).insert('e'));\n      expect(quill.history.stack.undo.length).toEqual(1);\n      // @ts-expect-error\n      await sleep((quill.history.options.delay as number) * 1.25);\n      quill.updateContents(new Delta().retain(13).insert('s'));\n      expect(quill.history.stack.undo.length).toEqual(2);\n    });\n\n    test('multiple undos', async () => {\n      const { quill, original } = setup();\n      expect(quill.history.stack.undo.length).toEqual(0);\n      quill.updateContents(new Delta().retain(12).insert('e'));\n      const contents = quill.getContents();\n      // @ts-expect-error\n      await sleep((quill.history.options.delay as number) * 1.25);\n      quill.updateContents(new Delta().retain(13).insert('s'));\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(contents);\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(original);\n    });\n\n    test('transform api change', () => {\n      const { quill } = setup();\n      // @ts-expect-error\n      quill.history.options.userOnly = true;\n      quill.updateContents(\n        new Delta().retain(12).insert('es'),\n        Quill.sources.USER,\n      );\n      quill.history.lastRecorded = 0;\n      quill.updateContents(\n        new Delta().retain(14).insert('!'),\n        Quill.sources.USER,\n      );\n      quill.history.undo();\n      quill.updateContents(new Delta().retain(4).delete(5), Quill.sources.API);\n      expect(quill.getContents()).toEqual(new Delta().insert('The foxes\\n'));\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(new Delta().insert('The fox\\n'));\n      quill.history.redo();\n      expect(quill.getContents()).toEqual(new Delta().insert('The foxes\\n'));\n      quill.history.redo();\n      expect(quill.getContents()).toEqual(new Delta().insert('The foxes!\\n'));\n    });\n\n    test('transform preserve intention', () => {\n      const { quill } = setup({ userOnly: true });\n      const url = 'https://www.google.com/';\n      quill.updateContents(\n        new Delta().insert(url, { link: url }),\n        Quill.sources.USER,\n      );\n      quill.history.lastRecorded = 0;\n      quill.updateContents(\n        new Delta().delete(url.length).insert('Google', { link: url }),\n        Quill.sources.API,\n      );\n      quill.history.lastRecorded = 0;\n      quill.updateContents(\n        new Delta().retain(quill.getLength() - 1).insert('!'),\n        Quill.sources.USER,\n      );\n      quill.history.lastRecorded = 0;\n      expect(quill.getContents()).toEqual(\n        new Delta().insert('Google', { link: url }).insert('The lazy fox!\\n'),\n      );\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(\n        new Delta().insert('Google', { link: url }).insert('The lazy fox\\n'),\n      );\n      quill.history.undo();\n      expect(quill.getContents()).toEqual(\n        new Delta().insert('Google', { link: url }).insert('The lazy fox\\n'),\n      );\n    });\n\n    test('ignore remote changes', () => {\n      const { quill } = setup();\n      // @ts-expect-error\n      quill.history.options.delay = 0;\n      // @ts-expect-error\n      quill.history.options.userOnly = true;\n      quill.setText('\\n');\n      quill.insertText(0, 'a', Quill.sources.USER);\n      quill.insertText(1, 'b', Quill.sources.API);\n      quill.insertText(2, 'c', Quill.sources.USER);\n      quill.insertText(3, 'd', Quill.sources.API);\n      expect(quill.getText()).toEqual('abcd\\n');\n      quill.history.undo();\n      expect(quill.getText()).toEqual('abd\\n');\n      quill.history.undo();\n      expect(quill.getText()).toEqual('bd\\n');\n      quill.history.redo();\n      expect(quill.getText()).toEqual('abd\\n');\n      quill.history.redo();\n      expect(quill.getText()).toEqual('abcd\\n');\n    });\n\n    test('correctly transform against remote changes', () => {\n      const { quill } = setup({ delay: 0, userOnly: true });\n      quill.setText('b\\n');\n      quill.insertText(1, 'd', Quill.sources.USER);\n      quill.insertText(0, 'a', Quill.sources.USER);\n      quill.insertText(2, 'c', Quill.sources.API);\n      expect(quill.getText()).toEqual('abcd\\n');\n      quill.history.undo();\n      expect(quill.getText()).toEqual('bcd\\n');\n      quill.history.undo();\n      expect(quill.getText()).toEqual('bc\\n');\n      quill.history.redo();\n      expect(quill.getText()).toEqual('bcd\\n');\n      quill.history.redo();\n      expect(quill.getText()).toEqual('abcd\\n');\n    });\n\n    test('correctly transform against remote changes breaking up an insert', () => {\n      const { quill } = setup({ delay: 0, userOnly: true });\n      quill.setText('\\n');\n      quill.insertText(0, 'ABC', Quill.sources.USER);\n      quill.insertText(3, '4', Quill.sources.API);\n      quill.insertText(2, '3', Quill.sources.API);\n      quill.insertText(1, '2', Quill.sources.API);\n      quill.insertText(0, '1', Quill.sources.API);\n      expect(quill.getText()).toEqual('1A2B3C4\\n');\n      quill.history.undo();\n      expect(quill.getText()).toEqual('1234\\n');\n      quill.history.redo();\n      expect(quill.getText()).toEqual('1A2B3C4\\n');\n      quill.history.undo();\n      expect(quill.getText()).toEqual('1234\\n');\n      quill.history.redo();\n      expect(quill.getText()).toEqual('1A2B3C4\\n');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/keyboard.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport Keyboard, {\n  SHORTKEY,\n  normalize,\n} from '../../../src/modules/keyboard.js';\n\nconst assert = <T>(value: T | null | undefined): T => {\n  if (value == null) {\n    throw new Error();\n  }\n  return value;\n};\n\nconst createKeyboardEvent = (key: string, override?: Partial<KeyboardEvent>) =>\n  new KeyboardEvent('keydown', {\n    key,\n    shiftKey: false,\n    metaKey: false,\n    ctrlKey: false,\n    altKey: false,\n    ...override,\n  });\n\ndescribe('Keyboard', () => {\n  describe('match', () => {\n    test('no modifiers', () => {\n      const binding = normalize({\n        key: 'a',\n      });\n      expect(Keyboard.match(createKeyboardEvent('a'), assert(binding))).toBe(\n        true,\n      );\n      expect(\n        Keyboard.match(\n          createKeyboardEvent('A', { altKey: true }),\n          assert(binding),\n        ),\n      ).toBe(false);\n    });\n\n    test('simple modifier', () => {\n      const binding = normalize({\n        key: 'a',\n        altKey: true,\n      });\n      expect(Keyboard.match(createKeyboardEvent('a'), assert(binding))).toBe(\n        false,\n      );\n      expect(\n        Keyboard.match(\n          createKeyboardEvent('a', { altKey: true }),\n          assert(binding),\n        ),\n      ).toBe(true);\n    });\n\n    test('optional modifier', () => {\n      const binding = normalize({\n        key: 'a',\n        altKey: null,\n      });\n      expect(Keyboard.match(createKeyboardEvent('a'), assert(binding))).toBe(\n        true,\n      );\n      expect(\n        Keyboard.match(\n          createKeyboardEvent('a', { altKey: true }),\n          assert(binding),\n        ),\n      ).toBe(true);\n    });\n\n    test('shortkey modifier', () => {\n      const binding = normalize({\n        key: 'a',\n        shortKey: true,\n      });\n      expect(Keyboard.match(createKeyboardEvent('a'), assert(binding))).toBe(\n        false,\n      );\n      expect(\n        Keyboard.match(\n          createKeyboardEvent('a', { [SHORTKEY]: true }),\n          assert(binding),\n        ),\n      ).toBe(true);\n    });\n\n    test('native shortkey modifier', () => {\n      const binding = normalize({\n        key: 'a',\n        [SHORTKEY]: true,\n      });\n      expect(Keyboard.match(createKeyboardEvent('a'), assert(binding))).toBe(\n        false,\n      );\n      expect(\n        Keyboard.match(\n          createKeyboardEvent('a', { [SHORTKEY]: true }),\n          assert(binding),\n        ),\n      ).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/normalizeExternalHTML/normalizers/googleDocs.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport normalize from '../../../../../src/modules/normalizeExternalHTML/normalizers/googleDocs.js';\n\ndescribe('Google Docs', () => {\n  test('remove unnecessary b tags', () => {\n    const html = `\n      <b\n        style=\"font-weight: normal;\"\n        id=\"docs-internal-guid-9f51ddb9-7fff-7da1-2cd6-e966f9297902\"\n      >\n        <span>Item 1</span><b>Item 2</b>\n      </b>\n      <b\n        style=\"font-weight: bold;\"\n      >Item 3</b>\n      `;\n\n    const doc = new DOMParser().parseFromString(html, 'text/html');\n    normalize(doc);\n    expect(doc.body.children).toMatchInlineSnapshot(`\n      HTMLCollection [\n        <span>\n          Item 1\n        </span>,\n        <b>\n          Item 2\n        </b>,\n        <b\n          style=\"font-weight: bold;\"\n        >\n          Item 3\n        </b>,\n      ]\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/normalizeExternalHTML/normalizers/msWord.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport normalize from '../../../../../src/modules/normalizeExternalHTML/normalizers/msWord.js';\n\ndescribe('Microsoft Word', () => {\n  test('keep the list style', () => {\n    const html = `\n      <html xmlns:w=\"urn:schemas-microsoft-com:office:word\">\n        <style>\n          @list l0:level3 { mso-level-number-format:bullet; }\n          @list l2:level1 { mso-level-number-format:alpha; }\n        </style>\n        <body>\n          <p style=\"mso-list: l0 level1 lfo1\"><span style=\"mso-list: Ignore;\">1. </span>item 1</p>\n          <p style=\"mso-list: l0 level3 lfo1\">item 2</p>\n          <p style=\"mso-list: l1 level4 lfo1\">item 3 in another list</p>\n          <p>Plain paragraph</p>\n          <p style=\"mso-list: l2 level1 lfo1\">the last item</p>\n        </body>\n      </html>\n      `;\n\n    const doc = new DOMParser().parseFromString(html, 'text/html');\n    normalize(doc);\n    expect(doc.body.children).toMatchInlineSnapshot(`\n      HTMLCollection [\n        <ul>\n          <li\n            data-list=\"ordered\"\n          >\n            item 1\n          </li>\n          <li\n            class=\"ql-indent-2\"\n            data-list=\"bullet\"\n          >\n            item 2\n          </li>\n        </ul>,\n        <ul>\n          <li\n            class=\"ql-indent-3\"\n            data-list=\"ordered\"\n          >\n            item 3 in another list\n          </li>\n        </ul>,\n        <p>\n          Plain paragraph\n        </p>,\n        <ul>\n          <li\n            data-list=\"ordered\"\n          >\n            the last item\n          </li>\n        </ul>,\n      ]\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/syntax.spec.ts",
    "content": "import hljs from 'highlight.js';\nimport Delta from 'quill-delta';\nimport { afterAll, beforeAll, describe, expect, test } from 'vitest';\nimport Quill from '../../../src/core.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Syntax, { CodeBlock, CodeToken } from '../../../src/modules/syntax.js';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport { normalizeHTML, sleep } from '../__helpers__/utils.js';\n\nconst HIGHLIGHT_INTERVAL = 10;\n\ndescribe('Syntax', () => {\n  beforeAll(() => {\n    Quill.register({ 'modules/syntax': Syntax }, true);\n    Syntax.register();\n    Syntax.DEFAULTS.languages = [\n      { key: 'javascript', label: 'JavaScript' },\n      { key: 'ruby', label: 'Ruby' },\n    ];\n  });\n\n  const createQuill = () => {\n    const container = document.body.appendChild(document.createElement('div'));\n    container.innerHTML = normalizeHTML(\n      `<pre data-language=\"javascript\">var test = 1;<br>var bugz = 0;<br></pre>\n      <p><br></p>`,\n    );\n    const quill = new Quill(container, {\n      modules: {\n        syntax: {\n          hljs,\n          interval: HIGHLIGHT_INTERVAL,\n        },\n      },\n      registry: createRegistry([\n        Bold,\n        CodeToken,\n        CodeBlock,\n        Quill.import('formats/code-block-container'),\n      ]),\n    });\n    return quill;\n  };\n\n  describe('highlighting', () => {\n    test('initialize', () => {\n      const quill = createQuill();\n      expect(quill.root).toEqualHTML(\n        `<div class=\"ql-code-block-container\" spellcheck=\"false\">\n          <div class=\"ql-code-block\" data-language=\"javascript\">var test = 1;</div>\n          <div class=\"ql-code-block\" data-language=\"javascript\">var bugz = 0;</div>\n        </div>\n        <p><br></p>`,\n      );\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('var bugz = 0;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('\\n'),\n      );\n    });\n\n    test('adds token', async () => {\n      const quill = createQuill();\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(\n        `<div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> test = <span class=\"ql-token hljs-number\">1</span>;</div>\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> bugz = <span class=\"ql-token hljs-number\">0</span>;</div>\n          </div>\n          <p><br></p>`,\n      );\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('var bugz = 0;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('\\n'),\n      );\n    });\n\n    test('tokens do not escape', async () => {\n      const quill = createQuill();\n      quill.deleteText(22, 6);\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(`\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> test = <span class=\"ql-token hljs-number\">1</span>;</div>\n          </div>\n          <p>var bugz</p>`);\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('var bugz\\n'),\n      );\n    });\n\n    test('change language', async () => {\n      const quill = createQuill();\n      quill.formatLine(0, 20, 'code-block', 'ruby');\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(`\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"ruby\">var test = <span class=\"ql-token hljs-number\">1</span>;</div>\n            <div class=\"ql-code-block\" data-language=\"ruby\">var bugz = <span class=\"ql-token hljs-number\">0</span>;</div>\n          </div>\n          <p><br></p>`);\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'ruby' })\n          .insert('var bugz = 0;')\n          .insert('\\n', { 'code-block': 'ruby' })\n          .insert('\\n'),\n      );\n    });\n\n    test('invalid language', async () => {\n      const quill = createQuill();\n      quill.formatLine(0, 20, 'code-block', 'invalid');\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(`\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"plain\">var test = 1;</div>\n            <div class=\"ql-code-block\" data-language=\"plain\">var bugz = 0;</div>\n          </div>\n          <p><br></p>`);\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'plain' })\n          .insert('var bugz = 0;')\n          .insert('\\n', { 'code-block': 'plain' })\n          .insert('\\n'),\n      );\n    });\n\n    test('unformat first line', async () => {\n      const quill = createQuill();\n      quill.formatLine(0, 1, 'code-block', false);\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(`\n          <p>var test = 1;</p>\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> bugz = <span class=\"ql-token hljs-number\">0</span>;</div>\n          </div>\n          <p><br></p>`);\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;\\nvar bugz = 0;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('\\n'),\n      );\n    });\n\n    test('split container', async () => {\n      const quill = createQuill();\n      quill.updateContents(new Delta().retain(14).insert('\\n'));\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(\n        `\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <select class=\"ql-ui\" contenteditable=\"false\">\n              <option value=\"javascript\">JavaScript</option>\n              <option value=\"ruby\">Ruby</option>\n            </select>\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> test = <span class=\"ql-token hljs-number\">1</span>;</div>\n          </div>\n          <p><br></p>\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <select class=\"ql-ui\" contenteditable=\"false\">\n              <option value=\"javascript\">JavaScript</option>\n              <option value=\"ruby\">Ruby</option>\n            </select>\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> bugz = <span class=\"ql-token hljs-number\">0</span>;</div>\n          </div>\n          <p><br></p>`,\n      );\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('\\nvar bugz = 0;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('\\n'),\n      );\n    });\n\n    test('merge containers', async () => {\n      const quill = createQuill();\n      quill.updateContents(new Delta().retain(14).insert('\\n'));\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      quill.deleteText(14, 1);\n      await sleep(HIGHLIGHT_INTERVAL + 1);\n      expect(quill.root).toEqualHTML(\n        `\n            <div class=\"ql-code-block-container\" spellcheck=\"false\">\n              <select class=\"ql-ui\" contenteditable=\"false\">\n                <option value=\"javascript\">JavaScript</option>\n                <option value=\"ruby\">Ruby</option>\n              </select>\n              <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> test = <span class=\"ql-token hljs-number\">1</span>;</div>\n              <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> bugz = <span class=\"ql-token hljs-number\">0</span>;</div>\n            </div>\n            <p><br></p>`,\n      );\n      expect(quill.getContents()).toEqual(\n        new Delta()\n          .insert('var test = 1;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('var bugz = 0;')\n          .insert('\\n', { 'code-block': 'javascript' })\n          .insert('\\n'),\n      );\n    });\n\n    describe('allowedChildren', () => {\n      beforeAll(() => {\n        CodeBlock.allowedChildren.push(Bold);\n      });\n\n      afterAll(() => {\n        CodeBlock.allowedChildren.pop();\n      });\n\n      test('modification', async () => {\n        const quill = createQuill();\n        quill.formatText(2, 3, 'bold', true);\n        await sleep(HIGHLIGHT_INTERVAL + 1);\n        expect(quill.root).toEqualHTML(`\n          <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">va</span><strong><span class=\"ql-token hljs-keyword\">r</span> t</strong>est = <span class=\"ql-token hljs-number\">1</span>;</div>\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">var</span> bugz = <span class=\"ql-token hljs-number\">0</span>;</div>\n          </div>\n          <p><br></p>`);\n        expect(quill.getContents()).toEqual(\n          new Delta()\n            .insert('va')\n            .insert('r t', { bold: true })\n            .insert('est = 1;')\n            .insert('\\n', { 'code-block': 'javascript' })\n            .insert('var bugz = 0;')\n            .insert('\\n', { 'code-block': 'javascript' })\n            .insert('\\n'),\n        );\n      });\n\n      test('removal', async () => {\n        const quill = createQuill();\n        quill.formatText(2, 3, 'bold', true);\n        await sleep(HIGHLIGHT_INTERVAL + 1);\n        quill.formatLine(0, 15, 'code-block', false);\n        expect(quill.root).toEqualHTML(\n          `<p>va<strong>r t</strong>est = 1;</p><p>var bugz = 0;</p><p><br></p>`,\n        );\n        expect(quill.getContents()).toEqual(\n          new Delta()\n            .insert('va')\n            .insert('r t', { bold: true })\n            .insert('est = 1;\\nvar bugz = 0;\\n\\n'),\n        );\n      });\n\n      test('addition', async () => {\n        const quill = createQuill();\n        quill.setText('var test = 1;\\n');\n        quill.formatText(2, 3, 'bold', true);\n        quill.formatLine(0, 1, 'code-block', 'javascript');\n        await sleep(HIGHLIGHT_INTERVAL + 1);\n        expect(quill.root).toEqualHTML(`\n            <div class=\"ql-code-block-container\" spellcheck=\"false\">\n            <div class=\"ql-code-block\" data-language=\"javascript\"><span class=\"ql-token hljs-keyword\">va</span><strong><span class=\"ql-token hljs-keyword\">r</span> t</strong>est = <span class=\"ql-token hljs-number\">1</span>;</div>\n          </div>`);\n        expect(quill.getContents()).toEqual(\n          new Delta()\n            .insert('va')\n            .insert('r t', { bold: true })\n            .insert('est = 1;')\n            .insert('\\n', { 'code-block': 'javascript' }),\n        );\n      });\n    });\n  });\n\n  describe('html', () => {\n    test('code language', () => {\n      const quill = createQuill();\n      expect(quill.getSemanticHTML()).toContain('data-language=\"javascript\"');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/table.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport Quill from '../../../src/core.js';\nimport { describe, expect, test } from 'vitest';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport {\n  TableBody,\n  TableCell,\n  TableContainer,\n  TableRow,\n} from '../../../src/formats/table.js';\nimport { normalizeHTML } from '../__helpers__/utils.js';\nimport Table from '../../../src/modules/table.js';\n\nconst createQuill = (html: string) => {\n  Quill.register({ 'modules/table': Table }, true);\n  const container = document.body.appendChild(document.createElement('div'));\n  container.innerHTML = normalizeHTML(html);\n  const quill = new Quill(container, {\n    modules: { table: true },\n    registry: createRegistry([TableBody, TableCell, TableContainer, TableRow]),\n  });\n  return quill;\n};\n\ndescribe('Table Module', () => {\n  describe('insert table', () => {\n    test('empty', () => {\n      const quill = createQuill('<p><br></p>');\n      const table = quill.getModule('table') as Table;\n      quill.setSelection(0);\n      table.insertTable(2, 3);\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td><br></td><td><br></td><td><br></td></tr>\n            <tr><td><br></td><td><br></td><td><br></td></tr>\n          </tbody>\n        </table>\n        <p><br></p>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('split', () => {\n      const quill = createQuill('<p>0123</p>');\n      const table = quill.getModule('table') as Table;\n      quill.setSelection(2);\n      table.insertTable(2, 3);\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td>01</td><td><br></td><td><br></td></tr>\n            <tr><td><br></td><td><br></td><td><br></td></tr>\n          </tbody>\n        </table>\n        <p>23</p>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n  });\n\n  describe('modify table', () => {\n    const setup = () => {\n      const tableHTML = `\n        <table>\n          <tbody>\n            <tr><td>a1</td><td>a2</td><td>a3</td></tr>\n            <tr><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `;\n      const quill = createQuill(tableHTML);\n      const table = quill.getModule('table') as Table;\n      return { quill, table };\n    };\n\n    test('insertRowAbove', () => {\n      const { quill, table } = setup();\n      quill.setSelection(0);\n      table.insertRowAbove();\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td><br></td><td><br></td><td><br></td></tr>\n            <tr><td>a1</td><td>a2</td><td>a3</td></tr>\n            <tr><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('insertRowBelow', () => {\n      const { quill, table } = setup();\n      quill.setSelection(0);\n      table.insertRowBelow();\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td>a1</td><td>a2</td><td>a3</td></tr>\n            <tr><td><br></td><td><br></td><td><br></td></tr>\n            <tr><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('insertColumnLeft', () => {\n      const { quill, table } = setup();\n      quill.setSelection(0);\n      table.insertColumnLeft();\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td><br></td><td>a1</td><td>a2</td><td>a3</td></tr>\n            <tr><td><br></td><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('insertColumnRight', () => {\n      const { quill, table } = setup();\n      quill.setSelection(0);\n      table.insertColumnRight();\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td>a1</td><td><br></td><td>a2</td><td>a3</td></tr>\n            <tr><td>b1</td><td><br></td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('deleteRow', () => {\n      const { quill, table } = setup();\n      quill.setSelection(0);\n      table.deleteRow();\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('deleteColumn', () => {\n      const { quill, table } = setup();\n      quill.setSelection(0);\n      table.deleteColumn();\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td>a2</td><td>a3</td></tr>\n            <tr><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('insertText before', () => {\n      const { quill } = setup();\n      quill.updateContents(new Delta().insert('\\n'));\n      expect(quill.root).toEqualHTML(\n        `\n        <p><br></p>\n        <table>\n          <tbody>\n            <tr><td>a1</td><td>a2</td><td>a3</td></tr>\n            <tr><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n\n    test('insertText after', () => {\n      const { quill } = setup();\n      quill.updateContents(new Delta().retain(18).insert('\\n'));\n      expect(quill.root).toEqualHTML(\n        `\n        <table>\n          <tbody>\n            <tr><td>a1</td><td>a2</td><td>a3</td></tr>\n            <tr><td>b1</td><td>b2</td><td>b3</td></tr>\n          </tbody>\n        </table>\n        <p><br></p>\n      `,\n        { ignoreAttrs: ['data-row'] },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/tableEmbed.spec.ts",
    "content": "import Delta from 'quill-delta';\nimport { tableHandler } from '../../../src/modules/tableEmbed.js';\nimport { afterEach, beforeEach, describe, expect, test } from 'vitest';\n\ndescribe('tableHandler', () => {\n  beforeEach(() => {\n    Delta.registerEmbed('table-embed', tableHandler);\n  });\n\n  afterEach(() => {\n    Delta.unregisterEmbed('table-embed');\n  });\n\n  describe('compose', () => {\n    test('adds a row', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' }, attributes: { height: 20 } },\n              ],\n              columns: [\n                { insert: { id: '22222222' } },\n                { insert: { id: '33333333' }, attributes: { width: 30 } },\n                { insert: { id: '44444444' } },\n              ],\n              cells: {\n                '1:2': {\n                  content: [{ insert: 'Hello' }],\n                  attributes: { align: 'center' },\n                },\n              },\n            },\n          },\n        },\n      ]);\n\n      const change = new Delta([\n        {\n          retain: { 'table-embed': { rows: [{ insert: { id: '55555555' } }] } },\n        },\n      ]);\n\n      expect(base.compose(change)).toEqual(\n        new Delta([\n          {\n            insert: {\n              'table-embed': {\n                rows: [\n                  { insert: { id: '55555555' } },\n                  { insert: { id: '11111111' }, attributes: { height: 20 } },\n                ],\n                columns: [\n                  { insert: { id: '22222222' } },\n                  { insert: { id: '33333333' }, attributes: { width: 30 } },\n                  { insert: { id: '44444444' } },\n                ],\n                cells: {\n                  '2:2': {\n                    content: [{ insert: 'Hello' }],\n                    attributes: { align: 'center' },\n                  },\n                },\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('adds two rows', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' }, attributes: { height: 20 } },\n              ],\n              columns: [\n                { insert: { id: '22222222' } },\n                { insert: { id: '33333333' }, attributes: { width: 30 } },\n                { insert: { id: '44444444' } },\n              ],\n              cells: {\n                '1:2': {\n                  content: [{ insert: 'Hello' }],\n                  attributes: { align: 'center' },\n                },\n              },\n            },\n          },\n        },\n      ]);\n\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '55555555' } },\n                { insert: { id: '66666666' } },\n              ],\n            },\n          },\n        },\n      ]);\n\n      expect(base.compose(change)).toEqual(\n        new Delta([\n          {\n            insert: {\n              'table-embed': {\n                rows: [\n                  { insert: { id: '55555555' } },\n                  { insert: { id: '66666666' } },\n                  { insert: { id: '11111111' }, attributes: { height: 20 } },\n                ],\n                columns: [\n                  { insert: { id: '22222222' } },\n                  { insert: { id: '33333333' }, attributes: { width: 30 } },\n                  { insert: { id: '44444444' } },\n                ],\n                cells: {\n                  '3:2': {\n                    content: [{ insert: 'Hello' }],\n                    attributes: { align: 'center' },\n                  },\n                },\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('adds a row and changes cell content', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' } },\n                { insert: { id: '22222222' }, attributes: { height: 20 } },\n              ],\n              columns: [\n                { insert: { id: '33333333' } },\n                { insert: { id: '44444444' }, attributes: { width: 30 } },\n                { insert: { id: '55555555' } },\n              ],\n              cells: {\n                '2:2': { content: [{ insert: 'Hello' }] },\n                '2:3': { content: [{ insert: 'World' }] },\n              },\n            },\n          },\n        },\n      ]);\n\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [{ insert: { id: '66666666' } }],\n              cells: {\n                '3:2': { attributes: { align: 'right' } },\n                '3:3': { content: [{ insert: 'Hello ' }] },\n              },\n            },\n          },\n        },\n      ]);\n\n      expect(base.compose(change)).toEqual(\n        new Delta([\n          {\n            insert: {\n              'table-embed': {\n                rows: [\n                  { insert: { id: '66666666' } },\n                  { insert: { id: '11111111' } },\n                  { insert: { id: '22222222' }, attributes: { height: 20 } },\n                ],\n                columns: [\n                  { insert: { id: '33333333' } },\n                  { insert: { id: '44444444' }, attributes: { width: 30 } },\n                  { insert: { id: '55555555' } },\n                ],\n                cells: {\n                  '3:2': {\n                    content: [{ insert: 'Hello' }],\n                    attributes: { align: 'right' },\n                  },\n                  '3:3': { content: [{ insert: 'Hello World' }] },\n                },\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('deletes a column', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' }, attributes: { height: 20 } },\n              ],\n              columns: [\n                { insert: { id: '22222222' } },\n                { insert: { id: '33333333' }, attributes: { width: 30 } },\n                { insert: { id: '44444444' } },\n              ],\n              cells: {\n                '1:2': {\n                  content: [{ insert: 'Hello' }],\n                  attributes: { align: 'center' },\n                },\n              },\n            },\n          },\n        },\n      ]);\n\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              columns: [{ retain: 1 }, { delete: 1 }],\n            },\n          },\n        },\n      ]);\n\n      expect(base.compose(change)).toEqual(\n        new Delta([\n          {\n            insert: {\n              'table-embed': {\n                rows: [\n                  { insert: { id: '11111111' }, attributes: { height: 20 } },\n                ],\n                columns: [\n                  { insert: { id: '22222222' } },\n                  { insert: { id: '44444444' } },\n                ],\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('removes a cell attributes', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              cells: { '1:2': { attributes: { align: 'center' } } },\n            },\n          },\n        },\n      ]);\n\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              cells: { '1:2': { attributes: { align: null } } },\n            },\n          },\n        },\n      ]);\n\n      expect(base.compose(change)).toEqual(\n        new Delta([{ insert: { 'table-embed': {} } }]),\n      );\n    });\n\n    test('removes all rows', () => {\n      const base = new Delta([\n        {\n          insert: { 'table-embed': { rows: [{ insert: { id: '11111111' } }] } },\n        },\n      ]);\n\n      const change = new Delta([\n        { retain: { 'table-embed': { rows: [{ delete: 1 }] } } },\n      ]);\n\n      expect(base.compose(change)).toEqual(\n        new Delta([{ insert: { 'table-embed': {} } }]),\n      );\n    });\n  });\n\n  describe('transform', () => {\n    test('transform rows and columns', () => {\n      const change1 = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' } },\n                { insert: { id: '22222222' } },\n                { insert: { id: '33333333' }, attributes: { height: 100 } },\n              ],\n              columns: [\n                { insert: { id: '44444444' }, attributes: { width: 100 } },\n                { insert: { id: '55555555' } },\n                { insert: { id: '66666666' } },\n              ],\n            },\n          },\n        },\n      ]);\n\n      const change2 = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [{ delete: 1 }, { retain: 1, attributes: { height: 50 } }],\n              columns: [\n                { delete: 1 },\n                { retain: 2, attributes: { width: 40 } },\n              ],\n            },\n          },\n        },\n      ]);\n\n      expect(change1.transform(change2)).toEqual(\n        new Delta([\n          {\n            retain: {\n              'table-embed': {\n                rows: [\n                  { retain: 3 },\n                  { delete: 1 },\n                  { retain: 1, attributes: { height: 50 } },\n                ],\n                columns: [\n                  { retain: 3 },\n                  { delete: 1 },\n                  { retain: 2, attributes: { width: 40 } },\n                ],\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('transform cells', () => {\n      const change1 = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [{ insert: { id: '22222222' } }],\n              cells: {\n                '8:1': {\n                  content: [{ insert: 'Hello 8:1!' }],\n                },\n                '21:2': {\n                  content: [{ insert: 'Hello 21:2!' }],\n                },\n              },\n            },\n          },\n        },\n      ]);\n\n      const change2 = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [{ delete: 1 }],\n              cells: {\n                '6:1': {\n                  content: [{ insert: 'Hello 6:1!' }],\n                },\n                '52:8': {\n                  content: [{ insert: 'Hello 52:8!' }],\n                },\n              },\n            },\n          },\n        },\n      ]);\n\n      expect(change1.transform(change2)).toEqual(\n        new Delta([\n          {\n            retain: {\n              'table-embed': {\n                rows: [{ retain: 1 }, { delete: 1 }],\n                cells: {\n                  '7:1': {\n                    content: [{ insert: 'Hello 6:1!' }],\n                  },\n                  '53:8': {\n                    content: [{ insert: 'Hello 52:8!' }],\n                  },\n                },\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('transform cell attributes', () => {\n      const change1 = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              cells: { '8:1': { attributes: { align: 'right' } } },\n            },\n          },\n        },\n      ]);\n\n      const change2 = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              cells: { '8:1': { attributes: { align: 'left' } } },\n            },\n          },\n        },\n      ]);\n\n      expect(change1.transform(change2)).toEqual(\n        new Delta([\n          {\n            retain: {\n              'table-embed': {\n                cells: { '8:1': { attributes: { align: 'left' } } },\n              },\n            },\n          },\n        ]),\n      );\n\n      expect(change1.transform(change2, true)).toEqual(\n        new Delta([{ retain: { 'table-embed': {} } }]),\n      );\n    });\n  });\n\n  describe('invert', () => {\n    test('reverts rows and columns', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' } },\n                { insert: { id: '22222222' } },\n              ],\n              columns: [\n                { insert: { id: '33333333' } },\n                { insert: { id: '44444444' }, attributes: { width: 100 } },\n              ],\n            },\n          },\n        },\n      ]);\n\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [{ delete: 1 }],\n              columns: [{ retain: 1 }, { delete: 1 }],\n            },\n          },\n        },\n      ]);\n\n      expect(change.invert(base)).toEqual(\n        new Delta([\n          {\n            retain: {\n              'table-embed': {\n                rows: [{ insert: { id: '11111111' } }],\n                columns: [\n                  { retain: 1 },\n                  { insert: { id: '44444444' }, attributes: { width: 100 } },\n                ],\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('inverts cell content', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' } },\n                { insert: { id: '22222222' } },\n              ],\n              columns: [\n                { insert: { id: '33333333' } },\n                { insert: { id: '44444444' } },\n              ],\n              cells: {\n                '1:2': {\n                  content: [{ insert: 'Hello 1:2' }],\n                  attributes: { align: 'center' },\n                },\n              },\n            },\n          },\n        },\n      ]);\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              rows: [{ insert: { id: '55555555' } }],\n              cells: {\n                '2:2': {\n                  content: [{ retain: 6 }, { insert: '2' }, { delete: 1 }],\n                },\n              },\n            },\n          },\n        },\n      ]);\n      expect(change.invert(base)).toEqual(\n        new Delta([\n          {\n            retain: {\n              'table-embed': {\n                rows: [{ delete: 1 }],\n                cells: {\n                  '1:2': {\n                    content: [{ retain: 6 }, { insert: '1' }, { delete: 1 }],\n                  },\n                },\n              },\n            },\n          },\n        ]),\n      );\n    });\n\n    test('inverts cells removed by row/column delta', () => {\n      const base = new Delta([\n        {\n          insert: {\n            'table-embed': {\n              rows: [\n                { insert: { id: '11111111' } },\n                { insert: { id: '22222222' } },\n              ],\n              columns: [\n                { insert: { id: '33333333' } },\n                { insert: { id: '44444444' } },\n              ],\n              cells: {\n                '1:2': {\n                  content: [{ insert: 'content' }],\n                  attributes: { align: 'center' },\n                },\n              },\n            },\n          },\n        },\n      ]);\n      const change = new Delta([\n        {\n          retain: {\n            'table-embed': {\n              columns: [{ retain: 1 }, { delete: 1 }],\n            },\n          },\n        },\n      ]);\n      expect(change.invert(base)).toEqual(\n        new Delta([\n          {\n            retain: {\n              'table-embed': {\n                columns: [{ retain: 1 }, { insert: { id: '44444444' } }],\n                cells: {\n                  '1:2': {\n                    content: [{ insert: 'content' }],\n                    attributes: { align: 'center' },\n                  },\n                },\n              },\n            },\n          },\n        ]),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/toolbar.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport Quill from '../../../src/core/quill.js';\nimport Toolbar, { addControls } from '../../../src/modules/toolbar.js';\nimport { normalizeHTML } from '../__helpers__/utils.js';\nimport SnowTheme from '../../../src/themes/snow.js';\nimport Clipboard from '../../../src/modules/clipboard.js';\nimport Keyboard from '../../../src/modules/keyboard.js';\nimport History from '../../../src/modules/history.js';\nimport Uploader from '../../../src/modules/uploader.js';\nimport { createRegistry } from '../__helpers__/factory.js';\nimport Input from '../../../src/modules/input.js';\nimport { SizeClass } from '../../../src/formats/size.js';\nimport Bold from '../../../src/formats/bold.js';\nimport Link from '../../../src/formats/link.js';\nimport { AlignClass } from '../../../src/formats/align.js';\nimport UINode from '../../../src/modules/uiNode.js';\n\nconst createContainer = (html = '') => {\n  const container = document.body.appendChild(document.createElement('div'));\n  container.innerHTML = normalizeHTML(html);\n  return container;\n};\n\ndescribe('Toolbar', () => {\n  describe('add controls', () => {\n    test('single level', () => {\n      const container = createContainer();\n      addControls(container, ['bold', 'italic']);\n      expect(container).toEqualHTML(`\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"bold\" class=\"ql-bold\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"italic\" class=\"ql-italic\" aria-pressed=\"false\"></button>\n        </span>\n      `);\n    });\n\n    test('nested group', () => {\n      const container = createContainer();\n      addControls(container, [\n        ['bold', 'italic'],\n        ['underline', 'strike'],\n      ]);\n      expect(container).toEqualHTML(`\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"bold\" class=\"ql-bold\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"italic\" class=\"ql-italic\" aria-pressed=\"false\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"underline\" class=\"ql-underline\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"strike\" class=\"ql-strike\" aria-pressed=\"false\"></button>\n        </span>\n      `);\n    });\n\n    test('button value', () => {\n      const container = createContainer();\n      addControls(container, ['bold', { header: '2' }]);\n      expect(container).toEqualHTML(`\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"bold\" class=\"ql-bold\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"header: 2\" class=\"ql-header\" aria-pressed=\"false\" value=\"2\"></button>\n        </span>\n      `);\n    });\n\n    test('select', () => {\n      const container = createContainer();\n      addControls(container, [{ size: ['10px', false, '18px', '32px'] }]);\n      expect(container).toEqualHTML(`\n        <span class=\"ql-formats\">\n          <select class=\"ql-size\">\n            <option value=\"10px\"></option>\n            <option selected=\"selected\"></option>\n            <option value=\"18px\"></option>\n            <option value=\"32px\"></option>\n          </select>\n        </span>\n      `);\n    });\n\n    test('everything', () => {\n      const container = createContainer();\n      addControls(container, [\n        [\n          { font: [false, 'sans-serif', 'monospace'] },\n          { size: ['10px', false, '18px', '32px'] },\n        ],\n        ['bold', 'italic', 'underline', 'strike'],\n        [\n          { list: 'ordered' },\n          { list: 'bullet' },\n          { align: [false, 'center', 'right', 'justify'] },\n        ],\n        ['link', 'image'],\n      ]);\n      expect(container).toEqualHTML(`\n        <span class=\"ql-formats\">\n          <select class=\"ql-font\">\n            <option selected=\"selected\"></option>\n            <option value=\"sans-serif\"></option>\n            <option value=\"monospace\"></option>\n          </select>\n          <select class=\"ql-size\">\n            <option value=\"10px\"></option>\n            <option selected=\"selected\"></option>\n            <option value=\"18px\"></option>\n            <option value=\"32px\"></option>\n          </select>\n        </span>\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"bold\" class=\"ql-bold\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"italic\" class=\"ql-italic\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"underline\" class=\"ql-underline\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"strike\" class=\"ql-strike\" aria-pressed=\"false\"></button>\n        </span>\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"list: ordered\" class=\"ql-list\" value=\"ordered\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"list: bullet\" class=\"ql-list\" value=\"bullet\" aria-pressed=\"false\"></button>\n          <select class=\"ql-align\">\n            <option selected=\"selected\"></option>\n            <option value=\"center\"></option>\n            <option value=\"right\"></option>\n            <option value=\"justify\"></option>\n          </select>\n        </span>\n        <span class=\"ql-formats\">\n          <button type=\"button\" aria-label=\"link\" class=\"ql-link\" aria-pressed=\"false\"></button>\n          <button type=\"button\" aria-label=\"image\" class=\"ql-image\" aria-pressed=\"false\"></button>\n        </span>\n      `);\n    });\n  });\n\n  describe('active', () => {\n    const setup = () => {\n      const container = createContainer(\n        `\n        <p>0123</p>\n        <p><strong>5678</strong></p>\n        <p><a href=\"http://quilljs.com/\">0123</a></p>\n        <p class=\"ql-align-center\">5678</p>\n        <p><span class=\"ql-size-small\">01</span><span class=\"ql-size-large\">23</span></p>\n      `,\n      );\n\n      Quill.register(\n        {\n          'themes/snow': SnowTheme,\n          'modules/toolbar': Toolbar,\n          'modules/clipboard': Clipboard,\n          'modules/keyboard': Keyboard,\n          'modules/history': History,\n          'modules/uploader': Uploader,\n          'modules/input': Input,\n          'modules/uiNode': UINode,\n        },\n        true,\n      );\n      const quill = new Quill(container, {\n        modules: {\n          toolbar: [\n            ['bold', 'link'],\n            [{ size: ['small', false, 'large'] }],\n            [{ align: '' }, { align: 'center' }],\n          ],\n        },\n        theme: 'snow',\n        registry: createRegistry([SizeClass, Bold, AlignClass, Link]),\n      });\n      return { container, quill };\n    };\n\n    test('toggle button', () => {\n      const { container, quill } = setup();\n      const boldButton = container.parentNode?.querySelector(\n        'button.ql-bold',\n      ) as HTMLButtonElement;\n      quill.setSelection(7);\n      expect(boldButton.classList.contains('ql-active')).toBe(true);\n      expect(boldButton.getAttribute('aria-pressed')).toBe('true');\n      quill.setSelection(2);\n      expect(boldButton.classList.contains('ql-active')).toBe(false);\n      expect(boldButton.getAttribute('aria-pressed')).toBe('false');\n    });\n\n    test('link', () => {\n      const { container, quill } = setup();\n      const linkButton = container.parentNode?.querySelector(\n        'button.ql-link',\n      ) as HTMLButtonElement;\n      quill.setSelection(12);\n      expect(linkButton.classList.contains('ql-active')).toBe(true);\n      expect(linkButton.getAttribute('aria-pressed')).toBe('true');\n      quill.setSelection(2);\n      expect(linkButton.classList.contains('ql-active')).toBe(false);\n      expect(linkButton.getAttribute('aria-pressed')).toBe('false');\n    });\n\n    test('dropdown', () => {\n      const { container, quill } = setup();\n      const sizeSelect = container.parentNode?.querySelector(\n        'select.ql-size',\n      ) as HTMLSelectElement;\n      quill.setSelection(21);\n      expect(sizeSelect.selectedIndex).toEqual(0);\n      quill.setSelection(23);\n      expect(sizeSelect.selectedIndex).toEqual(2);\n      quill.setSelection(21, 2);\n      expect(sizeSelect.selectedIndex).toBeLessThan(0);\n      quill.setSelection(2);\n      expect(sizeSelect.selectedIndex).toEqual(1);\n    });\n\n    test('custom button', () => {\n      const { container, quill } = setup();\n      const centerButton = container.parentNode?.querySelector(\n        'button.ql-align[value=\"center\"]',\n      ) as HTMLButtonElement;\n      const leftButton = container.parentNode?.querySelector(\n        'button.ql-align[value]',\n      ) as HTMLButtonElement;\n      quill.setSelection(17);\n      expect(centerButton.classList.contains('ql-active')).toBe(true);\n      expect(leftButton.classList.contains('ql-active')).toBe(false);\n      expect(centerButton.getAttribute('aria-pressed')).toBe('true');\n      expect(leftButton.getAttribute('aria-pressed')).toBe('false');\n      quill.setSelection(2);\n      expect(centerButton.classList.contains('ql-active')).toBe(false);\n      expect(leftButton.classList.contains('ql-active')).toBe(true);\n      expect(centerButton.getAttribute('aria-pressed')).toBe('false');\n      expect(leftButton.getAttribute('aria-pressed')).toBe('true');\n      quill.blur();\n      expect(centerButton.classList.contains('ql-active')).toBe(false);\n      expect(leftButton.classList.contains('ql-active')).toBe(false);\n      expect(centerButton.getAttribute('aria-pressed')).toBe('false');\n      expect(leftButton.getAttribute('aria-pressed')).toBe('false');\n    });\n\n    test('update on format', () => {\n      const { container, quill } = setup();\n      const boldButton = container?.parentNode?.querySelector('button.ql-bold');\n      quill.setSelection(1, 2);\n      expect(boldButton?.classList.contains('ql-active')).toBe(false);\n      quill.format('bold', true, 'user');\n      expect(boldButton?.classList.contains('ql-active')).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/modules/uiNode.spec.ts",
    "content": "import '../../../src/quill.js';\nimport { describe, expect, test } from 'vitest';\nimport UINode, {\n  TTL_FOR_VALID_SELECTION_CHANGE,\n} from '../../../src/modules/uiNode.js';\nimport Quill, { Delta } from '../../../src/core.js';\n\n// Fake timer is not supported in browser mode yet.\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\ndescribe('uiNode', () => {\n  test('extends deadline when multiple possible shortcuts are pressed', async () => {\n    const quill = new Quill(document.createElement('div'));\n    document.body.appendChild(quill.container);\n    quill.setContents(\n      new Delta().insert('item 1').insert('\\n', { list: 'bullet' }),\n    );\n    new UINode(quill, {});\n\n    for (let i = 0; i < 2; i += 1) {\n      quill.root.dispatchEvent(\n        new KeyboardEvent('keydown', { key: 'ArrowRight', metaKey: true }),\n      );\n      await delay(TTL_FOR_VALID_SELECTION_CHANGE / 2);\n    }\n\n    quill.root.dispatchEvent(\n      new KeyboardEvent('keydown', { key: 'ArrowLeft', metaKey: true }),\n    );\n    const range = document.createRange();\n    range.setStart(quill.root.querySelector('li')!, 0);\n    range.setEnd(quill.root.querySelector('li')!, 0);\n\n    const selection = document.getSelection();\n    selection?.removeAllRanges();\n    selection?.addRange(range);\n\n    await delay(TTL_FOR_VALID_SELECTION_CHANGE / 2);\n    expect(selection?.getRangeAt(0).startOffset).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/theme/base/tooltip.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport Quill from '../../../../src/core.js';\nimport Video from '../../../../src/formats/video.js';\nimport { BaseTooltip } from '../../../../src/themes/base.js';\nimport { createRegistry } from '../../__helpers__/factory.js';\n\nclass Tooltip extends BaseTooltip {\n  static TEMPLATE = '<input type=\"text\">';\n}\n\ndescribe('BaseTooltip', () => {\n  const setup = () => {\n    const container = document.body.appendChild(document.createElement('div'));\n    const quill = new Quill(container, { registry: createRegistry([Video]) });\n    const tooltip = new Tooltip(quill);\n    return { container, tooltip };\n  };\n\n  const insertVideo = (tooltip: Tooltip, url: string) => {\n    (tooltip.textbox as HTMLInputElement).value = url;\n    tooltip.root.setAttribute('data-mode', 'video');\n    tooltip.save();\n  };\n\n  describe('save', () => {\n    test('converts youtube video url to embedded', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'http://youtube.com/watch?v=QHH3iSeDBLo');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('http://www.youtube.com/embed/QHH3iSeDBLo');\n    });\n\n    test('converts www.youtube video url to embedded', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'http://www.youtube.com/watch?v=QHH3iSeDBLo');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('http://www.youtube.com/embed/QHH3iSeDBLo');\n    });\n\n    test('converts m.youtube video url to embedded', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'http://m.youtube.com/watch?v=QHH3iSeDBLo');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('http://www.youtube.com/embed/QHH3iSeDBLo');\n    });\n\n    test('preserves youtube video url protocol', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'https://m.youtube.com/watch?v=QHH3iSeDBLo');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('https://www.youtube.com/embed/QHH3iSeDBLo');\n    });\n\n    test('uses https as default youtube video url protocol', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'youtube.com/watch?v=QHH3iSeDBLo');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('https://www.youtube.com/embed/QHH3iSeDBLo');\n    });\n\n    test('converts vimeo video url to embedded', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'http://vimeo.com/47762693');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('http://player.vimeo.com/video/47762693/');\n    });\n\n    test('converts www.vimeo video url to embedded', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'http://www.vimeo.com/47762693');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('http://player.vimeo.com/video/47762693/');\n    });\n\n    test('preserves vimeo video url protocol', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'https://www.vimeo.com/47762693');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('https://player.vimeo.com/video/47762693/');\n    });\n\n    test('uses https as default vimeo video url protocol', () => {\n      const { container, tooltip } = setup();\n      insertVideo(tooltip, 'vimeo.com/47762693');\n      expect(\n        (container.querySelector('.ql-video') as HTMLVideoElement).src,\n      ).toContain('https://player.vimeo.com/video/47762693/');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/ui/picker.spec.ts",
    "content": "import { describe, expect, test } from 'vitest';\nimport Picker from '../../../src/ui/picker.js';\n\ndescribe('Picker', () => {\n  const setup = () => {\n    const container = document.body.appendChild(document.createElement('div'));\n    container.innerHTML =\n      '<select><option selected>0</option><option value=\"1\">1</option></select>';\n    const pickerSelectorInstance = new Picker(\n      container.firstChild as HTMLSelectElement,\n    );\n    const pickerSelector = container.querySelector('.ql-picker') as HTMLElement;\n    return { container, pickerSelectorInstance, pickerSelector };\n  };\n\n  test('initialization', () => {\n    const { container } = setup();\n    expect(container.querySelector('.ql-picker')).toBeTruthy();\n    expect(container.querySelector('.ql-active')).toBeFalsy();\n    expect(\n      container.querySelector('.ql-picker-item.ql-selected')?.outerHTML,\n    ).toEqualHTML(\n      '<span tabindex=\"0\" role=\"button\" class=\"ql-picker-item ql-selected\" data-label=\"0\"></span>',\n    );\n    expect(\n      container.querySelector('.ql-picker-item:not(.ql-selected)')?.outerHTML,\n    ).toEqualHTML(\n      '<span tabindex=\"0\" role=\"button\" class=\"ql-picker-item\" data-value=\"1\" data-label=\"1\"></span>',\n    );\n  });\n\n  test('escape charcters', () => {\n    const { container } = setup();\n    const select = document.createElement('select');\n    const option = document.createElement('option');\n    container.appendChild(select);\n    select.appendChild(option);\n    let value = '\"Helvetica Neue\", \\'Helvetica\\', sans-serif';\n    option.value = value;\n    value = value.replace(/\"/g, '\\\\\"');\n    expect(select.querySelector(`option[value=\"${value}\"]`)).toEqual(option);\n  });\n\n  test('label is initialized with the correct aria attributes', () => {\n    const { pickerSelector } = setup();\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-label')\n        ?.getAttribute('aria-expanded'),\n    ).toEqual('false');\n    const optionsId = pickerSelector.querySelector('.ql-picker-options')?.id;\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-label')\n        ?.getAttribute('aria-controls'),\n    ).toEqual(optionsId);\n  });\n\n  test('options container is initialized with the correct aria attributes', () => {\n    const { pickerSelector } = setup();\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('true');\n\n    const ariaControlsLabel = pickerSelector\n      .querySelector('.ql-picker-label')\n      ?.getAttribute('aria-controls');\n    expect(pickerSelector.querySelector('.ql-picker-options')?.id).toEqual(\n      ariaControlsLabel,\n    );\n    expect(\n      (pickerSelector.querySelector('.ql-picker-options') as HTMLSelectElement)\n        .tabIndex,\n    ).toEqual(-1);\n  });\n\n  test('aria attributes toggle correctly when the picker is opened via enter key', () => {\n    const { pickerSelector } = setup();\n    const pickerLabel = pickerSelector.querySelector('.ql-picker-label');\n    pickerLabel?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));\n    expect(pickerLabel?.getAttribute('aria-expanded')).toEqual('true');\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('false');\n  });\n\n  test('aria attributes toggle correctly when the picker is opened via mousedown', () => {\n    const { pickerSelector } = setup();\n    const pickerLabel = pickerSelector.querySelector('.ql-picker-label');\n    pickerLabel?.dispatchEvent(\n      new Event('mousedown', {\n        bubbles: true,\n        cancelable: true,\n      }),\n    );\n\n    expect(pickerLabel?.getAttribute('aria-expanded')).toEqual('true');\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('false');\n  });\n\n  test('aria attributes toggle correctly when an item is selected via click', () => {\n    const { pickerSelector } = setup();\n    const pickerLabel = pickerSelector.querySelector(\n      '.ql-picker-label',\n    ) as HTMLElement;\n    pickerLabel.click();\n\n    const pickerItem = pickerSelector.querySelector(\n      '.ql-picker-item',\n    ) as HTMLElement;\n    pickerItem.click();\n\n    expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('true');\n  });\n\n  test('aria attributes toggle correctly when an item is selected via enter', () => {\n    const { pickerSelector } = setup();\n    const pickerLabel = pickerSelector.querySelector(\n      '.ql-picker-label',\n    ) as HTMLElement;\n    pickerLabel.click();\n    const pickerItem = pickerSelector.querySelector(\n      '.ql-picker-item',\n    ) as HTMLElement;\n    pickerItem.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));\n    expect(pickerLabel?.getAttribute('aria-expanded')).toEqual('false');\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('true');\n  });\n\n  test('aria attributes toggle correctly when the picker is closed via clicking on the label again', () => {\n    const { pickerSelector } = setup();\n    const pickerLabel = pickerSelector.querySelector(\n      '.ql-picker-label',\n    ) as HTMLElement;\n    pickerLabel.click();\n    pickerLabel.click();\n    expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('true');\n  });\n\n  test('aria attributes toggle correctly when the picker is closed via escaping out of it', () => {\n    const { pickerSelector } = setup();\n    const pickerLabel = pickerSelector.querySelector(\n      '.ql-picker-label',\n    ) as HTMLElement;\n    pickerLabel.click();\n    pickerLabel.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));\n    expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');\n    expect(\n      pickerSelector\n        .querySelector('.ql-picker-options')\n        ?.getAttribute('aria-hidden'),\n    ).toEqual('true');\n  });\n});\n"
  },
  {
    "path": "packages/quill/test/unit/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\nimport { resolve } from 'path';\n\nexport default defineConfig({\n  resolve: {\n    extensions: ['.ts', '.js'],\n  },\n  test: {\n    include: [resolve(__dirname, '**/*.spec.ts')],\n    typecheck: {\n      enabled: true,\n      include: [resolve(__dirname, '**/*.test-d.ts')],\n    },\n    setupFiles: [\n      resolve(__dirname, '__helpers__/expect.ts'),\n      resolve(__dirname, '__helpers__/cleanup.ts'),\n    ],\n    browser: {\n      enabled: true,\n      provider: 'playwright',\n      name: process.env.BROWSER || 'chromium',\n    },\n  },\n});\n"
  },
  {
    "path": "packages/quill/tsconfig.json",
    "content": "{\n  \"ts-node\": {\n    \"compilerOptions\": {\n      \"esModuleInterop\": true\n    }\n  },\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"resolveJsonModule\": true,\n    \"declaration\": false,\n    \"module\": \"ES2020\",\n    \"moduleResolution\": \"bundler\",\n    \"strictNullChecks\": true,\n    \"noImplicitAny\": true,\n    \"noUnusedLocals\": true\n  },\n  \"include\": [\"src/**/*\", \"test/**/*\"]\n}\n"
  },
  {
    "path": "packages/quill/webpack.common.cjs",
    "content": "/*eslint-env node*/\n\nconst { resolve } = require('path');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\n\nconst tsRules = {\n  test: /\\.ts$/,\n  include: [resolve(__dirname, 'src')],\n  use: ['babel-loader'],\n};\n\nconst sourceMapRules = {\n  test: /\\.js$/,\n  enforce: 'pre',\n  use: ['source-map-loader'],\n};\n\nconst svgRules = {\n  test: /\\.svg$/,\n  include: [resolve(__dirname, 'src/assets/icons')],\n  use: [\n    {\n      loader: 'html-loader',\n      options: {\n        minimize: true,\n      },\n    },\n  ],\n};\n\nconst stylRules = {\n  test: /\\.styl$/,\n  include: [resolve(__dirname, 'src/assets')],\n  use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader'],\n};\n\nmodule.exports = {\n  entry: {\n    quill: './src/quill.ts',\n    'quill.core': './src/core.ts',\n    'quill.core.css': './src/assets/core.styl',\n    'quill.bubble.css': './src/assets/bubble.styl',\n    'quill.snow.css': './src/assets/snow.styl',\n  },\n  output: {\n    filename: '[name].js',\n    library: {\n      name: 'Quill',\n      type: 'umd',\n      export: 'default',\n    },\n    path: resolve(__dirname, 'dist/dist'),\n    clean: true,\n  },\n  resolve: {\n    extensions: ['.js', '.styl', '.ts'],\n    extensionAlias: {\n      '.js': ['.ts', '.js'],\n    },\n  },\n  module: {\n    rules: [tsRules, stylRules, svgRules, sourceMapRules],\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: '[name]',\n    }),\n  ],\n};\n"
  },
  {
    "path": "packages/quill/webpack.config.cjs",
    "content": "/*eslint-env node*/\n\nconst { BannerPlugin, DefinePlugin } = require('webpack');\nconst common = require('./webpack.common.cjs');\nconst { merge } = require('webpack-merge');\nrequire('webpack-dev-server');\nconst { readFileSync } = require('fs');\nconst { join, resolve } = require('path');\n\nconst pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));\n\nconst bannerPack = new BannerPlugin({\n  banner: [\n    `Quill Editor v${pkg.version}`,\n    pkg.homepage,\n    `Copyright (c) 2017-${new Date().getFullYear()}, Slab`,\n    'Copyright (c) 2014, Jason Chen',\n    'Copyright (c) 2013, salesforce.com',\n  ].join('\\n'),\n  entryOnly: true,\n});\nconst constantPack = new DefinePlugin({\n  QUILL_VERSION: JSON.stringify(pkg.version),\n});\n\nmodule.exports = (env) =>\n  merge(common, {\n    mode: env.production ? 'production' : 'development',\n    devtool: 'source-map',\n    plugins: [bannerPack, constantPack],\n    devServer: {\n      static: {\n        directory: resolve(__dirname, './dist'),\n      },\n      hot: false,\n      allowedHosts: 'all',\n      devMiddleware: {\n        stats: 'minimal',\n      },\n    },\n    stats: 'minimal',\n  });\n"
  },
  {
    "path": "packages/website/.eslintrc.json",
    "content": "{\n  \"extends\": \"next\"\n}\n"
  },
  {
    "path": "packages/website/.gitignore",
    "content": "node_modules\n\n.next\nout\ncertificates"
  },
  {
    "path": "packages/website/README.md",
    "content": "# Quill Official Website\n\n[https://quilljs.com](https://quilljs.com)\n"
  },
  {
    "path": "packages/website/content/blog/a-new-delta.mdx",
    "content": "---\ntitle: A New Delta\ndate: 2014-09-29\n---\n\nPart of providing a complete API in Quill is providing events for when and what changes occur in the editor. Those changes are currently represented by a [Delta](/docs/delta/) object, which aims to be intuitive, human-readable, and expressive for any change or document that might need to represented. Over the past few weeks I’ve been working on a new format that better fulfills those goals and addresses the challenges in the current format.\n\nDocumentation for the new Delta format can be found in its own [GitHub repository](https://github.com/ottypes/rich-text) but I will go over some of the rationale behind some of the changes in this post.\n\n{/* more */}\n\n### Reduced Complexity\n\nWhen the Delta format was originally designed, it had ambitious goals of being general purpose and being able to represent any kind of document. The new format reduces the scope to just rich text documents, allowing for a much tighter implementation[^1].\n\nQuill is not specifically built to be a collaborative editor but the ability to be used as one is a good benchmark of the API. The new Delta format maintains this capability and fulfills the specifications of an [ottype](https://github.com/ottypes/docs), making it compatible with [ShareJS](https://github.com/share/ShareJS).\n\n### Explicit Deletes\n\nIn the current Delta format, a delete operation is implied by a lack of a retain operation. Basically everything is deleted unless you say it should be kept. This has some nice properties from an implementation perspective[^2] but was probably the largest source of confusion for users trying to work with Deltas and challenged the human-readability goal. It is very difficult to keep track of indexes to figure out what was not accounted for, to figure out what should be deleted.\n\nThe new format has an explicit delete operation and by default everything is kept. Here’s a comparison of the two formats both representing removing the ‘b’ in ‘abc’.\n\n```javascript\nvar oldFormat = {\n  startLength: 3,\n  endLength: 2,\n  ops: [\n    { start: 0, end: 1 },\n    { start: 2, end: 3 }\n  ]\n};\n\nvar newFormat = {\n  ops: [\n    { retain: 1 },\n    { delete: 1 }\n  ]\n};\n```\n\nA side effect of having explicit deletes and defaulting to keeping text is that in practice the representation for new Deltas will usually be smaller.\n\n### Embed Support\n\nThe new Delta format provides native support for embeds, which can be used to represent images, video, etc. There is no support for this in the current format and implementation is hackily achieved by representing an ‘!’ with a image key in the attributes (which will break when video support is added).\n\n```javascript\nvar oldFormat = {\n  startLength: 0,\n  endLength: 1,\n  ops: [{\n    text: '!',\n    attributes: { image: 'https://octodex.github.com/images/labtocat.png' }\n  }]\n};\n\nvar newFormat = {\n  ops: [{\n    insert: 1, attributes: { image: 'https://octodex.github.com/images/labtocat.png' }\n  }]\n};\n```\n\n### Going Forward\n\nThis new format will be the finalized representation for changes and state in Quill going forward and is one of the major steps toward a 1.0 release (a topic for another post).\n\n[^1]: Currently 28658 vs 9507 lines of code (though in practice is less relevant due to minification and gzip).\n[^2]: Minimizes number of operations to support, and easy to calculate the length of text of the resulting document which is useful for sanity checks.\n"
  },
  {
    "path": "packages/website/content/blog/an-official-cdn-for-quill.mdx",
    "content": "---\ntitle: An Offical CDN for Quill\ndate: 2014-08-12\n---\n\nQuill now has an offical Content Distribution Network so you can have access to a reliable, high-speed host for the library. To include a file:\n\n```html\n<link rel=\"stylesheet\" href=\"//cdn.quilljs.com/0.16.0/quill.snow.css\" />\n```\n\n```html\n<script src=\"//cdn.quilljs.com/0.16.0/quill.min.js\"></script>\n```\n\nYou can also use \"latest\" as the version:\n\n```html\n<script src=\"//cdn.quilljs.com/latest/quill.min.js\"></script>\n```\n"
  },
  {
    "path": "packages/website/content/blog/announcing-quill-1-0.mdx",
    "content": "---\ntitle: Announcing Quill 1.0\ndate: 2016-09-06\n---\n\nQuill 1.0 has arrived. It was just two years ago that Quill made its public debut as an open source project. Today, it can be found in applications and products of all sizes, ranging from personal projects and promising startups, to established public companies.\n\n**Quill is designed as an easy to use editor, to support content creation across the web.** It is built on top of consistent and predictable constructs, exposed through a powerful [API](/docs/api). With coverage across both ends of the complexity spectrum, Quill aims to be the defacto rich text editor for the web.\n\nIn the 111 releases, Quill has iterated relentlessly towards this goal. It has stabilized its API, moved essential internal implementations into customizable modules, and exposed its document model for users to define and add entirely new formats and content.\n\n1. [New Features](/blog/announcing-quill-1-0/#new-features)\n2. [Parchment](/blog/announcing-quill-1-0/#parchment)\n3. [Website](/blog/announcing-quill-1-0/#website)\n4. [What's Next](/blog/announcing-quill-1-0/#whats-next)\n5. [Getting Started](/blog/announcing-quill-1-0/#getting-started)\n6. [Credits](/blog/announcing-quill-1-0/#credits)\n\n{/* more */}\n\n### New Features\n\nEditable [Syntax Highlighted Code](/docs/modules/syntax/) blocks can now seamlessly exist inline with the rest of your text.\n\n![Syntax Highlighted Code](/assets/images/blog/syntax.png)\n\nBeautifully rendered [LaTeX formulas](/docs/modules/formula/) can be inserted to your contents.\n\n![Formula](/assets/images/blog/formula.png)\n\n**Many, many new formats** have been added, including superscript, subscript, inline code, code blocks, headers, blockquotes, text direction, videos, and nested lists. Inline styles can now also use classes [instead](/playground/#class-vs-inline-style).\n\nAn entirely new theme [Bubble](/docs/themes/#bubble) has also been added, based on a floating toolbar.\n\n![Bubble Theme](/assets/images/blog/bubble.png)\n\nTake a look at the [Changelog](https://github.com/quilljs/quill/blob/develop/CHANGELOG.md) to see an exhaustive list.\n\n### Parchment\n\nAs the biggest feature in Quill 1.0, [Parchment](https://github.com/quilljs/parchment/) deserves its own mention. It offers a powerful abstraction over the DOM to enable custom formats and content in Quill. It is responsible for the numerous new formats added in Quill 1.0, including videos, syntax highlighted code, and formulas.\n\nWith Parchment, you can now enhance or customize existing Quill formats or add entirely new ones in your own application. Take a look at [Cloning Medium with Parchment](/guides/cloning-medium-with-parchment/) for a detailed guide.\n\n### Website\n\nIn addition to the fancy new features, Quill's documentation site has also been given an upgrade. The [referential](/docs/) portions have all been filled out, with no more incomplete or unstable pages. A new [Guides](/guides/) section has been added to cover some of the common how-tos and whys behind design decisions.\n\nThere is now also an [Interactive Playground](/playground/), for tinkerers and immediate gratification. You can use it try out Quill's features and experiment with its API, without any setup or installation.\n\n### What's Next\n\nQuill will continue to refine itself in being the easiest editor to use, while allowing for the most sophisticated customizations. With a stable and solid foundation on 1.0, some areas of focus will include:\n\n- Iterating on modules and themes, making them easier to create, customize and share\n- Add remaining common formats, such as tables\n- Better internationalization support\n\n### Getting Started\n\nIf you are just joining us today, the example editors on the [homepage](/) are an excellent demonstration of the gorgeous editing experiences you can add to your application. Make sure to open up your developer console to play around with the API.\n\nHead to the [Interactive Playground](/playground) if you want to tinker some more or to the [Quickstart](/docs/quickstart) docs if you are ready to add Quill, with just a few lines of code.\n\n### Credits\n\nLast, but not least, **a special thank you goes out to Quill's community.** Whether you contributed code for the codebase, help for other community members, reports or context for bugs, ideas for new features or improve existing ones, feedback on use cases, or evangelism for the project, Quill would not be nearly as successful without your collective efforts. Thank you for your contribution!\n"
  },
  {
    "path": "packages/website/content/blog/are-we-there-yet-to-1-0.mdx",
    "content": "---\ntitle: Are We There Yet (to 1.0)?\ndate: 2016-03-14\n---\n\nWhen Quill laid out its [1.0 roadmap](/blog/the-road-to-1-0/), core to its journey was the development of a new document model called Parchment. Today a beta release of Parchment is being made available on [GitHub](https://github.com/quilljs/parchment) and [NPM](https://www.npmjs.com/package/parchment).\n\nWhat this means is its design and API is reasonably stable and the adventurous can now take an early look. The latest Quill source is already using Parchment to implement its formatting and content capabilities, and its [integration](https://github.com/quilljs/quill/tree/develop/formats) would be a helpful example of Parchment in action. Of course, this is in addition to Parchment’s own [documentation](https://github.com/quilljs/parchment/blob/master/README.md).\n\n{/* more */}\n\n### New Formats\n\nParchment enables Quill to scalably support many formats and many are being added in 1.0. The list includes headers, blockquotes, code, superscript, subscript, text direction, nested lists, and video embeds. Syntax highlighted code and formulas will also be available through externally supported modules. In addition, formats that previously relied on style attributes are reimplemented to optionally use classes instead. By default, fonts, sizes, and text alignment will use classes, while foreground and background colors will still use style attributes, since there are so many possible color values.\n\n### Quill 1.0 Beta\n\nWith Parchment out of the way, Quill is nearing its own 1.0 release. This will also be prefaced with a beta period, optimistically planned for early April. You can also set up the [local development](https://github.com/quilljs/quill/blob/develop/.github/CONTRIBUTING.md#local-development) environment to follow along with the latest changes and progress.\n\nIf you are currently using Quill at your company or project, we'd love to hear about your use case [hello@quilljs.com](mailto:hello@quilljs.com)!\n"
  },
  {
    "path": "packages/website/content/blog/quill-1-0-beta-release.mdx",
    "content": "---\ntitle: Quill 1.0 Beta Release\ndate: 2016-05-03\n---\n\nToday Quill is ready for its first beta preview of 1.0. This is the biggest rewrite to Quill since its inception and enables many new possibilities not available in previous versions of Quill, nor any other editor. The code is as always available on GitHub and through npm:\n\n```\nnpm install quill@1.0.0-beta.0\n```\n\nThe skeleton of a new documentation site is also being built out at [beta.quilljs.com](https://beta.quilljs.com). Whereas the current site focuses on being a referential resource, the new site will also be a guide to provide insight on approaching different customization goals. There is also an [interactive playground](https://beta.quilljs.com/playground/) to try out various configurations and explore the API.\n\n{/* more */}\n\nThe goal now is of course an official 1.0 release. To get there, Quill will now enter a weekly cadence of beta releases, so you can expect rapid interations on stability and bug fixes each week. GitHub is still the center of all development so please do report [Issues](https://github.com/quilljs/quill/issues) as you encounter them in the beta preview.\n"
  },
  {
    "path": "packages/website/content/blog/quill-1-0-release-candidate-released.mdx",
    "content": "---\ntitle: Quill 1.0 Release Candidate... Released!\ndate: 2016-08-18\n---\n\nToday Quill enters its highly anticipated 1.0 release candidacy. Through the 11 beta releases, over 300 reported bugs were fixed, and almost 1000 commits were made. **Thank you to all contributors who pitched in, in ways small and large, isolated and numerous, either reporting issues, committing code, or otherwise helping out the community!**\n\nThe [API](/docs/api) should now be considered stable, with only backwards compatible bug fixes to look forward to. No additional new features are planned until after the official 1.0 release.\n\n{/* more */}\n\nYou can now access the release candidate from npm:\n\n```bash\nnpm install quill@1.0.0-rc.0\n```\n\nAs always our CDN is also available:\n\n```html\n<link href=\"//cdn.quilljs.com/1.0.0-rc.0/quill.snow.css\" rel=\"stylesheet\" />\n<link href=\"//cdn.quilljs.com/1.0.0-rc.0/quill.bubble.css\" rel=\"stylesheet\" />\n\n<script src=\"//cdn.quilljs.com/1.0.0-rc.0/quill.js\"></script>\n<script src=\"//cdn.quilljs.com/1.0.0-rc.0/quill.min.js\"></script>\n```\n\nIf you are just joining from the 0.20.1 or older, take a look at the updated [Upgrading to 1.0 Guide](/guides/upgrading-to-1-0/). If you prefer to stay with 0.20, the [documentation](/0.20/) will remain available.\n\n### Many New Formats\n\nQuill 1.0 adds several new formats and improves on existing ones. Superscript, subscript, inline code and blocks, headers, blockquotes, text direction, and video support have all been added. List can also now be nested and formats previously implemented with just inline styles can now use classes [instead](/playground/#class-vs-inline-style).\n\nSyntax highlighted [code](/docs/modules/syntax/) and LaTeX [formulas](/docs/modules/formula/) can also be added with an optional module.\n\n### Brand New Theme\n\nAn entirely new theme [Bubble](/docs/themes/#bubble) has also been added, based on a floating toolbar.\n\n![Color Icon](/assets/images/blog/bubble.png)\n\nBoth Snow and Bubble now also use SVG icons to sharply scale to whatever size your application demands. The icons are implemented to be added to the DOM directly so you can customize the active color and enable less obvious UI enhancements.\n\n![Color Icon](/assets/images/blog/color.png)\n\n### More Configurable Modules\n\nExisting modules have much more customization capabilities in 1.0, with new configurations and APIs. Most notably:\n\n- [Clipboard](/docs/modules/clipboard/) introduces the ability to customize interpretation of pasted content before it reaches the editor.\n- [Keyboard](/docs/modules/keyboard/) adds a context option to give much more granular control over when bindings are triggered.\n- [Toolbar](/docs/modules/toolbar/) can be much more easily configured with just an array, and now exposes options to overwrite its handler.\n\n### Parchment\n\n[Parchment](https://github.com/quilljs/parchment/) also enters its 1.0 release candidacy today. For Quill, not only did Parchment enable the addition the numerous formats, it enables a path to the new generation of content creation. Text today is written to be rendered on the web, offering a much richer canvas than a printed piece of paper. Content can now be live, interactive, or even collaborative.\n\nUsers have already been using Quill in beta to successfully support these editing experiences. This is possible in part due to the [API](/docs/api) Quill was designed with at inception, but now Parchment takes that further by providing a powerful abstraction layer over the DOM. This enables the addition of new content and formats that cannot exist on paper or even anticipated by Quill's developers.\n\nOf course, Quill still ships with ready to go defaults, so you can integrate and use it with just a few lines of code&mdash;you never have to do more if your product needs never demands it.\n\nJump to the [demo](/) to see Quill's new features or try out some code in the [Interactive Playground](/playground/)!\n"
  },
  {
    "path": "packages/website/content/blog/quill-v0-19-no-more-iframes.mdx",
    "content": "---\ntitle: Quill v0.19 - No More Iframes\ndate: 2014-11-06\n---\n\nCustomizability is core to Quill's ethos and the new [v0.19 release](https://github.com/quilljs/quill/releases/tag/v0.19.0) is a big step towards fulfilling that mission. In previous versions Quill utilized an iframe to contain the editor. This unfortunately prevented expected browser behaviors and made it difficult for developers to access and extend Quill[^1]. Its removal is the biggest change in v0.19 and some rippling effects are expected. They, and other changes for v0.19, are summarized here.\n\n### Styles\n\nWith iframes gone it is now much easier to customize the styling of the Quill editor and unecessary for Quill to do so on your behalf in most cases. This leads to a few changes:\n\nYou can now pass `false` into the [style config](/docs/configuration/#styles) to prevent Quill from injecting any `<style>` tags. No change is necessary if the default behavior is preferred, but this added option makes it easier and more efficient for those that prefer to completely control styling with stylesheets. For this latter route, `quill.base.css` is now included in releases and the CDN.\n\n{/* more */}\n\nSince adding and customizing styles is no longer inaccessible, the `addStyles` helper is no longer particularly useful and has been removed.\n\nAll Quill containers' class names have changed to be prefixed with `ql-`. If your application was using these names in any way they will need to be updated.\n\nQuill is also no longer \"protected\" from external styles so it should be treated with the same caution as any other front end library. In particular, avoiding overly general css rules will help ensure a peaceful coexistence.\n\n### Scripts\n\nIt was always possible to access and manipulate Quill's editor, but without an iframe the task is now easy. While this greatly simplifies custom enhancements it also makes it easier to inadvertently interfere with Quill's necessary operations. But with modern developer tools and practices, the risks can be mitigated and ultimately the benefits of customization is worthwhile.\n\nThis is a good time to reiterate good bug reporting practices, not just for Quill but for all open source projects. Reducing bugs into the simplest case and providing repeatable reproduction instructions will help isolate the source of issues.\n\n### Default Theme\n\nThe default theme in Quill has been renamed from `default` to `base`. No change should be necessary unless the `default` theme was explicitly being included in the [theme config](/docs/configuration/#theme) in which case it should be changed to `base`.\n\n### NPM\n\nQuill is now listed in npm as `quill` instead of `quilljs`. Quill was already listed as `quill` on bower so its name will now be consistent across package managers.\n\n### Contributing\n\nFinally, community contribution and involvement has been tremendous and both the project and everyone using it reaps the benefits. A big thanks for all the contributions so far and keep them coming!\n\n[^1]: See [Editors and Iframes](https://www.jasonchen.me/editors-and-iframes/) for full details.\n"
  },
  {
    "path": "packages/website/content/blog/the-road-to-1-0.mdx",
    "content": "---\ntitle: The Road to 1.0\ndate: 2015-09-15\n---\n\nQuill launched with the ambitious goal of becoming _the_ rich text editor for the web, being both easy to use for drop in use cases, and powerful enough for complex ones. Its current [API](/docs/api) is core to those goals.\n\nIn the past months, much effort has been placed into providing even greater means for customization, particularly the editor's contents. With this nearing completion, Quill is approaching its 1.0 coming of age.\n\n### Parchment\n\nA full introduction and guide to Parchment is still forthcoming, but in short it is a new document model for Quill. An editor's document model is an important abstraction over the DOM that allows the editor and API users to reason about its contents through a much simpler yet more expressive interface than directly interacting with the browser. For Quill, this enables an elegant solution to the longstanding problem of format customization.\n\n{/* more */}\n\nPrior to Parchment, Quill required near complete control over its editor container and descendant DOM nodes in order to provide its precise retrieval and manipulation API. Even simple modifications such as changing the default link open behavior required hacks and defining new content types, such as syntax highlighted code, was impossible.\n\nParchment hands control of entire subtrees back to the user, allowing the definition of new nodes or overwriting existing ones. The requirement is that certain methods such as `getValue` and `getFormat` be defined in order to happily exist within a Parchment document. Those familiar will find this very similar to `render` and Components in [React](https://facebook.github.io/react/), a significant influencer of Parchment's design.\n\nWhile the Parchment interface is still being stabilized, a preview of a definition for a [KaTeX](https://github.com/Khan/KaTeX) equation looks like this (with ES6 syntax):\n\n```javascript\nclass Equation extends Parchment.Embed {\n  constructor(value) {\n    super(value);\n    this.value = value;\n    this.domNode.setAttribute('contenteditable', false);\n    katex.render(value, this.domNode);\n  }\n\n  getValue() {\n    return this.value;\n  }\n}\n\nQuill.registerFormat(Equation);\n```\n\nThe current priority is to integrate Parchment into Quill as its new document model. However, Parchment is and will remain organized as its own [repository](https://github.com/quilljs/parchment), as it was designed as a general purpose tool. Hopefully one day it may serve as the document model for other editors as well.\n\n### Formats\n\nParchment opens the doors to scalably support many more formats, many of which will be included in the 1.0 release. The complete list is not ready for announcement but they will at least include semantic headers and nested lists. Equations and syntax highlighted code will also be added as separate repositories because of their likely dependency on external libraries.\n\n### Modules\n\nQuill organizes most of its source code as modules to make it easy to overwrite their default behavior. Unfortunately a documentation gap currently exists for these modules--this will have to be filled for their extensibility to be realized.\n\nSome non-essential modules will also be moved out into their own repositories. Custom builds are planned to conveniently include or exclude these modules, along with other permutations, though this may land post 1.0 depending on timing.\n\n### Beyond 1.0\n\nWith Quill 1.0, the main foundations will be complete and much more emphasis will be placed into building examples and enhancing support, with internationalization, accessibility, and cross application interactions (copy/paste) as main focus points.\n\nIn addition, Quill's UI is due for an upgrade. While the aesthetics of Quill is already completely customizable, more numerous defaults could be available for those wanting a drop in solution. Here's a sneak peak at a couple of upcoming themes in the works:\n\n<p>\n  <img\n    className=\"road-1-theme-preview\"\n    src=\"/assets/images/blog/theme-1.png\"\n    alt=\"Quill Theme 1\"\n  />\n  <img\n    className=\"road-1-theme-preview\"\n    src=\"/assets/images/blog/theme-2.png\"\n    alt=\"Quill Theme 2\"\n  />\n</p>\n\nFinally, the community deserves a great thank you for all of your contributions and support! All the [bug reports](https://github.com/quilljs/quill/labels/bug), [features suggestions](https://github.com/quilljs/quill/labels/feature) and [pull requests](https://github.com/quilljs/quill/pulls?q=is%3Apr) make Quill what it is today. Keep these coming! Exciting times are ahead for web editing and for Quill.\n"
  },
  {
    "path": "packages/website/content/blog/the-state-of-quill-and-2-0.mdx",
    "content": "---\ntitle: The State of Quill and 2.0\ndate: 2017-09-21\nexternal: https://medium.com/@jhchen/the-state-of-quill-and-2-0-fb38db7a59b9\n---\n\nThe 2.0 branch of Quill has officially been opened and development\ncommenced. One design principle Quill embraces is to first make it\npossible, then make it easy. This allows the technical challenges to\nbe proved out and provides clarity around use cases so that the\nright audience is designed for. Quill 1.0 pushed the boundaries on\nthe former, and now 2.0 will focus on the latter.\n\nLet’s take a look at how we got here and where Quill is going!\n"
  },
  {
    "path": "packages/website/content/blog/upgrading-to-rich-text-deltas.mdx",
    "content": "---\ntitle: Upgrading to Rich Text Deltas\ndate: 2014-10-19\n---\n\nThe new rich text type is now live and being used in Quill v0.18.0. It is a big step towards 1.0 and will be the way documents and changes are represented going forward. In most cases this update is non-disruptive and an upgrade can be a simple increment of the version number[^1].\n\nHowever, if you happened to be storing the old Delta format, here's a short guide on how to migrate.\n\nThe main relevant differences between the old and new Deltas are:\n\n1. Explicit deletes - We need to go through the old Delta, find the implied deletes and insert explicit delete operations into the new Delta\n2. Support for embeds - If we see the hacky representation of embeds, replace with the new representation\n\n{/* more */}\n\n```javascript\nvar richText = require('rich-text');\n\nvar newDelta = new richText.Delta();\nvar index = 0;\nvar changeLength = 0;\noldDelta.ops.forEach(function (op) {\n  if (_.isString(op.value)) {\n    // Insert operation\n    if (op.value === '!' && op.attributes && _.isString(op.attributes.src)) {\n      // Found the old hacky representation for an embed\n      // Quill only supports images so far so we can be confident this is an image\n      // which is represented by 1\n      newDelta.insert(1, op.attributes);\n    } else {\n      newDelta.insert(op.value, op.attributes);\n    }\n    changeLength += op.value.length;\n  } else if (_.isNumber(op.start) && _.isNumber(op.end)) {\n    // Retain operation\n    if (op.start > index) {\n      // Delete operation was implied by the current retain operation\n      var length = op.start - index;\n      newDelta.delete(length);\n      changeLength -= length;\n    }\n    // Now handle or retain operation\n    newDelta.retain(op.end - op.start, op.attributes);\n    index = op.end;\n  } else {\n    throw new Error('You have a misformed delta');\n  }\n});\n\n// Handle implied deletes at the end of the document\nif (oldDelta.endLength < oldDelta.startLength + changeLength) {\n  var length = oldDelta.startLength + changeLength - oldDelta.endLength;\n  newDelta.delete(length);\n}\n```\n\nIf you cannot use the rich-text module or if you are using this as a general guide for another language, the following might be helpful in crafting insert, delete and retain operations.\n\n```javascript\nvar nweDelta = { ops: [] };\noldDelta.ops.forEach(function () {\n  // Following a similar logic to the earlier snippet\n  // except replacing .insert(), .retain(), .delete() with:\n  // insertOp(newDelta.ops, value, formats)\n  // retainOp(newDelta.ops, length, formats)\n  // deleteOp(newDelta.ops, length)\n});\n\nfunction insertOp(opsArr, text, formats) {\n  var op = { insert: text };\n  if (formats && Object.keys(formats).length > 0) {\n    op.attributes = formats;\n  }\n  opsArr.push(op);\n}\n\nfunction deleteOp(opsArr, length) {\n  opsArr.push({ delete: length });\n}\n\nfunction retainOp(opsArr, length, formats) {\n  var op = { retain: length };\n  if (formats && Object.keys(formats).length > 0) {\n    op.attributes = formats;\n  }\n  opsArr.push(op);\n}\n```\n\nThere are some optimizations performed by rich-text such as excluding no-ops (delete 0 characters) and merging two adjacent operations of the same type (insert 'A' followed by insert 'B' is merged to be a single insert 'AB' operation). But you should not have to worry about these cases since the old Delta format had similar optimizations.\n\n[^1]: All it took to upgrade the examples on quilljs.com was: [2580c2](https://github.com/quilljs/quill/commit/2580c2a5d440622d226fbef407df7a5a5e9dcf61)\n"
  },
  {
    "path": "packages/website/content/docs/api.mdx",
    "content": "---\ntitle: API\n---\n\n<div className=\"table-of-contents\">\n  {data.api.map(({ hashes, title }) => (\n    <nav key={title}>\n      <h4 anchor=\"off\">{title}</h4>\n      <ul>\n        {hashes.map(hash => (\n          <li key={hash}>\n            <a href={`#${hash.toLowerCase()}`}>\n              {hash.replace('-experimental', '')}\n            </a>\n          </li>\n        ))}\n      </ul>\n    </nav>\n  ))}\n</div>\n\n## Content\n\n### deleteText\n\nDeletes text from the editor, returning a [Delta](/docs/delta/) representing the change. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\ndeleteText(index: number, length: number, source: string = 'api'): Delta\n```\n\n**Example 1**\n\n```typescript\nquill.deleteText(6, 4);\n```\n\n**Example 2**\n\n<SandpackWithQuillTemplate\n  beforeEditor={`\n<button id=\"deleteButton\">Delete Text</button>\n\n<hr/>\n  `}\n  content={`\n<p>Hello, World!</p>\n  `}\n  files={{\n    'index.js': `\nconst quill = new Quill('#editor', { theme: 'snow' });\n\ndocument.querySelector('#deleteButton').addEventListener('click', () => {\n  quill.deleteText(5, 7);\n});\n    `\n  }}\n/>\n\n### getContents\n\nRetrieves contents of the editor, with formatting data, represented by a [Delta](/docs/delta/) object.\n\n**Methods**\n\n```typescript\ngetContents(index: number = 0, length: number = remaining): Delta\n```\n\n**Examples**\n\n```typescript\nconst delta = quill.getContents();\n```\n\n### getLength\n\nRetrieves the length of the editor contents. Note even when Quill is empty, there is still a blank line represented by '\\n', so `getLength` will return 1.\n\n**Methods**\n\n```typescript\ngetLength(): number\n```\n\n**Examples**\n\n```typescript\nconst length = quill.getLength();\n```\n\n### getText\n\nRetrieves the string contents of the editor. Non-string content are omitted, so the returned string's length may be shorter than the editor's as returned by [`getLength`](#getlength). Note even when Quill is empty, there is still a blank line in the editor, so in these cases `getText` will return '\\n'.\n\nThe `length` parameter defaults to the length of the remaining document.\n\n**Methods**\n\n```typescript\ngetText(index: number = 0, length: number = remaining): string\n```\n\n**Examples**\n\n```typescript\nconst text = quill.getText(0, 10);\n```\n\n### getSemanticHTML\n\nGet the HTML representation of the editor contents.\nThis method is useful for exporting the contents of the editor in a format that can be used in other applications.\n\nThe `length` parameter defaults to the length of the remaining document.\n\n**Methods**\n\n```typescript\ngetSemanticHTML(index: number = 0, length: number = remaining): string\n```\n\n**Examples**\n\n```typescript\nconst html = quill.getSemanticHTML(0, 10);\n```\n\n### insertEmbed\n\nInsert embedded content into the editor, returning a [Delta](/docs/delta/) representing the change. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\ninsertEmbed(index: number, type: string, value: any, source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png');\n```\n\n### insertText\n\nInserts text into the editor, optionally with a specified format or multiple [formats](/docs/formats/). Returns a [Delta](/docs/delta/) representing the change. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\ninsertText(index: number, text: string, source: string = 'api'): Delta\ninsertText(index: number, text: string, format: string, value: any,\n           source: string = 'api'): Delta\ninsertText(index: number, text: string, formats: { [name: string]: any },\n           source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.insertText(0, 'Hello', 'bold', true);\n\nquill.insertText(5, 'Quill', {\n  color: '#ffff00',\n  italic: true,\n});\n```\n\n### setContents\n\nOverwrites editor with given contents. Contents should end with a [newline](/docs/delta/#line-formatting). Returns a Delta representing the change. This will be the same as the Delta passed in, if given Delta had no invalid operations. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nsetContents(delta: Delta, source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.setContents([\n  { insert: 'Hello ' },\n  { insert: 'World!', attributes: { bold: true } },\n  { insert: '\\n' },\n]);\n```\n\n### setText\n\nSets contents of editor with given text, returning a [Delta](/docs/delta/) representing the change. Note Quill documents must end with a newline so one will be added for you if omitted. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nsetText(text: string, source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello\\n');\n```\n\n### updateContents\n\nApplies Delta to editor contents, returning a [Delta](/docs/delta/) representing the change. These Deltas will be the same if the Delta passed in had no invalid operations. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nupdateContents(delta: Delta, source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\n// Assuming editor currently contains [{ insert: 'Hello World!' }]\nquill.updateContents(new Delta()\n  .retain(6)                  // Keep 'Hello '\n  .delete(5)                  // 'World' is deleted\n  .insert('Quill')\n  .retain(1, { bold: true })  // Apply bold to exclamation mark\n);\n// Editor should now be [\n//  { insert: 'Hello Quill' },\n//  { insert: '!', attributes: { bold: true} }\n// ]\n```\n\n<Hint>\nThis method updates the contents from the beginning, not from the current selection.\nUse `Delta#retain(length: number)` to skip the contents you wish to leave unchanged.\n</Hint>\n\n## Formatting\n\n### format\n\nFormat text at user's current selection, returning a [Delta](/docs/delta/) representing the change. If the user's selection length is 0, i.e. it is a cursor, the format will be set active, so the next character the user types will have that formatting. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nformat(name: string, value: any, source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.format('color', 'red');\nquill.format('align', 'right');\n```\n\n### formatLine\n\nFormats all lines in given range, returning a [Delta](/docs/delta/) representing the change. See [formats](/docs/formats/) for a list of available formats. Has no effect when called with inline formats. To remove formatting, pass `false` for the value argument. The user's selection may not be preserved. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nformatLine(index: number, length: number, source: string = 'api'): Delta\nformatLine(index: number, length: number, format: string, value: any,\n           source: string = 'api'): Delta\nformatLine(index: number, length: number, formats: { [name: string]: any },\n           source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello\\nWorld!\\n');\n\nquill.formatLine(1, 2, 'align', 'right');   // right aligns the first line\nquill.formatLine(4, 4, 'align', 'center');  // center aligns both lines\n```\n\n### formatText\n\nFormats text in the editor, returning a [Delta](/docs/delta/) representing the change. For line level formats, such as text alignment, target the newline character or use the [`formatLine`](#formatline) helper. See [formats](/docs/formats/) for a list of available formats. To remove formatting, pass `false` for the value argument. The user's selection may not be preserved. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nformatText(index: number, length: number, source: string = 'api'): Delta\nformatText(index: number, length: number, format: string, value: any,\n           source: string = 'api'): Delta\nformatText(index: number, length: number, formats: { [name: string]: any },\n           source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello\\nWorld!\\n');\n\nquill.formatText(0, 5, 'bold', true);      // bolds 'hello'\n\nquill.formatText(0, 5, {                   // unbolds 'hello' and set its color to blue\n  'bold': false,\n  'color': 'rgb(0, 0, 255)'\n});\n\nquill.formatText(5, 1, 'align', 'right');  // right aligns the 'hello' line\n```\n\n### getFormat\n\nRetrieves common formatting of the text in the given range. For a format to be reported, all text within the range must have a truthy value. If there are different truthy values, an array with all truthy values will be reported. If no range is supplied, the user's current selection range is used. May be used to show which formats have been set on the cursor. If called with no arguments, the user's current selection range will be used.\n\n**Methods**\n\n```typescript\ngetFormat(range: Range = current): Record<string, unknown>\ngetFormat(index: number, length: number = 0): Record<string, unknown>\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello World!');\nquill.formatText(0, 2, 'bold', true);\nquill.formatText(1, 2, 'italic', true);\nquill.getFormat(0, 2);   // { bold: true }\nquill.getFormat(1, 1);   // { bold: true, italic: true }\n\nquill.formatText(0, 2, 'color', 'red');\nquill.formatText(2, 1, 'color', 'blue');\nquill.getFormat(0, 3);   // { color: ['red', 'blue'] }\n\nquill.setSelection(3);\nquill.getFormat();       // { italic: true, color: 'blue' }\n\nquill.format('strike', true);\nquill.getFormat();       // { italic: true, color: 'blue', strike: true }\n\nquill.formatLine(0, 1, 'align', 'right');\nquill.getFormat();       // { italic: true, color: 'blue', strike: true,\n                         //   align: 'right' }\n```\n\n### removeFormat\n\nRemoves all formatting and embeds within given range, returning a [Delta](/docs/delta/) representing the change. Line formatting will be removed if any part of the line is included in the range. The user's selection may not be preserved. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`. Calls where the `source` is `\"user\"` when the editor is [disabled](#disable) are ignored.\n\n**Methods**\n\n```typescript\nremoveFormat(index: number, length: number, source: string = 'api'): Delta\n```\n\n**Examples**\n\n```typescript\nquill.setContents([\n  { insert: 'Hello', { bold: true } },\n  { insert: '\\n', { align: 'center' } },\n  { insert: { formula: 'x^2' } },\n  { insert: '\\n', { align: 'center' } },\n  { insert: 'World', { italic: true }},\n  { insert: '\\n', { align: 'center' } }\n]);\n\nquill.removeFormat(3, 7);\n// Editor contents are now\n// [\n//   { insert: 'Hel', { bold: true } },\n//   { insert: 'lo\\n\\nWo' },\n//   { insert: 'rld', { italic: true }},\n//   { insert: '\\n', { align: 'center' } }\n// ]\n\n```\n\n## Selection\n\n### getBounds\n\nRetrieves the pixel position (relative to the editor container) and dimensions of a selection at a given location. The user's current selection need not be at that index. Useful for calculating where to place tooltips.\n\n**Methods**\n\n```typescript\ngetBounds(index: number, length: number = 0):\n  { left: number, top: number, height: number, width: number }\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello\\nWorld\\n');\nquill.getBounds(7); // Returns { height: 15, width: 0, left: 27, top: 31 }\n```\n\n### getSelection\n\nRetrieves the user's selection range, optionally to focus the editor first. Otherwise `null` may be returned if editor does not have focus.\n\n**Methods**\n\n```typescript\ngetSelection(focus = false): { index: number, length: number }\n```\n\n**Examples**\n\n```typescript\nconst range = quill.getSelection();\nif (range) {\n  if (range.length == 0) {\n    console.log('User cursor is at index', range.index);\n  } else {\n    const text = quill.getText(range.index, range.length);\n    console.log('User has highlighted: ', text);\n  }\n} else {\n  console.log('User cursor is not in editor');\n}\n```\n\n### setSelection\n\nSets user selection to given range, which will also focus the editor. Providing `null` as the selection range will blur the editor. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`.\n\n**Methods**\n\n```typescript\nsetSelection(index: number, length: number = 0, source: string = 'api')\nsetSelection(range: { index: number, length: number },\n             source: string = 'api')\n```\n\n**Examples**\n\n```typescript\nquill.setSelection(0, 5);\n```\n\n\n### scrollSelectionIntoView\n\nScroll the current selection into the visible area.\nIf the selection is already visible, no scrolling will occur.\n\n<Hint>\nQuill calls this method automatically when [setSelection](#setselection) is called, unless the source is `\"silent\"`.\n</Hint>\n\n**Methods**\n\n```typescript\nscrollSelectionIntoView();\n```\n\n**Examples**\n\n```typescript\nquill.scrollSelectionIntoView();\n```\n\n\n## Editor\n\n### blur\n\nRemoves focus from the editor.\n\n**Methods**\n\n```typescript\nblur();\n```\n\n**Examples**\n\n```typescript\nquill.blur();\n```\n\n### disable\n\nShorthand for [`enable(false)`](#enable).\n\n### enable\n\nSet ability for user to edit, via input devices like the mouse or keyboard. Does not affect capabilities of API calls, when the `source` is `\"api\"` or `\"silent\"`.\n\n**Methods**\n\n```typescript\nenable(enabled: boolean = true);\n```\n\n**Examples**\n\n```typescript\nquill.enable();\nquill.enable(false);   // Disables user input\n```\n\n### focus\n\nFocuses the editor and restores its last range.\n\n**Methods**\n\n```typescript\nfocus(options: { preventScroll?: boolean } = {});\n```\n\n**Examples**\n\n```typescript\n// Focus the editor, and scroll the selection into view\nquill.focus();\n\n// Focus the editor, but don't scroll\nquill.focus({ preventScroll: true });\n```\n\n### hasFocus\n\nChecks if editor has focus. Note focus on toolbar, tooltips, does not count as the editor.\n\n**Methods**\n\n```typescript\nhasFocus(): boolean\n```\n\n**Examples**\n\n```typescript\nquill.hasFocus();\n```\n\n### update\n\nSynchronously check editor for user updates and fires events, if changes have occurred. Useful for collaborative use cases during conflict resolution requiring the latest up to date state. [Source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`.\n\n**Methods**\n\n```typescript\nupdate(((source: string) = 'user'));\n```\n\n**Examples**\n\n```typescript\nquill.update();\n```\n\n### scrollRectIntoView #experimental\n\nScrolls the editor to the given pixel position.\n[`scrollSelectionIntoView`](#scrollselectionintoview) is implemented by calling this method with the bounds of the current selection.\n\n```typescript\nscrollRectIntoView(bounds: {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n});\n```\n\n**Example 1**\n\n```typescript\n// Scroll the editor to reveal the range of { index: 20, length: 5 }\nconst bounds = this.selection.getBounds(20, 5);\nif (bounds) {\n  quill.scrollRectIntoView(bounds);\n}\n```\n\n**Example 2**\n\n<SandpackWithQuillTemplate\n  files={{\n    'index.js': `\nlet text = \"\";\nfor (let i = 0; i < 100; i += 1) {\n  text += \\`line \\${i + 1}\\\\n\\`;\n}\n\nconst quill = new Quill('#editor', { theme: 'snow' });\nquill.setText(text);\n\nconst target = 'line 50';\nconst bounds = quill.selection.getBounds(\n  text.indexOf(target),\n  target.length\n);\nif (bounds) {\n  quill.scrollRectIntoView(bounds);\n}\n    `\n  }}\n/>\n\n## Events\n\n### text-change\n\nEmitted when the contents of Quill have changed. Details of the change, representation of the editor contents before the change, along with the source of the change are provided. The source will be `\"user\"` if it originates from the users. For example:\n\n- User types into the editor\n- User formats text using the toolbar\n- User uses a hotkey to undo\n- User uses OS spelling correction\n\nChanges may occur through an API but as long as they originate from the user, the provided source should still be `\"user\"`. For example, when a user clicks on the toolbar, technically the toolbar module calls a Quill API to effect the change. But source is still `\"user\"` since the origin of the change was the user's click.\n\nAPIs causing text to change may also be called with a `\"silent\"` source, in which case `text-change` will not be emitted. This is not recommended as it will likely break the undo stack and other functions that rely on a full record of text changes.\n\nChanges to text may cause changes to the selection (ex. typing advances the cursor), however during the `text-change` handler, the selection is not yet updated, and native browser behavior may place it in an inconsistent state. Use [`selection-change`](#selection-change) or [`editor-change`](#editor-change) for reliable selection updates.\n\n**Callback Signature**\n\n```typescript\nhandler(delta: Delta, oldContents: Delta, source: string)\n```\n\n**Examples**\n\n```typescript\nquill.on('text-change', (delta, oldDelta, source) => {\n  if (source == 'api') {\n    console.log('An API call triggered this change.');\n  } else if (source == 'user') {\n    console.log('A user action triggered this change.');\n  }\n});\n```\n\n### selection-change\n\nEmitted when a user or API causes the selection to change, with a range representing the selection boundaries. A null range indicates selection loss (usually caused by loss of focus from the editor). You can also use this event as a focus change event by just checking if the emitted range is null or not.\n\nAPIs causing the selection to change may also be called with a `\"silent\"` source, in which case `selection-change` will not be emitted. This is useful if `selection-change` is a side effect. For example, typing causes the selection to change but would be very noisy to also emit a `selection-change` event on every character.\n\n**Callback Signature**\n\n```typescript\nhandler(range: { index: number, length: number },\n        oldRange: { index: number, length: number },\n        source: string)\n```\n\n**Examples**\n\n```typescript\nquill.on('selection-change', (range, oldRange, source) => {\n  if (range) {\n    if (range.length == 0) {\n      console.log('User cursor is on', range.index);\n    } else {\n      const text = quill.getText(range.index, range.length);\n      console.log('User has highlighted', text);\n    }\n  } else {\n    console.log('Cursor not in the editor');\n  }\n});\n```\n\n### editor-change\n\nEmitted when either `text-change` or `selection-change` would be emitted, even when the source is `\"silent\"`. The first parameter is the event name, either `text-change` or `selection-change`, followed by the arguments normally passed to those respective handlers.\n\n**Callback Signature**\n\n```typescript\nhandler(name: string, ...args)\n```\n\n**Examples**\n\n```typescript\nquill.on('editor-change', (eventName, ...args) => {\n  if (eventName === 'text-change') {\n    // args[0] will be delta\n  } else if (eventName === 'selection-change') {\n    // args[0] will be old range\n  }\n});\n```\n\n### on\n\nAdds event handler. See [text-change](#text-change) or [selection-change](#selection-change) for more details on the events themselves.\n\n**Methods**\n\n```typescript\non(name: string, handler: Function): Quill\n```\n\n**Examples**\n\n```typescript\nquill.on('text-change', () => {\n  console.log('Text change!');\n});\n```\n\n### once\n\nAdds handler for one emission of an event. See [text-change](#text-change) or [selection-change](#selection-change) for more details on the events themselves.\n\n**Methods**\n\n```typescript\nonce(name: string, handler: Function): Quill\n```\n\n**Examples**\n\n```typescript\nquill.once('text-change', () => {\n  console.log('First text change!');\n});\n```\n\n### off\n\nRemoves event handler.\n\n**Methods**\n\n```typescript\noff(name: string, handler: Function): Quill\n```\n\n**Examples**\n\n```typescript\nfunction handler() {\n  console.log('Hello!');\n}\n\nquill.on('text-change', handler);\nquill.off('text-change', handler);\n```\n\n## Model\n\n### find\n\nStatic method returning the Quill or [Blot](https://github.com/quilljs/parchment) instance for the given DOM node. In the latter case, passing in true for the `bubble` parameter will search up the given DOM's ancestors until it finds a corresponding [Blot](https://github.com/quilljs/parchment).\n\n**Methods**\n\n```typescript\nQuill.find(domNode: Node, bubble: boolean = false): Blot | Quill\n```\n\n**Examples**\n\n```typescript\nconst container = document.querySelector('#container');\nconst quill = new Quill(container);\nconsole.log(Quill.find(container) === quill); // Should be true\n\nquill.insertText(0, 'Hello', 'link', 'https://world.com');\nconst linkNode = document.querySelector('#container a');\nconst linkBlot = Quill.find(linkNode);\n\n// Find Quill instance from a blot\nconsole.log(Quill.find(linkBlot.scroll.domNode.parentElement));\n```\n\n### getIndex\n\nReturns the distance between the beginning of document to the occurrence of the given [Blot](https://github.com/quilljs/parchment).\n\n**Methods**\n\n```typescript\ngetIndex(blot: Blot): number\n```\n\n**Examples**\n\n```typescript\nlet [line, offset] = quill.getLine(10);\nlet index = quill.getIndex(line); // index + offset should == 10\n```\n\n### getLeaf\n\nReturns the leaf [Blot](https://github.com/quilljs/parchment) at the specified index within the document.\n\n**Methods**\n\n```typescript\ngetLeaf(index: number): [LeafBlot | null, number]\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello Good World!');\nquill.formatText(6, 4, 'bold', true);\n\nlet [leaf, offset] = quill.getLeaf(7);\n// leaf should be a Text Blot with value \"Good\"\n// offset should be 1, since the returned leaf started at index 6\n```\n\n### getLine\n\nReturns the line [Blot](https://github.com/quilljs/parchment) at the specified index within the document.\n\n**Methods**\n\n```typescript\ngetLine(index: number): [Block | BlockEmbed | null, number]\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello\\nWorld!');\n\nlet [line, offset] = quill.getLine(7);\n// line should be a Block Blot representing the 2nd \"World!\" line\n// offset should be 1, since the returned line started at index 6\n```\n\n### getLines\n\nReturns the lines contained within the specified location.\n\n**Methods**\n\n```typescript\ngetLines(index: number = 0, length: number = remaining): (Block | BlockEmbed)[]\ngetLines(range: Range): (Block | BlockEmbed)[]\n```\n\n**Examples**\n\n```typescript\nquill.setText('Hello\\nGood\\nWorld!');\nquill.formatLine(1, 1, 'list', 'bullet');\n\nlet lines = quill.getLines(2, 5);\n// array with a ListItem and Block Blot,\n// representing the first two lines\n```\n\n## Extension\n\n### debug\n\nStatic method enabling logging messages at a given level: `'error'`, `'warn'`, `'log'`, or `'info'`. Passing `true` is equivalent to passing `'log'`. Passing `false` disables all messages.\n\n**Methods**\n\n```typescript\nQuill.debug(level: string | boolean)\n```\n\n**Examples**\n\n```typescript\nQuill.debug('info');\n```\n\n### import\n\nStatic method returning Quill library, format, module, or theme. In general the path should map exactly to Quill source code directory structure. Unless stated otherwise, modification of returned entities may break required Quill functionality and is strongly discouraged.\n\n**Methods**\n\n```typescript\nQuill.import(path): any\n```\n\n**Examples**\n\n```typescript\nconst Parchment = Quill.import('parchment');\nconst Delta = Quill.import('delta');\n\nconst Toolbar = Quill.import('modules/toolbar');\nconst Link = Quill.import('formats/link');\n// Similar to ES6 syntax `import Link from 'quill/formats/link';`\n```\n\n<Hint>\nDon't confuse this with the [`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) keyword for ECMAScript modules.\n`Quill.import()` doesn't load scripts over the network, it just returns the corresponding module from the Quill library without causing any side-effects.\n</Hint>\n\n### register\n\nRegisters a module, theme, or format(s), making them available to be added to an editor. Can later be retrieved with [`Quill.import`](/docs/api/#import). Use the path prefix of `'formats/'`, `'modules/'`, or `'themes/'` for registering formats, modules or themes, respectively. For formats specifically there is a shorthand to just pass in the format directly and the path will be autogenerated. Will overwrite existing definitions with the same path.\n\n**Methods**\n\n```typescript\nQuill.register(format: Attributor | BlotDefinintion, supressWarning: boolean = false)\nQuill.register(path: string, def: any, supressWarning: boolean = false)\nQuill.register(defs: { [path: string]: any }, supressWarning: boolean = false)\n```\n\n**Examples**\n\n```typescript\nconst Module = Quill.import('core/module');\n\nclass CustomModule extends Module {}\n\nQuill.register('modules/custom-module', CustomModule);\n```\n\n```typescript\nQuill.register({\n  'formats/custom-format': CustomFormat,\n  'modules/custom-module-a': CustomModuleA,\n  'modules/custom-module-b': CustomModuleB,\n});\n\nQuill.register(CustomFormat);\n// You cannot do Quill.register(CustomModuleA); as CustomModuleA is not a format\n```\n\n### addContainer\n\nAdds and returns a container element inside the Quill container, sibling to the editor itself. By convention, Quill modules should have a class name prefixed with `ql-`. Optionally include a refNode where container should be inserted before.\n\n**Methods**\n\n```typescript\naddContainer(className: string, refNode?: Node): Element\naddContainer(domNode: Node, refNode?: Node): Element\n```\n\n**Examples**\n\n```typescript\nconst container = quill.addContainer('ql-custom');\n```\n\n### getModule\n\nRetrieves a module that has been added to the editor.\n\n**Methods**\n\n```typescript\ngetModule(name: string): any\n```\n\n**Examples**\n\n```typescript\nconst toolbar = quill.getModule('toolbar');\n```\n"
  },
  {
    "path": "packages/website/content/docs/configuration.mdx",
    "content": "---\ntitle: Configuration\n---\n\nQuill allows several ways to customize it to suit your needs. This section is dedicated to tweaking existing functionality. See the [Modules](/docs/modules/) section for adding new functionality and the [Themes](/docs/themes/) section for styling.\n\n\n## Container\n\nQuill requires a container where the editor will be appended. You can pass in either a CSS selector or a DOM object.\n\n```javascript\nconst quill = new Quill('#editor');  // First matching element will be used\n```\n\n```javascript\nconst container = document.getElementById('editor');\nconst quill = new Quill(container);\n```\n\nIf the container is not empty, Quill will initialize with the existing contents.\n\n## Options\n\nTo configure Quill, pass in an options object:\n\n<SandpackWithQuillTemplate\n  files={{\n    \"/index.js\": `\nconst options = {\n  debug: 'info',\n  modules: {\n    toolbar: true,\n  },\n  placeholder: 'Compose an epic...',\n  theme: 'snow'\n};\nconst quill = new Quill('#editor', options);\n`}}\n/>\n\nThe following keys are recognized:\n\n### bounds\n\nDefault: `document.body`\n\nDOM Element or a CSS selector for a DOM Element, within which the editor's ui elements (i.e. tooltips, etc.) should be confined. Currently, it only considers left and right boundaries.\n\n\n### debug\n\nDefault: `warn`\n\nShortcut for [debug](/docs/api/#debug). Note `debug` is a static method and will affect other instances of Quill editors on the page. Only warning and error messages are enabled by default.\n\n### formats\n\nDefault: `null`\n\nA list of formats that are recognized and can exist within the editor contents.\n\nBy default, all formats that are defined in the Quill library are allowed.\nTo restrict formatting to a smaller list, pass in an array of the format names to support.\n\nYou can create brand new formats or more fully customize the content using [Registries](/docs/registries/).\nSpecifying a `registry` option will ignore this `formats` option.\n\n<Sandpack\n  defaultShowPreview\n  activeFile=\"index.js\"\n  files={{\n    'index.html': `\n<!-- Include stylesheet -->\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n<div id=\"editor\">\n</div>\n<!-- Include the Quill library -->\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<script src=\"/index.js\"></script>`,\n    \"/index.js\": `\nconst Parchment = Quill.import('parchment');\n\nconst quill = new Quill('#editor', {\n  formats: ['italic'],\n});\n\nconst Delta = Quill.import('delta');\nquill.setContents(\n  new Delta()\n    .insert('Only ')\n    .insert('italic', { italic: true })\n    .insert(' is allowed. ')\n    .insert('Bold', { bold: true })\n    .insert(' is not.')\n);\n`}}\n/>\n\n### placeholder\n\nDefault: None\n\nPlaceholder text to show when editor is empty.\n\n<SandpackWithQuillTemplate\n  files={{\n    \"/index.js\": `\nconst options = {\n  placeholder: 'Hello, World!',\n  theme: 'snow'\n};\nconst quill = new Quill('#editor', options);\n`}}\n/>\n\n\n### readOnly\n\nDefault: `false`\n\nWhether to instantiate the editor to read-only mode.\n\n<SandpackWithQuillTemplate\n  files={{\n    \"/index.js\": `\nconst options = {\n  readOnly: true,\n  modules: {\n    toolbar: null\n  },\n  theme: 'snow'\n};\nconst quill = new Quill('#editor', options);\nconst Delta = Quill.import('delta');\nquill.setContents(\n  new Delta()\n    .insert('Hello, ')\n    .insert('World', { bold: true })\n    .insert('\\\\n')\n);\n\n`}}\n/>\n\n### registry\n\nDefault: `null`\n\nBy default all formats defined by Quill are supported in the editor contents through a shared registry between editor instances. Use `formats` to restrict formatting for simple use cases and `registry` for greater customization. Specifying this `registry` option will ignore the `formatting` option. Learn more about [Registries](/docs/registries/).\n\n### theme\n\nName of theme to use. The builtin options are `\"bubble\"` or `\"snow\"`. An invalid or falsy value will load a default minimal theme. Note the theme's specific stylesheet still needs to be included manually. See [Themes](/docs/themes/) for more information.\n"
  },
  {
    "path": "packages/website/content/docs/customization/registries.mdx",
    "content": "---\ntitle: Registries\n---\n\nRegistries allow multiple editors with different formats to coexist on the same page.\n\nIf you register a format with `Quill.register()`, the format will be registered to a global registry,\nwhich will be used by all Quill instances.\n\nHowever, in some cases, you might want to have multiple registries, so that different Quill instances\ncan have different formats. For example, you might want to have a Quill instance that only supports\nbold and italic, and another Quill instance that supports bold, italic, and underline.\n\n## Usage\n\nTo create a Quill with a custom registry, you can pass in a registry object to the Quill constructor:\n\n```js\nconst registry = new Parchment.Registry();\n\n// Register the formats that you need for the editor with `registry.register()`.\n// We will cover this in more detail in the next section.\n\nconst quill = new Quill('#editor', {\n  registry,\n  // ...other options\n})\n```\n\n## Register Formats\n\nA custom registry doesn't come with any formats by default. You should register the formats that you need with `registry.register()`.\nThere are some essential formats that you will need to register in order to have a functional editor:\n\n<Sandpack\n  defaultShowPreview\n  files={{\n    'index.html': `\n<!-- Include stylesheet -->\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n<div id=\"editor\">\n</div>\n<!-- Include the Quill library -->\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<script src=\"/index.js\"></script>`,\n    \"/index.js\": `\nconst Parchment = Quill.import('parchment');\n\n// Essential formats\nconst Block = Quill.import('blots/block');\nconst Break = Quill.import('blots/break');\nconst Container = Quill.import('blots/container');\nconst Cursor = Quill.import('blots/cursor');\nconst Inline = Quill.import('blots/inline');\nconst Scroll = Quill.import('blots/scroll');\nconst Text= Quill.import('blots/text');\n\nconst registry = new Parchment.Registry();\nregistry.register(\n  Scroll,\n  Block,\n  Break,\n  Container,\n  Cursor,\n  Inline,\n  Text,\n);\n\nconst quill = new Quill('#editor', {\n  registry,\n  theme: 'snow'\n});\n`}}\n/>\n\n<Hint>\nYou may have noticed that the format buttons on the toolbar above doesn't work.\nThis is because we haven't registered any of the corresponding formats yet.\n\nThe toolbar module doesn't detect whether a format is available or not, so it will always show the buttons.\nFollow [this guide](/docs/modules/toolbar) to learn more about how to customize the toolbar.\n</Hint>\n\n"
  },
  {
    "path": "packages/website/content/docs/customization/themes.mdx",
    "content": "---\ntitle: Themes\n---\n\nThemes allow you to easily make your editor look good with minimal effort. Quill features two officially supported themes: [Snow](#snow) and [Bubble](#bubble).\n\n### Usage\n\n```html\n<!-- Add the theme's stylesheet -->\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.bubble.css\" />\n\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<script>\n  const quill = new Quill('#editor', {\n    theme: 'bubble', // Specify theme in configuration\n  });\n</script>\n```\n\n## Bubble\n\nBubble is a simple tooltip based theme.\n\n<Sandpack\n  preferPreview\n  files={{\n    'index.html': `\n<link href=\"{{site.cdn}}/quill.bubble.css\" rel=\"stylesheet\" />\n\n<div id=\"editor\" style=\"margin: 50px 0;\">\n  <p>Hello, <strong>World</strong></p>\n</div>\n\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<script>\n  const quill = new Quill('#editor', {\n    placeholder: 'Compose an epic...',\n    theme: 'bubble',\n  });\n</script>`\n  }}\n/>\n<a className=\"standalone-link\" href=\"/standalone/bubble/\">\n  Standalone\n</a>\n\n## Snow\n\nSnow is a clean, flat toolbar theme.\n\n<Sandpack\n  preferPreview\n  files={{\n    'index.html': `\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n\n<div id=\"editor\">\n  <p>Hello, <strong>World</strong></p>\n</div>\n\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<script>\n  const quill = new Quill('#editor', {\n    placeholder: 'Compose an epic...',\n    theme: 'snow',\n  });\n</script>`\n  }}\n/>\n<a className=\"standalone-link\" href=\"/standalone/snow/\">\n  Standalone\n</a>\n\n### Customization\n\nThemes primarily control the visual look of Quill through its CSS stylesheet, and many changes can easily be made by overriding these rules. This is easiest to do, as with any other web application, by simply using your browser developer console to inspect the elements to view the rules affecting them.\n\nMany other customizations can be done through the respective modules. For example, the toolbar is perhaps the most visible user interface, but much of the customization is done through the [Toolbar module](/docs/modules/toolbar/).\n"
  },
  {
    "path": "packages/website/content/docs/customization.mdx",
    "content": "---\ntitle: Customization\n---\n\nQuill was designed with customization and extension in mind. This is achieved by implementing a small editor core exposed by a granular, well defined [API](/docs/api). The core is augmented by [modules](/docs/modules), using the same [APIs](/docs/api) you have access to.\n\nIn general, common customizations are handled in [configurations](#configurations/), user interfaces by [Themes](#themes) and CSS, functionality by [modules](#modules), and editor contents by [Parchment](#content-and-formatting).\n\n### Configurations\n\nQuill favors Code over Configuration&trade;, but for very common needs, especially where the equivalent code would be lengthy or complex, Quill provides [configuration](/docs/configuration/) options. This would be a good first place to look to determine if you even need to implement any custom functionality.\n\nTwo of the most powerful options is `modules` and `theme`. You can drastically change or expand what Quill can and does do by simply adding or removing individual [modules](/docs/modules/) or using a different [theme](/docs/themes/).\n\n### Themes\n\nQuill officially supports a standard toolbar theme [Snow](/docs/themes/#snow) and a floating tooltip theme [Bubble](/docs/themes/#bubble). Since Quill is not confined within an iframe like many legacy editors, many visual modifications can be made with just CSS, using one of the existing themes.\n\nIf you would like to drastically change UI interactions, you can omit the `theme` configuration option, which will give you an unstyled Quill editor. You do still need to include a minimal stylesheet that, for example, makes sure spaces render in all browsers and ordered lists are appropriately numbered.\n\n```html\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.core.css\" />\n```\n\nFrom there you can implement and attach your own UI elements like custom dropdowns or tooltips. The last section of [Cloning Medium with Parchment](/guides/cloning-medium-with-parchment/#final-polish) provides an example of this in action.\n\n### Modules\n\nQuill is designed with a modular architecture composed of a small editing core, surrounded by modules that augment its functionality. Some of this functionality is quite integral to editing, such as the [History](/docs/modules/history/) module, which manages undo and redo. Because all modules use the same public [API](/docs/api) exposed to the developer, even interchanging core modules is possible, when necessary.\n\nLike Quill's core itself, many [modules](/docs/modules/) expose additional configuration options and APIs. Before deciding to replace a module, take a look at its documentation. Often your desired customization is already implemented as a configuration or API call.\n\nOtherwise, if you would like to drastically change functionality an existing module already covers, you can simply not include it&mdash;or explicitly exclude it when a theme includes it by default&mdash;and implement the functionality to your liking external to Quill, using the same [API](/docs/api) the default module uses.\n\n<SandpackWithQuillTemplate\n  files={{\n    'index.js': `\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: false    // Snow includes toolbar by default\n  },\n  theme: 'snow'\n});\n`,\n  }}\n/>\n\nA few modules&mdash;[Clipboard](/docs/modules/clipboard/), [Keyboard](/docs/modules/keyboard/), and [History](/docs/modules/history/)&mdash;need to be included as core functionality depend on the APIs they provide. For example, even though undo and redo is basic, expected, editor functionality, the native browser behavior handling of this is inconsistent and unpredictable. The History module bridges the gap by implementing its own undo manager and exposing `undo()` and `redo()` as APIs.\n\nNevertheless, staying true to Quill modular design, you can still drastically change the way undo and redo&mdash;or any other core functionality&mdash;works by implementing your own undo manager to replace the History module. As long as you implement the same API interface, Quill will happily use your replacement module. This is most easily done by inheriting from the existing module, and overwriting the methods you want to change. Take a look at the [modules](/docs/modules/) documentation for a very simple example of overwriting the core [Clipboard](/docs/modules/clipboard/) module.\n\nFinally, you may want to add functionality not provided by existing modules. In this case, it may be convenient to organize this as a Quill module, which the [Building A Custom Module](/guides/building-a-custom-module/) guide covers. Of course, it is certainly valid to just keep this logic separate from Quill, in your application code instead.\n\n### Content and Formatting\n\nQuill allows modification and extension of the contents and formats that it understands through its document model [Parchment](https://github.com/quilljs/parchment/). Content and formats are represented in Parchment as either Blots or Attributors, which roughly correspond to Nodes or Attributes in the DOM.\n\n#### Class vs Inline\n\nQuill uses classes, instead of inline style attributes, when possible, but both are implemented for you to pick and choose. A live example of this is implemented as a [Playground snippet](/playground/#class-vs-inline-style).\n\n```js\nconst ColorClass = Quill.import('attributors/class/color');\nconst SizeStyle = Quill.import('attributors/style/size');\nQuill.register(ColorClass, true);\nQuill.register(SizeStyle, true);\n\n// Initialize as you would normally\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: true,\n  },\n  theme: 'snow',\n});\n```\n\n#### Customizing Attributors\n\nIn addition to choosing the particular Attributor, you can also customize existing ones. Here is an example of the font whitelist to add additional fonts.\n\n```js\nconst FontAttributor = Quill.import('attributors/class/font');\nFontAttributor.whitelist = [\n  'sofia',\n  'slabo',\n  'roboto',\n  'inconsolata',\n  'ubuntu',\n];\nQuill.register(FontAttributor, true);\n```\n\nNote you still need to add styling for these classes into your CSS files.\n\n```html\n<link href=\"https://fonts.googleapis.com/css?family=Roboto\" rel=\"stylesheet\" />\n<style>\n  .ql-font-roboto {\n    font-family: 'Roboto', sans-serif;\n  }\n</style>\n```\n\n#### Customizing Blots\n\nFormats represented by Blots can also be customized. Here is how you would change the DOM Node used to represent bold formatting.\n\n<SandpackWithQuillTemplate\n  files={{\n    'index.js': `\nconst Bold = Quill.import('formats/bold');\nBold.tagName = 'B';   // Quill uses <strong> by default\nQuill.register(Bold, true);\n\n// Initialize as you would normally\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: true\n  },\n  theme: 'snow'\n});\n\nconst Delta = Quill.import('delta');\nquill.setContents(\n  new Delta()\n    .insert('Rendered with <b>!', { bold: true })\n    .insert('\\\\n')\n);\n`\n}}\n/>\n\n#### Extending Blots\n\nYou can also extend existing formats. Here is a quick ES6 implementation of a list item that does not permit formatting its contents. Code blocks are implemented in exactly this way.\n\n```js\nconst ListItem = Quill.import('formats/list/item');\n\nclass PlainListItem extends ListItem {\n  formatAt(index, length, name, value) {\n    if (name === 'list') {\n      // Allow changing or removing list format\n      super.formatAt(name, value);\n    }\n    // Otherwise ignore\n  }\n}\n\nQuill.register(PlainListItem, true);\n\n// Initialize as you would normally\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: true,\n  },\n  theme: 'snow',\n});\n```\n\nYou can view a list of Blots and Attributors available by calling `console.log(Quill.imports);`. Direct modification of this object is not supported. Use [`Quill.register`](/docs/api/#register) instead.\n\nA complete reference on Parchment, Blots and Attributors can be found on Parchment's own [README](https://github.com/quilljs/parchment/). For an in-depth walkthrough, take a look at [Cloning Medium with Parchment](/guides/cloning-medium-with-parchment/), which starts with Quill understanding just plain text, to adding all of the formats [Medium](https://medium.com/) supports. Most of the time, you will not have to build formats from scratch since most are already implemented in Quill, but it is still useful to understanding how Quill works at this deeper level.\n"
  },
  {
    "path": "packages/website/content/docs/delta.mdx",
    "content": "---\ntitle: Delta\n---\n\nDeltas are a simple, yet expressive format that can be used to describe Quill's contents and changes. The format is a strict subset of JSON, is human readable, and easily parsible by machines. Deltas can describe any Quill document, includes all text and formatting information, without the ambiguity and complexity of HTML.\n\n<Hint>\nDon't be confused by its name <em>Delta</em>&mdash;Deltas represents both documents and changes to documents. If you think of Deltas as the instructions from going from one document to another, the way Deltas represent a document is by expressing the instructions starting from an empty document.\n</Hint>\n\nDeltas are implemented as a separate [standalone library](https://github.com/quilljs/delta/), allowing its use outside of Quill. It is suitable for [Operational Transform](https://en.wikipedia.org/wiki/Operational_transformation) and can be used in realtime, Google Docs like applications. For a more in depth explanation behind Deltas, see [Designing the Delta Format](/guides/designing-the-delta-format/).\n\n<Hint>\nIt is not recommended to construct Deltas by hand&mdash;rather use the chainable [`insert()`](https://github.com/quilljs/delta#insert), [`delete()`](https://github.com/quilljs/delta#delete), and [`retain()`](https://github.com/quilljs/delta#retain) methods to create new Deltas. You can use [`import()`](/docs/api/#import) to access Delta from Quill.\n</Hint>\n\n## Document\n\nThe Delta format is almost entirely self-explanatory&mdash;the example below describes the string \"Gandalf the Grey\" where \"Gandalf\" is bolded and \"Grey\" is colored #cccccc.\n\n```javascript\n{\n  ops: [\n    { insert: 'Gandalf', attributes: { bold: true } },\n    { insert: ' the ' },\n    { insert: 'Grey', attributes: { color: '#cccccc' } }\n  ]\n}\n```\n\nAs its name would imply, describing content is actually a special case for Deltas. The above example is more specifically instructions to insert a bolded string \"Gandalf\", an unformatted string \" the \", followed by the string \"Grey\" colored #cccccc. When Deltas are used to describe content, it can be thought of as the content that would be created if the Delta was applied to an empty document.\n\nSince Deltas are a data format, there is no inherent meaning to the values of `attribute` keypairs. For example, there is nothing in the Delta format that dictates color value must be in hex&mdash;this is a choice that Quill makes, and can be modified if desired with [Parchment](https://github.com/quilljs/parchment/).\n\n### Embeds\n\nFor non-text content such as images or formulas, the insert key can be an object. The object should have one key, which will be used to determine its type. This is the `blotName` if you are building custom content with [Parchment](https://github.com/quilljs/parchment/). Like text, embeds can still have an `attributes` key to describe formatting to be applied to the embed. All embeds have a length of one.\n\n```javascript\n{\n  ops: [{\n    // An image link\n    insert: {\n      image: 'https://quilljs.com/assets/images/icon.png'\n    },\n    attributes: {\n      link: 'https://quilljs.com'\n    }\n  }]\n}\n```\n\n### Line Formatting\n\nAttributes associated with a newline character describes formatting for that line.\n\n```javascript\n{\n  ops: [\n    { insert: 'The Two Towers' },\n    { insert: '\\n', attributes: { header: 1 } },\n    { insert: 'Aragorn sped on up the hill.\\n' }\n  ]\n}\n```\n\nAll Quill documents must end with a newline character, even if there is no formatting applied to the last line. This way, you will always have a character position to apply line formatting to.\n\nMany line formats are exclusive. For example Quill does not allow a line to simultaneously be both a header and a list, despite being possible to represent in the Delta format.\n\n## Changes\n\nWhen you register a listener for Quill's [`text-change`](/docs/api/#text-change) event, one of the arguments you will get is a Delta describing what changed. In addition to `insert` operations, this Delta might also have `delete` or `retain` operations.\n\n### Delete\n\nThe `delete` operation instructs exactly what it implies: delete the next number of characters.\n\n```javascript\n{\n  ops: [\n    { delete: 10 } // Delete the next 10 characters\n  ]\n}\n```\n\nSince `delete` operations do not include _what_ was deleted, a Delta is not reversible.\n\n### Retain\n\nA `retain` operation simply means keep the next number of characters, without modification. If `attributes` is specified, it still means keep those characters, but apply the formatting specified by the `attributes` object. A `null` value for an attributes key is used to specify format removal.\n\nStarting with the above \"Gandalf the Grey\" example:\n\n```javascript\n// {\n//   ops: [\n//     { insert: 'Gandalf', attributes: { bold: true } },\n//     { insert: ' the ' },\n//     { insert: 'Grey', attributes: { color: '#cccccc' } }\n//   ]\n// }\n\n{\n  ops: [\n    // Unbold and italicize \"Gandalf\"\n    { retain: 7, attributes: { bold: null, italic: true } },\n\n    // Keep \" the \" as is\n    { retain: 5 },\n\n    // Insert \"White\" formatted with color #fff\n    { insert: 'White', attributes: { color: '#fff' } },\n\n    // Delete \"Grey\"\n    { delete: 4 }\n  ]\n}\n```\n\nNote that a Delta's instructions always starts at the beginning of the document. And because of plain `retain` operations, we never need to specify an index for a `delete` or `insert` operation.\n\n### Playground\n\nPlay around with Quill and take a look at how its content and changes look. Open your developer console for another view into the Deltas.\n\n<SandpackWithQuillTemplate\n  preferPreview\n  afterEditor={`\n  <pre id=\"playground\" style=\"font-size: 12px\">\n  </pre>\n  `}\n  files={{\n    'index.js': `\nconst quill = new Quill('#editor', { theme: 'snow' });\n\nquill.on(Quill.events.TEXT_CHANGE, update);\nconst playground = document.querySelector('#playground');\nupdate();\n\nfunction formatDelta(delta) {\n  return \\`<div>\\${JSON.stringify(delta.ops, null, 2)}</div>\\`;\n}\n\nfunction update(delta) {\n  const contents = quill.getContents();\n  let html = \\`<h3>contents</h3>\\${formatDelta(contents)}\\`\n  if (delta) {\n    html = \\`\\${html}<h3>change</h3>\\${formatDelta(delta)}\\`;\n  }\n  playground.innerHTML = html;\n}\n\n    `\n  }}\n/>\n"
  },
  {
    "path": "packages/website/content/docs/formats.mdx",
    "content": "---\ntitle: Formats\n---\n\nQuill supports a number of formats, both in UI controls and API calls.\n\nBy default, all formats are enabled and allowed in a Quill editor. They can be configured with the [formats](/docs/configuration/#formats) option. This is separate from adding a control in the [Toolbar](/docs/modules/toolbar/). For example, you can configure Quill to allow bolded content to be pasted into an editor that has no bold button in the toolbar.\n\n<Sandpack\n  preferPreview\n  files={{\n'index.html': `\n<link href=\"/styles.css\" rel=\"stylesheet\" />\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n<script src=\"{{site.highlightjs}}/highlight.min.js\"></script>\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<link\n  rel=\"stylesheet\"\n  href=\"{{site.highlightjs}}/styles/atom-one-dark.min.css\"\n/>\n<script src=\"{{site.katex}}/katex.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.katex}}/katex.min.css\" />\n\n<div id=\"toolbar-container\">\n  <span class=\"ql-formats\">\n    <select class=\"ql-font\"></select>\n    <select class=\"ql-size\"></select>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-bold\"></button>\n    <button class=\"ql-italic\"></button>\n    <button class=\"ql-underline\"></button>\n    <button class=\"ql-strike\"></button>\n  </span>\n  <span class=\"ql-formats\">\n    <select class=\"ql-color\"></select>\n    <select class=\"ql-background\"></select>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-script\" value=\"sub\"></button>\n    <button class=\"ql-script\" value=\"super\"></button>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-header\" value=\"1\"></button>\n    <button class=\"ql-header\" value=\"2\"></button>\n    <button class=\"ql-blockquote\"></button>\n    <button class=\"ql-code-block\"></button>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-list\" value=\"ordered\"></button>\n    <button class=\"ql-list\" value=\"bullet\"></button>\n    <button class=\"ql-indent\" value=\"-1\"></button>\n    <button class=\"ql-indent\" value=\"+1\"></button>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-direction\" value=\"rtl\"></button>\n    <select class=\"ql-align\"></select>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-link\"></button>\n    <button class=\"ql-image\"></button>\n    <button class=\"ql-video\"></button>\n    <button class=\"ql-formula\"></button>\n  </span>\n  <span class=\"ql-formats\">\n    <button class=\"ql-clean\"></button>\n  </span>\n</div>\n<div id=\"editor\">\n</div>\n\n<!-- Initialize Quill editor -->\n<script>\n  const quill = new Quill('#editor', {\n    modules: {\n      syntax: true,\n      toolbar: '#toolbar-container',\n    },\n    placeholder: 'Compose an epic...',\n    theme: 'snow',\n  });\n</script>\n`,\n  }}\n/>\n\n<a className=\"standalone-link\" href=\"/standalone/full/\">\n  Standalone\n</a>\n\n#### Inline\n\n- Background Color - `background`\n- Bold - `bold`\n- Color - `color`\n- Font - `font`\n- Inline Code - `code`\n- Italic - `italic`\n- Link - `link`\n- Size - `size`\n- Strikethrough - `strike`\n- Superscript/Subscript - `script`\n- Underline - `underline`\n\n#### Block\n\n- Blockquote - `blockquote`\n- Header - `header`\n- Indent - `indent`\n- List - `list`\n- Text Alignment - `align`\n- Text Direction - `direction`\n- Code Block - `code-block`\n\n#### Embeds\n\n- Formula - `formula` (requires [KaTeX](https://katex.org/))\n- Image - `image`\n- Video - `video`\n"
  },
  {
    "path": "packages/website/content/docs/guides/building-a-custom-module.mdx",
    "content": "---\ntitle: Building a Custom Module\n---\n\nQuill's core strength as an editor is its rich API and powerful customization capabilities. As you implement functionality on top of Quill's API, it may be convenient to organize this as a module. For the purpose of this guide, we will walk through one way to build a word counter module, a commonly found feature in many word processors.\n\n<Hint>\nInternally modules are how much of Quill's functionality is organized. You can overwrite these default [modules](/docs/modules/) by implementing your own and registering it with the same name.\n</Hint>\n\n### Counting Words\n\nAt its core a word counter simply counts the number of words in the editor and displays this value in some UI. Thus we need to:\n\n1. Listen for text changes in Quill.\n1. Count the number of words.\n1. Display this value.\n\nLet's jump straight in with a complete example!\n\n<Sandpack\n  defaultShowPreview\n  externalResources={[\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.js\",\n  ]}\n  files={{\n    'index.html': `\n<link href=\"/index.css\" rel=\"stylesheet\">\n\n<div id=\"editor\"></div>\n<div id=\"counter\">0</div>\n\n<script src=\"/index.js\"></script>\n    `,\n    'index.css': `\n#editor {\n  border: 1px solid #ccc;\n}\n\n#counter {\n  border: 1px solid #ccc;\n  border-width: 0px 1px 1px 1px;\n  color: #aaa;\n  padding: 5px 15px;\n  text-align: right;\n}\n    `,\n    'index.js': `\nfunction Counter(quill, options) {\n  const container = document.querySelector('#counter');\n  quill.on(Quill.events.TEXT_CHANGE, () => {\n    const text = quill.getText();\n    // There are a couple issues with counting words\n    // this way but we'll fix these later\n    container.innerText = text.split(/\\\\s+/).length;\n  });\n}\n\nQuill.register('modules/counter', Counter);\n\n// We can now initialize Quill with something like this:\nconst quill = new Quill('#editor', {\n  modules: {\n    counter: true\n  }\n});\n    `\n  }}\n/>\n\nThat's all it takes to add a custom module to Quill! A function can be [registered](/docs/api/#quillregistermodule/) as a module and it will be passed the corresponding Quill editor object along with any options.\n\n### Using Options\n\nModules are passed an options object that can be used to fine tune the desired behavior. We can use this to accept a selector for the counter container instead of a hard-coded string. Let's also customize the counter to either count words or characters:\n\n<Sandpack\n  defaultShowPreview\n  externalResources={[\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.js\",\n  ]}\n  files={{\n    'index.html': `\n<link href=\"/index.css\" rel=\"stylesheet\">\n\n<div id=\"editor\"></div>\n<div id=\"counter\">0</div>\n\n<script src=\"/index.js\"></script>\n    `,\n    'index.css': `\n#editor {\n  border: 1px solid #ccc;\n}\n\n#counter {\n  border: 1px solid #ccc;\n  border-width: 0px 1px 1px 1px;\n  color: #aaa;\n  padding: 5px 15px;\n  text-align: right;\n}\n    `,\n    'index.js': `\nfunction Counter(quill, options) {\n  const container = document.querySelector(options.container);\n  quill.on(Quill.events.TEXT_CHANGE, () => {\n    const text = quill.getText();\n    if (options.unit === 'word') {\n      container.innerText = text.split(/\\\\s+/).length + ' words';\n    } else {\n      container.innerText = text.length + ' characters';\n    }\n\n  });\n}\n\nQuill.register('modules/counter', Counter);\n\n// We can now initialize Quill with something like this:\nconst quill = new Quill('#editor', {\n  modules: {\n    counter: {\n      container: '#counter',\n      unit: 'word'\n    }\n  }\n});\n    `\n  }}\n/>\n\n### Constructors\n\nSince any function can be registered as a Quill module, we could have implemented our counter as an ES5 constructor or ES6 class. This allows us to access and utilize the module directly.\n\n<Sandpack\n  defaultShowPreview\n  externalResources={[\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.js\",\n  ]}\n  files={{\n    'index.html': `\n<link href=\"/index.css\" rel=\"stylesheet\">\n\n<div id=\"editor\"></div>\n<div id=\"counter\">0</div>\n\n<script src=\"/index.js\"></script>\n    `,\n    'index.css': `\n#editor {\n  border: 1px solid #ccc;\n}\n\n#counter {\n  border: 1px solid #ccc;\n  border-width: 0px 1px 1px 1px;\n  color: #aaa;\n  padding: 5px 15px;\n  text-align: right;\n}\n    `,\n    'index.js': `\nclass Counter {\n  constructor(quill, options) {\n    const container = document.querySelector(options.container);\n    quill.on(Quill.events.TEXT_CHANGE, () => {\n      const text = quill.getText();\n      if (options.unit === 'word') {\n        container.innerText = text.split(/\\\\s+/).length + ' words';\n      } else {\n        container.innerText = text.length + ' characters';\n      }\n\n    });\n  }\n\n  calculate() {\n    const text = this.quill.getText();\n\n    return this.options.unit === 'word' ?\n      text.split(/\\\\s+/).length :\n      text.length;\n  }\n}\n\nQuill.register('modules/counter', Counter);\n\n// We can now initialize Quill with something like this:\nconst quill = new Quill('#editor', {\n  modules: {\n    counter: {\n      container: '#counter',\n      unit: 'word'\n    }\n  }\n});\n    `\n  }}\n/>\n\n### Wrapping It All Up\n\nNow let's polish off the module in ES6 and fix a few pesky bugs. That's all there is to it!\n\n<Sandpack\n  defaultShowPreview\n  externalResources={[\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.js\",\n  ]}\n  files={{\n    'index.html': `\n<link href=\"/index.css\" rel=\"stylesheet\">\n\n<div id=\"editor\"></div>\n<div id=\"counter\">0</div>\n\n<script src=\"/index.js\"></script>\n    `,\n    'index.css': `\n#editor {\n  border: 1px solid #ccc;\n}\n\n#counter {\n  border: 1px solid #ccc;\n  border-width: 0px 1px 1px 1px;\n  color: #aaa;\n  padding: 5px 15px;\n  text-align: right;\n}\n    `,\n    'index.js': `\nclass Counter {\n  constructor(quill, options) {\n    this.quill = quill;\n    this.options = options;\n    this.container = document.querySelector(options.container);\n    quill.on(Quill.events.TEXT_CHANGE, this.update.bind(this));\n  }\n\n  calculate() {\n    const text = this.quill.getText();\n\n    if (this.options.unit === 'word') {\n      const trimmed = text.trim();\n      // Splitting empty text returns a non-empty array\n      return trimmed.length > 0 ? trimmed.split(/\\\\s+/).length : 0;\n    } else {\n      return text.length;\n    }\n  }\n\n  update() {\n    const length = this.calculate();\n    let label = this.options.unit;\n    if (length !== 1) {\n      label += 's';\n    }\n    this.container.innerText = \\`\\${length} \\${label}\\`;\n  }\n}\n\nQuill.register('modules/counter', Counter);\n\n// We can now initialize Quill with something like this:\nconst quill = new Quill('#editor', {\n  modules: {\n    counter: {\n      container: '#counter',\n      unit: 'word'\n    }\n  }\n});\n    `\n  }}\n/>"
  },
  {
    "path": "packages/website/content/docs/guides/cloning-medium-with-parchment.js",
    "content": "export const externalResources = [\n  'https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css',\n  'https://fonts.googleapis.com/css?family=Open+Sans%3A300,400,600,700',\n  'https://platform.twitter.com/widgets.js',\n];\n\nexport const basicHTML = `\n<link href=\"/styles.css\" rel=\"stylesheet\">\n\n<div id=\"tooltip-controls\">\n  <button id=\"bold-button\"><i class=\"fa fa-bold\"></i></button>\n  <button id=\"italic-button\"><i class=\"fa fa-italic\"></i></button>\n  <button id=\"link-button\"><i class=\"fa fa-link\"></i></button>\n  <button id=\"blockquote-button\"><i class=\"fa fa-quote-right\"></i></button>\n  <button id=\"header-1-button\"><i class=\"fa fa-header\"><sub>1</sub></i></button>\n  <button id=\"header-2-button\"><i class=\"fa fa-header\"><sub>2</sub></i></button>\n</div>\n<div id=\"sidebar-controls\">\n  <button id=\"image-button\"><i class=\"fa fa-camera\"></i></button>\n  <button id=\"video-button\"><i class=\"fa fa-play\"></i></button>\n  <button id=\"tweet-button\"><i class=\"fa fa-twitter\"></i></button>\n  <button id=\"divider-button\"><i class=\"fa fa-minus\"></i></button>\n</div>\n`;\n\nexport const html = `\n<link href=\"{{site.cdn}}/quill.core.css\" rel=\"stylesheet\" />\n<script src=\"{{site.cdn}}/quill.core.js\"></script>\n${basicHTML}\n<div id=\"editor\">Tell your story...</div>\n\n<script type=\"module\" src=\"/index.js\"></script>\n`;\n\nexport const basicCSS = `\n#editor {\n  border: 1px solid #ccc;\n  font-family: 'Open Sans', Helvetica, sans-serif;\n  font-size: 1.2em;\n  height: 180px;\n  margin: 0 auto;\n  width: 450px;\n}\n\n#tooltip-controls, #sidebar-controls {\n  text-align: center;\n}\n\nbutton {\n  background: transparent;\n  border: none;\n  cursor: pointer;\n  display: inline-block;\n  font-size: 18px;\n  padding: 0;\n  height: 32px;\n  width: 32px;\n  text-align: center;\n}\nbutton:active, button:focus {\n  outline: none;\n}\n`;\n\nexport const boldBlot = `\nconst Inline = Quill.import('blots/inline');\n\nclass BoldBlot extends Inline {\n  static blotName = 'bold';\n  static tagName = 'strong';\n}\n\nQuill.register(BoldBlot);\n`;\n\nexport const italicBlot = `\nconst Inline = Quill.import('blots/inline');\n\nclass ItalicBlot extends Inline {\n  static blotName = 'italic';\n  static tagName = 'em';\n}\n\nQuill.register(ItalicBlot);\n`;\n\nexport const linkBlot = `\nconst Inline = Quill.import('blots/inline');\n\nclass LinkBlot extends Inline {\n  static blotName = 'link';\n  static tagName = 'a';\n\n  static create(url) {\n    let node = super.create();\n    // Sanitize url if desired\n    node.setAttribute('href', url);\n    // Okay to set other non-format related attributes\n    node.setAttribute('target', '_blank');\n    return node;\n  }\n  \n  static formats(node) {\n    // We will only be called with a node already\n    // determined to be a Link blot, so we do\n    // not need to check ourselves\n    return node.getAttribute('href');\n  }\n}\n\nQuill.register(LinkBlot);\n`;\n\nexport const blockquoteBlot = `\nconst Block = Quill.import('blots/block');\n\nclass BlockquoteBlot extends Block {\n  static blotName = 'blockquote';\n  static tagName = 'blockquote';\n}\n\nQuill.register(BlockquoteBlot);\n`;\n\nexport const headerBlot = `\nconst Block = Quill.import('blots/block');\n\nclass HeaderBlot extends Block {\n  static blotName = 'header';\n  static tagName = ['h1', 'h2'];\n}\n\nQuill.register(HeaderBlot);\n`;\n\nexport const cssWithBlockquoteAndHeader = `\n${basicCSS}\n\n#editor h1 + p,\n#editor h2 + p {\n  margin-top: 0.5em; \n}\n#editor blockquote {\n  border-left: 4px solid #111;\n  padding-left: 1em;\n}\n`;\n\nexport const dividerBlot = `\nconst BlockEmbed = Quill.import('blots/block/embed');\n\nclass DividerBlot extends BlockEmbed {\n  static blotName = 'divider';\n  static tagName = 'hr';\n}\n\nQuill.register(DividerBlot);\n`;\n\nexport const imageBlot = `\nconst BlockEmbed = Quill.import('blots/block/embed');\n\nclass ImageBlot extends BlockEmbed {\n  static blotName = 'image';\n  static tagName = 'img';\n\n  static create(value) {\n    let node = super.create();\n    node.setAttribute('alt', value.alt);\n    node.setAttribute('src', value.url);\n    return node;\n  }\n  \n  static value(node) {\n    return {\n      alt: node.getAttribute('alt'),\n      url: node.getAttribute('src')\n    };\n  }\n}\n\nQuill.register(ImageBlot);\n`;\n\nexport const videoBlot = `\nconst BlockEmbed = Quill.import('blots/block/embed');\n\nclass VideoBlot extends BlockEmbed {\n  static blotName = 'video';\n  static tagName = 'iframe';\n\n  static create(url) {\n    let node = super.create();\n    node.setAttribute('src', url);\n    node.setAttribute('frameborder', '0');\n    node.setAttribute('allowfullscreen', true);\n    return node;\n  }\n  \n  static formats(node) {\n    let format = {};\n    if (node.hasAttribute('height')) {\n      format.height = node.getAttribute('height');\n    }\n    if (node.hasAttribute('width')) {\n      format.width = node.getAttribute('width');\n    }\n    return format;\n  }\n  \n  static value(node) {\n    return node.getAttribute('src');\n  }\n  \n  format(name, value) {\n    if (name === 'height' || name === 'width') {\n      if (value) {\n        this.domNode.setAttribute(name, value);\n      } else {\n        this.domNode.removeAttribute(name, value);\n      }\n    } else {\n      super.format(name, value);\n    }\n  }\n}\n\nQuill.register(VideoBlot);\n`;\n\nexport const tweetBlot = `\nconst BlockEmbed = Quill.import('blots/block/embed');\n\nclass TweetBlot extends BlockEmbed {\n  static blotName = 'tweet';\n  static tagName = 'div';\n  static className = 'tweet';\n\n  static create(id) {\n    let node = super.create();\n    node.dataset.id = id;\n    twttr.widgets.createTweet(id, node);\n    return node;\n  }\n  \n  static value(domNode) {\n    return domNode.dataset.id;\n  }\n}\n\nQuill.register(TweetBlot);\n`;\n"
  },
  {
    "path": "packages/website/content/docs/guides/cloning-medium-with-parchment.mdx",
    "content": "---\ntitle: Cloning Medium with Parchment\n---\n\nTo provide a consistent editing experience, you need both consistent data and predictable behaviors. The DOM unfortunately lacks both of these. The solution for modern editors is to maintain their own document model to represent their contents. [Parchment](https://github.com/quilljs/parchment/) is that solution for Quill. It is organized in its own codebase with its own API layer. Through Parchment you can customize the content and formats Quill recognizes, or add entirely new ones.\n\nIn this guide, we will use the building blocks provided by Parchment and Quill to replicate the editor on Medium. We will start with the bare bones of Quill, without any themes, extraneous modules, or formats. At this basic level, Quill only understands plain text. But by the end of this guide, links, videos, and even tweets will be understood.\n\n## Groundwork\n\nLet's start without even using Quill, with just a textarea and button, hooked up to a dummy event listener. We'll use jQuery for convenience throughout this guide, but neither Quill nor Parchment depends on this. We'll also add some basic styling, with the help of [Google Fonts](https://fonts.google.com/) and [Font Awesome](https://fontawesome.io/). None of this has anything to do with Quill or Parchment, so we'll move through quickly.\n\n<Sandpack\n  externalResources={scope.externalResources}\n  defaultShowPreview\n  showFileTree\n  files={{\n    'index.html': `\n${scope.basicHTML}\n<textarea id=\"editor\">Tell your story...</textarea>\n\n<script type=\"module\" src=\"/index.js\"></script>\n    `,\n    'styles.css': `\n#editor {\n  display: block;\n  font-family: 'Open Sans', Helvetica, sans-serif;\n  font-size: 1.2em;\n  height: 180px;\n  margin: 0 auto;\n  width: 450px;\n}\n\n#tooltip-controls, #sidebar-controls {\n  text-align: center;\n}\n\nbutton {\n  background: transparent;\n  border: none;\n  cursor: pointer;\n  display: inline-block;\n  font-size: 18px;\n  padding: 0;\n  height: 32px;\n  width: 32px;\n  text-align: center;\n}\nbutton:active, button:focus {\n  outline: none;\n}\n    `,\n    'index.js': `\ndocument.querySelectorAll('button').forEach((button) => {\n  button.addEventListener('click', () => {\n    alert('Click!');\n  });\n});\n    `\n  }}\n/>\n\n## Adding Quill Core\n\nNext, we'll replace the textarea with Quill core, absent of themes, formats and extraneous modules. Open up your developer console to inspect the demo while you type into the editor. You can see the basic building blocks of a Parchment document at work.\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  files={{\n    'index.html': scope.html,\n    'styles.css': `\n${scope.basicCSS}\n    `,\n    'index.js': `\ndocument.querySelectorAll('button').forEach((button) => {\n  button.addEventListener('click', () => {\n    alert('Click!');\n  });\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\n\nLike the DOM, a Parchment document is a tree. Its nodes, called Blots, are an abstraction over DOM Nodes. A few blots are already defined for us: Scroll, Block, Inline, Text and Break. As you type, a Text blot is synchronized with the corresponding DOM Text node; enters are handled by creating a new Block blot. In Parchment, Blots that can have children must have at least one child, so empty Blocks are filled with a Break blot. This makes handling leaves simple and predictable. All this is organized under a root Scroll blot.\n\nYou cannot observe an Inline blot by just typing at this point since it does not contribute meaningful structure or formatting to the document. A valid Quill document must be canonical and compact. There is only one valid DOM tree that can represent a given document, and that DOM tree contains the minimal number of nodes.\n\nSince `<p><span>Text</span></p>` and `<p>Text</p>` represent the same content, the former is invalid and it is part of Quill's optimization process to unwrap the `<span>`. Similarly, once we add formatting, `<p><em>Te</em><em>st</em></p>` and `<p><em><em>Test</em></em></p>` are also invalid, as they are not the most compact representation.\n\nBecause of these constraints, **Quill cannot support arbitrary DOM trees and HTML changes**. But as we will see, the consistency and predicability this structure provides enables us to easily build rich editing experiences.\n\n## Basic Formatting\n\nWe mentioned earlier that an Inline does not contribute formatting. This is the exception, rather than the rule, made for the base Inline class. The base Block blot works the same way for block level elements.\n\nTo implement bold and italics, we need only to inherit from Inline, set the `blotName` and `tagName`, and register it with Quill. For a compelete reference of the signatures of inherited and static methods and variables, take a look at [Parchment](https://github.com/quilljs/parchment/).\n\n```js\nconst Inline = Quill.import('blots/inline');\n\nclass BoldBlot extends Inline {\n  static blotName = 'bold';\n  static tagName = 'strong';\n}\n\nclass ItalicBlot extends Inline {\n  static blotName = 'italic';\n  static tagName = 'em';\n}\n\nQuill.register(BoldBlot);\nQuill.register(ItalicBlot);\n```\n\nWe follow Medium's example here in using `strong` and `em` tags but you could just as well use `b` and `i` tags. The name of the blot will be used as the name of the format by Quill. By registering our blots, we can now use Quill's full API on our new formats:\n\n```js\nQuill.register(BoldBlot);\nQuill.register(ItalicBlot);\n\nconst quill = new Quill('#editor');\n\nquill.insertText(0, 'Test', { bold: true });\nquill.formatText(0, 4, 'italic', true);\n// If we named our italic blot \"myitalic\", we would call\n// quill.formatText(0, 4, 'myitalic', true);\n```\n\nLet's get rid of our dummy button handler and hook up the bold and italic buttons to Quill's [`format()`](/docs/api/#format). We will hardcode `true` to always add formatting for simplicity. In your application, you can use [`getFormat()`](/docs/api/#getformat) to retrieve the current formatting over a arbitrary range to decide whether to add or remove a format. The [Toolbar](/docs/modules/toolbar/) module implements this for Quill, and we will not reimplement it here.\n\nOpen your developer console and try out Quill's [APIs](/docs/api) on your new bold and italic formats! Make sure to set the context to the correct CodePen iframe to be able to access the `quill` variable in the demo.\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"index.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': `\n${scope.basicCSS}\n    `,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\nNote that if you apply both bold and italic to some text, regardless of what order you do so, Quill wraps the `<strong>` tag outside of the `<em>` tag, in a consistent order.\n\n## Links\n\nLinks are slightly more complicated, since we need more than a boolean to store the link url. This affects our Link blot in two ways: creation and format retrieval. We will represent the url as a string value, but we could easily do so in other ways, such as an object with a url key, allowing for other key/value pairs to be set and define a link. We will demonstrate this later with [images](#images).\n\n```js\nclass LinkBlot extends Inline {\n  static blotName = 'link';\n  static tagName = 'a';\n\n  static create(value) {\n    const node = super.create();\n    // Sanitize url value if desired\n    node.setAttribute('href', value);\n    // Okay to set other non-format related attributes\n    // These are invisible to Parchment so must be static\n    node.setAttribute('target', '_blank');\n    return node;\n  }\n\n  static formats(node) {\n    // We will only be called with a node already\n    // determined to be a Link blot, so we do\n    // not need to check ourselves\n    return node.getAttribute('href');\n  }\n}\n\nQuill.register(LinkBlot);\n```\n\nNow we can hook our link button up to a fancy `prompt`, again to keep things simple, before passing to Quill's `format()`.\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"formats/linkBlot.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': `\n${scope.basicCSS}\n    `,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'formats/linkBlot.js': scope.linkBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\nimport './formats/linkBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nonClick('#link-button', () => {\n  const value = prompt('Enter link URL');\n  quill.format('link', value);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\n## Blockquote and Headers\n\nBlockquotes are implemented the same way as Bold blots, except we will inherit from Block, the base block level Blot. While Inline blots can be nested, Block blots cannot. Instead of wrapping, Block blots replace one another when applied to the same text range.\n\n```js\nconst Block = Quill.import('blots/block');\n\nclass BlockquoteBlot extends Block {\n  static blotName = 'blockquote';\n  static tagName = 'blockquote';\n}\n```\n\nHeaders are implemented exactly the same way, with only one difference: it can be represented by more than one DOM element. The value of the format by default becomes the tagName, instead of just `true`. We can customize this by extending `formats()`, similar to how we did so for [links](#links).\n\n```js\nclass HeaderBlot extends Block {\n  static blotName = 'header';\n  // Medium only supports two header sizes, so we will only demonstrate two,\n  // but we could easily just add more tags into this array\n  static tagName = ['H1', 'H2'];\n\n  static formats(node) {\n    return HeaderBlot.tagName.indexOf(node.tagName) + 1;\n  }\n}\n```\n\nLet's hook these new blots up to their respective buttons and add some CSS for the `<blockquote>` tag.\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"formats/blockquoteBlot.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': `\n${scope.cssWithBlockquoteAndHeader}\n    `,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'formats/linkBlot.js': scope.linkBlot,\n    'formats/blockquoteBlot.js': scope.blockquoteBlot,\n    'formats/headerBlot.js': scope.headerBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\nimport './formats/linkBlot.js';\nimport './formats/blockquoteBlot.js';\nimport './formats/headerBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nonClick('#link-button', () => {\n  const value = prompt('Enter link URL');\n  quill.format('link', value);\n});\n\nonClick('#blockquote-button', () => {\n  quill.format('blockquote', true);\n});\n\nonClick('#header-1-button', () => {\n  quill.format('header', 1);\n});\n\nonClick('#header-2-button', () => {\n  quill.format('header', 2);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\nTry setting some text to H1, and in your console, run `quill.getContents()`. You will see our custom static `formats()` function at work. Make sure to set the context to the correct CodePen iframe to be able to access the `quill` variable in the demo.\n\n## Dividers\n\nNow let's implement our first leaf Blot. While our previous Blot examples contribute formatting and implement `format()`, leaf Blots contribute content and implement `value()`. Leaf Blots can either be Text or Embed Blots, so our section divider will be an Embed. Once created, Embed Blots' value is immutable, requiring deletion and reinsertion to change the content at that location.\n\nOur methodology is similar to before, except we inherit from a BlockEmbed. Embed also exists under `blots/embed`, but is meant for inline level blots. We want the block level implementation instead for dividers.\n\n```js\nconst BlockEmbed = Quill.import('blots/block/embed');\n\nclass DividerBlot extends BlockEmbed {\n  static blotName = 'divider';\n  static tagName = 'hr';\n}\n```\n\nOur click handler calls [`insertEmbed()`](/docs/api/#insertembed), which does not as conveniently determine, save, and restore the user selection for us like [`format()`](/docs/api/#format) does, so we have to do a little more work to preserve selection ourselves. In addition, when we try to insert a BlockEmbed in the middle of the Block, Quill splits the Block for us. To make this behavior more clear, we will explicitly split the block oursevles by inserting a newline before inserting the divider. Take a look at the Babel tab in the CodePen for specifics.\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"formats/dividerBlot.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': scope.cssWithBlockquoteAndHeader,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'formats/linkBlot.js': scope.linkBlot,\n    'formats/blockquoteBlot.js': scope.blockquoteBlot,\n    'formats/headerBlot.js': scope.headerBlot,\n    'formats/dividerBlot.js': scope.dividerBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\nimport './formats/linkBlot.js';\nimport './formats/blockquoteBlot.js';\nimport './formats/headerBlot.js';\nimport './formats/dividerBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nonClick('#link-button', () => {\n  const value = prompt('Enter link URL');\n  quill.format('link', value);\n});\n\nonClick('#blockquote-button', () => {\n  quill.format('blockquote', true);\n});\n\nonClick('#header-1-button', () => {\n  quill.format('header', 1);\n});\n\nonClick('#header-2-button', () => {\n  quill.format('header', 2);\n});\n\nonClick('#divider-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\n## Images\n\nImages can be added with what we learned building the [Link](#links) and [Divider](#divider) blots. We will use an object for the value to show how this is supported. Our button handler to insert images will use a static value, so we are not distracted by tooltip UI code irrelevant to [Parchment](https://github.com/quilljs/parchment/), the focus of this guide.\n\n```js\nconst BlockEmbed = Quill.import('blots/block/embed');\n\nclass ImageBlot extends BlockEmbed {\n  static blotName = 'image';\n  static tagName = 'img';\n\n  static create(value) {\n    const node = super.create();\n    node.setAttribute('alt', value.alt);\n    node.setAttribute('src', value.url);\n    return node;\n  }\n\n  static value(node) {\n    return {\n      alt: node.getAttribute('alt'),\n      url: node.getAttribute('src')\n    };\n  }\n}\n```\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"formats/imageBlot.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': scope.cssWithBlockquoteAndHeader,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'formats/linkBlot.js': scope.linkBlot,\n    'formats/blockquoteBlot.js': scope.blockquoteBlot,\n    'formats/headerBlot.js': scope.headerBlot,\n    'formats/dividerBlot.js': scope.dividerBlot,\n    'formats/imageBlot.js': scope.imageBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\nimport './formats/linkBlot.js';\nimport './formats/blockquoteBlot.js';\nimport './formats/headerBlot.js';\nimport './formats/dividerBlot.js';\nimport './formats/imageBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nonClick('#link-button', () => {\n  const value = prompt('Enter link URL');\n  quill.format('link', value);\n});\n\nonClick('#blockquote-button', () => {\n  quill.format('blockquote', true);\n});\n\nonClick('#header-1-button', () => {\n  quill.format('header', 1);\n});\n\nonClick('#header-2-button', () => {\n  quill.format('header', 2);\n});\n\nonClick('#divider-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nonClick('#image-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'image', {\n    alt: 'Quill Cloud',\n    url: 'https://quilljs.com/0.20/assets/images/cloud.png'\n  }, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\n\n## Videos\n\nWe will implement videos in a similar way as we did [images](#images). We could use the HTML5 `<video>` tag but we cannot play YouTube videos this way, and since this is likely the more common and relevant use case, we will use an `<iframe>` to support this. We do not have to here, but if you want multiple Blots to use the same tag, you can use `className` in addition to `tagName`, demonstrated in the next [Tweet](#tweet) example.\n\nAdditionally we will add support for widths and heights, as unregistered formats. Formats specific to Embeds do not have to be registered separately, as long as there is no namespace collision with registered formats. This works since Blots just pass unknown formats to its children, eventually reaching the leaves. This also allows different Embeds to handle unregistered formats differently. For example, our [image](#images) embed from earlier could have recognized and handled the `width` format differently than our video does here.\n\n```js\nclass VideoBlot extends BlockEmbed {\n  static blotName = 'video';\n  static tagName = 'iframe';\n\n  static create(url) {\n    const node = super.create();\n    node.setAttribute('src', url);\n    // Set non-format related attributes with static values\n    node.setAttribute('frameborder', '0');\n    node.setAttribute('allowfullscreen', true);\n\n    return node;\n  }\n\n  static formats(node) {\n    // We still need to report unregistered embed formats\n    const format = {};\n    if (node.hasAttribute('height')) {\n      format.height = node.getAttribute('height');\n    }\n    if (node.hasAttribute('width')) {\n      format.width = node.getAttribute('width');\n    }\n    return format;\n  }\n\n  static value(node) {\n    return node.getAttribute('src');\n  }\n\n  format(name, value) {\n    // Handle unregistered embed formats\n    if (name === 'height' || name === 'width') {\n      if (value) {\n        this.domNode.setAttribute(name, value);\n      } else {\n        this.domNode.removeAttribute(name, value);\n      }\n    } else {\n      super.format(name, value);\n    }\n  }\n}\n```\n\nNote if you open your console and call [`getContents`](/docs/api/#getcontents), Quill will report the video as:\n\n```js\n{\n  ops: [{\n    insert: {\n      video: 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0'\n    },\n    attributes: {\n      height: '170',\n      width: '400'\n    }\n  }]\n}\n```\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"formats/videoBlot.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': scope.cssWithBlockquoteAndHeader,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'formats/linkBlot.js': scope.linkBlot,\n    'formats/blockquoteBlot.js': scope.blockquoteBlot,\n    'formats/headerBlot.js': scope.headerBlot,\n    'formats/dividerBlot.js': scope.dividerBlot,\n    'formats/imageBlot.js': scope.imageBlot,\n    'formats/videoBlot.js': scope.videoBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\nimport './formats/linkBlot.js';\nimport './formats/blockquoteBlot.js';\nimport './formats/headerBlot.js';\nimport './formats/dividerBlot.js';\nimport './formats/imageBlot.js';\nimport './formats/videoBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nonClick('#link-button', () => {\n  const value = prompt('Enter link URL');\n  quill.format('link', value);\n});\n\nonClick('#blockquote-button', () => {\n  quill.format('blockquote', true);\n});\n\nonClick('#header-1-button', () => {\n  quill.format('header', 1);\n});\n\nonClick('#header-2-button', () => {\n  quill.format('header', 2);\n});\n\nonClick('#divider-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nonClick('#image-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'image', {\n    alt: 'Quill Cloud',\n    url: 'https://quilljs.com/0.20/assets/images/cloud.png'\n  }, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nonClick('#video-button', () => {\n  let range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  let url = 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0';\n  quill.insertEmbed(range.index + 1, 'video', url, Quill.sources.USER);\n  quill.formatText(range.index + 1, 1, { height: '170', width: '400' });\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\n## Tweets\n\nMedium supports many embed types, but we will just focus on Tweets for this guide. The Tweet blot is implemented almost exactly the same as [images](#images). We take advantage of the fact that Embed blots do not have to correspond to a void node. It can be any arbitrary node and Quill will treat it like a void node and not traverse its children or descendants. This allows us to use a `<div>` and the native Twitter JavaScript library to do what it pleases within the `<div>` container we specify.\n\nSince our root Scroll Blot also uses a `<div>`, we also specify a `className` to disambiguate. Note Inline blots use `<span>` and Block Blots use `<p>` by default, so if you would like to use these tags for your custom Blots, you will have to specify a `className` in addition to a `tagName`.\n\nWe use the Tweet id as the value defining our Blot. Again our click handler uses a static value to avoid distraction from irrelevant UI code.\n\n```js\nclass TweetBlot extends BlockEmbed {\n  static blotName = 'tweet';\n  static tagName = 'div';\n  static className = 'tweet';\n\n  static create(id) {\n    const node = super.create();\n    node.dataset.id = id;\n    // Allow twitter library to modify our contents\n    twttr.widgets.createTweet(id, node);\n    return node;\n  }\n\n  static value(domNode) {\n    return domNode.dataset.id;\n  }\n}\n```\n\n<Sandpack\n  externalResources={scope.externalResources}\n  showFileTree\n  defaultShowPreview\n  activeFile=\"formats/tweetBlot.js\"\n  files={{\n    'index.html': scope.html,\n    'styles.css': scope.cssWithBlockquoteAndHeader,\n    'formats/boldBlot.js': scope.boldBlot,\n    'formats/italicBlot.js': scope.italicBlot,\n    'formats/linkBlot.js': scope.linkBlot,\n    'formats/blockquoteBlot.js': scope.blockquoteBlot,\n    'formats/headerBlot.js': scope.headerBlot,\n    'formats/dividerBlot.js': scope.dividerBlot,\n    'formats/imageBlot.js': scope.imageBlot,\n    'formats/videoBlot.js': scope.videoBlot,\n    'formats/tweetBlot.js': scope.tweetBlot,\n    'index.js': `\nimport './formats/boldBlot.js';\nimport './formats/italicBlot.js';\nimport './formats/linkBlot.js';\nimport './formats/blockquoteBlot.js';\nimport './formats/headerBlot.js';\nimport './formats/dividerBlot.js';\nimport './formats/imageBlot.js';\nimport './formats/videoBlot.js';\nimport './formats/tweetBlot.js';\n\nconst onClick = (selector, callback) => {\n  document.querySelector(selector).addEventListener('click', callback);\n};\n\nonClick('#bold-button', () => {\n  quill.format('bold', true);\n});\n\nonClick('#italic-button', () => {\n  quill.format('italic', true);\n});\n\nonClick('#link-button', () => {\n  const value = prompt('Enter link URL');\n  quill.format('link', value);\n});\n\nonClick('#blockquote-button', () => {\n  quill.format('blockquote', true);\n});\n\nonClick('#header-1-button', () => {\n  quill.format('header', 1);\n});\n\nonClick('#header-2-button', () => {\n  quill.format('header', 2);\n});\n\nonClick('#divider-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nonClick('#image-button', () => {\n  const range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'image', {\n    alt: 'Quill Cloud',\n    url: 'https://quilljs.com/0.20/assets/images/cloud.png'\n  }, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nonClick('#video-button', () => {\n  let range = quill.getSelection(true);\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  let url = 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0';\n  quill.insertEmbed(range.index + 1, 'video', url, Quill.sources.USER);\n  quill.formatText(range.index + 1, 1, { height: '170', width: '400' });\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nonClick('#tweet-button', () => {\n  const range = quill.getSelection(true);\n  const id = '464454167226904576';\n  quill.insertText(range.index, '\\\\n', Quill.sources.USER);\n  quill.insertEmbed(range.index + 1, 'tweet', id, Quill.sources.USER);\n  quill.setSelection(range.index + 2, Quill.sources.SILENT);\n});\n\nconst quill = new Quill('#editor');\n    `\n  }}\n/>\n\n\n{/*\n\n### Final Polish\n\nWe began with just a bunch of buttons and a Quill core that just understands plaintext. With Parchment, we are able to add bold, italic, links, blockquotes, headers, section dividers, images, videos, and even Tweets. All of this comes while maintaining a predictable and consistent document, allowing us to use Quill's powerful [APIs](/docs/api) with these new formats and content.\n\nLet's add some final polish to finish off our demo. It won't compare to Medium's UI, but we'll try to get close.\n\n<CodePen hash=\"qNJrYB\" defaultTab=\"result\" />\n\n*/}"
  },
  {
    "path": "packages/website/content/docs/guides/designing-the-delta-format.mdx",
    "content": "---\ntitle: Designing the Delta Format\n---\n\nRich text editors lack a specification to express its own contents. Until recently, most rich text editors did not even know what was in their own edit areas. These editors just pass the user HTML, along with the burden of parsing and interpretting this. At any given time, this interpretation will differ from those of major browser vendors, leading to different editing experiences for users.\n\nQuill is the first rich text editor to actually understand its own contents. Key to this is Deltas, the specification describing rich text. Deltas are designed to be easy to understand and use. We will walk through some of the thinking behind Deltas, to shed light on *why* things are the way they are.\n\nIf you are looking for a reference on *what* Deltas are, the [Delta documentation](/docs/delta/) is a more concise resource.\n\n\n## Plain Text\n\nLet's start at the basics with just plain text. There already is a ubiquitous format to store plain text: the string. Now if we want to build upon this and describe formatted text, such as when a range is bold, we need to add additional information.\n\nArrays are the only other ordered data type available, so we use an array of objects. This also allows us to leverage JSON for compatibility with a breadth of tools.\n\n```javascript\nconst content = [\n  { text: 'Hello' },\n  { text: 'World', bold: true }\n];\n```\n\nWe can add italics, underline, and other formats to the main object if we want to; but it is cleaner to separate `text` from all of this so we organize formatting under one field, which we will name `attributes`.\n\n```javascript\nconst content = [\n  { text: 'Hello' },\n  { text: 'World', attributes: { bold: true } }\n];\n```\n\n\n### Compact\n\nEven with our simple Delta format so far, it is unpredictable since the above \"Hello World\" example can be represented differently, so we cannot predict which will be generated:\n\n```javascript\nconst content = [\n  { text: 'Hel' },\n  { text: 'lo' },\n  { text: 'World', attributes: { bold: true } }\n];\n```\n\nTo solve this, we add the constraint that Deltas must be compact. With this constraint, the above representation is not a valid Delta, since it can be represented more compactly by the previous example, where \"Hel\" and \"lo\" were not separate. Similarly we cannot have `{ bold: false, italic: true, underline: null }`, because `{ italic: true }` is more compact.\n\n\n### Canonical\n\nWe have not assigned any meaning to `bold`, just that it describes some formatting for text. We could very well have used different names, such as `weighted` or `strong`, or used a different range of possible values, such as a numerical or descriptive range of weights. An example can be found in CSS, where most of these ambiguities are at play. If we saw bolded text on a page, we cannot predict if its rule set is `font-weight: bold` or `font-weight: 700`. This makes the task of parsing CSS to discern its meaning, much more complex.\n\nWe do not define the set of possible attributes, nor their meanings, but we do add an additional constraint that Deltas must be canonical. If two Deltas are equal, the content they represent must be equal, and there cannot be two unequal Deltas that represent the same content. Programmatically, this allows you to simply deep compare two Deltas to determine if the content they represent is equal.\n\nSo if we had the following, the only conclusion we can draw is `a` is different from `b`, but not what `a` or `b` means.\n\n```javascript\nconst content = [{\n  text: \"Mystery\",\n  attributes: {\n    a: true,\n    b: true\n  }\n}];\n```\n\nIt is up to the implementer to pick appropriate names:\n\n```javascript\nconst content = [{\n  text: \"Mystery\",\n  attributes: {\n    italic: true,\n    bold: true\n  }\n}];\n```\n\nThis canonicalization applies to both keys and values, `text` and `attributes`. For example, Quill by default:\n\n- Uses six character hex values to represent colors and not RGB\n- There is only one way to represent a newline which is with `\\n`, not `\\r` or `\\r\\n`\n- <code>text: \"Hello&nbsp;&nbsp;World\"</code> unambiguously means there are precisely two spaces between \"Hello\" and \"World\"\n\nSome of these choices may be customized by the user, but the canonical constraint in Deltas dictate that the choice must be unique.\n\nThis unambiguous predictability makes Deltas easier to work with, both because you have fewer cases to handle and because there are no surprises in what a corresponding Delta will look like. Long term, this makes applications using Deltas easier to understand and maintain.\n\n\n## Line Formatting\n\nLine formats affect the contents of an entire line, so they present an interesting challenge for our compact and canonical constraints. A seemingly reasonable way to represent centered text would be the following:\n\n```javascript\nconst content = [\n  { text: \"Hello\", attributes: { align: \"center\" } },\n  { text: \"\\nWorld\" }\n];\n```\n\nBut what if the user deletes the newline character? If we just naively get rid of the newline character, the Delta would now look like this:\n\n```javascript\nconst content = [\n  { text: \"Hello\", attributes: { align: \"center\" } },\n  { text: \"World\" }\n];\n```\n\nIs this line still centered? If the answer is no, then our representation is not compact, since we do not need the attribute object and can combine the two strings:\n\n```javascript\nconst content = [\n  { text: \"HelloWorld\" }\n];\n```\n\nBut if the answer is yes, then we violate the canonical constraint since any permutation of characters having an align attribute would represent the same content.\n\nSo we cannot just naively get rid of the newline character. We also have to either get rid of line attributes, or expand them to fill all characters on the line.\n\nWhat if we removed the newline from the following?\n\n```javascript\nconst content = [\n  { text: \"Hello\", attributes: { align: \"center\" } },\n  { text: \"\\n\" },\n  { text: \"World\", attributes: { align: \"right\" } }\n];\n```\n\nIt is not clear if our resulting line is aligned center or right. We could delete both or have some ordering rule to favor one over the other, but our Delta is becoming more complex and harder to work with on this path.\n\nThis problem begs for atomicity, and we find this in the *newline* character itself. But we have an off by one problem in that if we have *n* lines, we only have *n-1* newline characters.\n\nTo solve this, Quill \"adds\" a newline to all documents and always ends Deltas with \"\\n\".\n\n```javascript\n// Hello World on two lines\nconst content = [\n  { text: \"Hello\" },\n  { text: \"\\n\", attributes: { align: \"center\" } },\n  { text: \"World\" },\n  { text: \"\\n\", attributes: { align: \"right\" } }   // Deltas must end with newline\n];\n```\n\n\n## Embedded Content\n\nWe want to add embedded content like images or video. Strings were natural to use for text but we have a lot more options for embeds. Since there are different types of embeds, our choice just needs to include this type information, and then the actual content. There are many reasonable options here but we will use an object whose only key is the embed type and the value is the content representation, which may have any type or value.\n\n```javascript\nconst img = {\n  image: {\n    url: 'https://quilljs.com/logo.png'\n  }\n};\n\nconst f = {\n  formula: 'e=mc^2'\n};\n```\n\nSimilar to text, images might have some defining characteristics, and some transient ones. We used `attributes` for text content and can use the same `attributes` field for images. But because of this, we can keep the general structure we have been using, but should rename our `text` key into something more general. For reasons we will explore later, we will choose the name `insert`. Putting this all together we have:\n\n```javascript\nconst content = [{\n  insert: 'Hello'\n}, {\n  insert: 'World',\n  attributes: { bold: true }\n}, {\n  insert: {\n    image: 'https://exclamation.com/mark.png'\n  },\n  attributes: { width: '100' }\n}];\n```\n\n\n## Describing Changes\n\nAs the name Delta implies, our format can describe changes to documents, as well as documents themselves. In fact we can think of documents as the changes we would make to the empty document, to get to the one we are describing. As you might have already guessed, using Deltas to also describe changes is why we renamed `text` to `insert` earlier. We call each element in our Delta array an Operation.\n\n#### Delete\n\nTo describe deleting text, we need to know where and how many characters to delete. To delete embeds, there needs not be any special treatment, other than to understand the length of an embed. If it is anything other than one, we would then need to specify what happens when only part of an embed is deleted. There is currently no such specification, so regardless of how many pixels make up an image, how many minutes long a video is, or how many slides are in a deck; embeds are all of length **one**.\n\nOne reasonable way to describe a deletion is to explicitly store its index and length.\n\n```javascript\nconst delta = [{\n  delete: {\n    index: 4,\n    length: 1\n  }\n}, {\n  delete: {\n    index: 12,\n    length: 3\n  }\n}];\n```\n\nWe would have to order the deletions based on indexes, and ensure no ranges overlap, otherwise our canonical constraint would be violated. There are a couple other shortcomings to this index and length approach, but they are easier to appreciate after describing format changes.\n\n#### Insert\n\nNow that Deltas may be describing changes to a non-empty document, `{ insert: \"Hello\" }` is insufficient, because we do not know where \"Hello\" should be inserted. We can solve this by also adding an index, similar to `delete`.\n\n#### Format\n\nSimilar to deletes, we need to specify the range of text to format, along with the format change itself. Formatting exists in the `attributes` object, so a simple solution is to provide an additional `attributes` object to merge with the existing one. This merge is shallow to keep things simple. We have not found a use case that is compelling enough to require a deep merge and warrants the added complexity.\n\n```javascript\nconst delta = [{\n  format: {\n    index: 4,\n    length: 1\n  },\n  attributes: {\n    bold: true\n  }\n}];\n```\n\nThe special case is when we want to remove formatting. We will use `null` for this purpose, so `{ bold: null }` would mean remove the bold format. We could have specified any falsy value, but there may be legitimate use cases for an attribute value to be `0` or the empty string.\n\n**Note:** We now have to be careful with indexes at the application layer. As mentioned earlier, Deltas do not ascribe any inherent meaning to any the `attributes`' key-value pairs, nor any embed types or values. Deltas do not know an image does not have duration, text does not have alternative texts, and videos cannot be bolded. The following is a *legal* Delta that might have been the result of applying other *legal* Deltas, by an application not being careful of format ranges.\n\n```javascript\nconst delta = [{\n  insert: {\n    image: \"https://imgur.com/\"\n  },\n  attributes: {\n    duration: 600\n  }\n}, {\n  insert: \"Hello\",\n  attributes: {\n    alt: \"Funny cat photo\"\n  }\n}, {\n  insert: {\n    video: \"https://youtube.com/\"\n  },\n  attributes: {\n    bold: true\n  }\n}];\n```\n\n#### Pitfalls\n\nFirst, we should be clear that an index must refer to its position in the document **before** any Operations are applied. Otherwise, a later Operation may delete a previous insert, unformat a previous format, etc., which would violate compactness.\n\nOperations must also be strictly ordered to satisfy our canonical constraint. Ordering by index, then length, and then type is one valid way this can be accomplished.\n\nAs stated earlier, delete ranges cannot overlap. The case against overlapping format ranges is less brief, but it turns out we do not want overlapping formats either.\n\nThe number of reasons a Delta might be invalid is piling up. A better format would simply not allow such cases to be expressed at all.\n\n#### Retain\n\nIf we step back from our compactness formalities for a moment, we can describe a much simpler format to express inserting, deleting, and formatting:\n\n- A Delta would have Operations that are at least as long as the document being modified.\n- Each Operation would describe what happens to the character at that index.\n- Optional insert Operations may make the Delta longer than the document it describes.\n\nThis necessitates the creation of a new Operation, that will simply mean \"keep this character as is\". We call this a `retain`.\n\n```javascript\n// Starting with \"HelloWorld\",\n// bold \"Hello\", and insert a space right after it\nconst change = [\n  { format: true, attributes: { bold: true } },  // H\n  { format: true, attributes: { bold: true } },  // e\n  { format: true, attributes: { bold: true } },  // l\n  { format: true, attributes: { bold: true } },  // l\n  { format: true, attributes: { bold: true } },  // o\n  { insert: ' ' },\n  { retain: true },  // W\n  { retain: true },  // o\n  { retain: true },  // r\n  { retain: true },  // l\n  { retain: true }   // d\n]\n```\n\nSince every character is described, explicit indexes and lengths are no longer necessary. This makes overlapping ranges and out-of-order indexes impossible to express.\n\nTherefore, we can make the easy optimization to merge adjacent equal Operations, re-introducing *length*. If the last Operation is a `retain` we can simply drop it, for it simply instructs to \"do nothing to the rest of the document\".\n\n```javascript\nconst change = [\n  { format: 5, attributes: { bold: true } }\n  { insert: ' ' }\n]\n```\n\nFurthermore, you might notice that a `retain` is in some ways just a special case of `format`. For instance, there is no practical difference between `{ format: 1, attributes: {} }` and `{ retain: 1 }`. Compacting would drop the empty `attributes` object leaving us with just `{ format: 1 }`, creating a canonicalization conflict. Thus, in our example we will simply combine `format` and `retain`, and keep the name `retain`.\n\n```javascript\nconst change = [\n  { retain: 5, attributes: { bold: true } },\n  { insert: ' ' }\n]\n```\n\nWe now have a Delta that is very close to the current standard format.\n\n#### Ops\n\nRight now we have an easy to use JSON Array that describes rich text. This is great at the storage and transport layers, but applications could benefit from more functionality. We can add this by implementing Deltas as a class, that can be easily initialized from or exported to JSON, and then providing it with relevant methods.\n\nAt the time of Delta's inception, it was not possible to sub-class an Array. For this reason Deltas are expressed as Objects, with a single property `ops` that stores an array of Operations like the ones we have been discussing.\n\n```javascript\nconst delta = {\n  ops: [{\n    insert: 'Hello'\n  }, {\n    insert: 'World',\n    attributes: { bold: true }\n  }, {\n    insert: {\n    image: 'https://exclamation.com/mark.png'\n    },\n    attributes: { width: '100' }\n  }]\n};\n```\n\nFinally, we arrive at the [Delta format](/docs/delta/), as it exists today.\n"
  },
  {
    "path": "packages/website/content/docs/installation.mdx",
    "content": "---\ntitle: Installation\n---\n\nQuill comes ready to use in several convenient forms.\n\n## CDN\n\nA globally distributed and available CDN is provided, backed by [jsDelivr](https://www.jsdelivr.com/).\nThis is the most convenient way to get started with Quill, and requires no build steps or package managers.\n\n### Full Build\n\nFor most users, the full build is the easiest way to get started with Quill.\nIt include the core Quill library, as well as common themes, formats, and modules.\n\nTo import the full build, you will need to include the \"quill.js\" script and the stylesheet for the theme you wish to use.\n\n<Sandpack\n  files={{\n    \"index.html\": `\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\">\n\n<div id=\"editor\">\n  <h2>Demo Content</h2>\n  <p>Preset build with <code>snow</code> theme, and some common formats.</p>\n</div>\n\n<script>\n  const quill = new Quill('#editor', {\n    theme: 'snow'\n  });\n</script>\n    `\n  }}\n/>\n\n<Hint>\nLearn more about how to [customize the toolbar](/docs/formats).\n</Hint>\n\n### Core Build\n\nTo fully customize your Quill build, you can import the core library and add only the formats and modules you need.\n\n<Sandpack\n  files={{\n    \"index.html\": `\n<link href=\"{{site.cdn}}/quill.core.css\" rel=\"stylesheet\">\n<script src=\"{{site.cdn}}/quill.core.js\"></script>\n\n<div id=\"editor\">\n  <p>Core build with no theme, formatting, non-essential modules</p>\n</div>\n\n<script>\n  const quill = new Quill('#editor');\n</script>\n    `\n  }}\n/>\n\n<Hint>\nLearn more about how to [make your own formats](/guides/cloning-medium-with-parchment).\n</Hint>\n\nCDN builds expose `Quill` to the global `window` object.\n`Quill` provides an [`import()`](/docs/api#import) method for accessing components of the Quill library, including its formats, modules, or themes.\n\n## npm\n\nIf your project uses bundlers such as [Webpack](https://webpack.js.org/) or [Vite](https://vitejs.dev/), it's recommended to install Quill via [npm](https://www.npmjs.com/).\n\n```bash\nnpm install quill@{{site.version}}\n```\n\nSimilar to the CDN approach, you can import the full build from `\"quill\"` or the core build from `\"quill/core\"`.\n\n```js\nimport Quill from 'quill';\n// Or if you only need the core build\n// import Quill from 'quill/core';\n\nconst quill = new Quill('#editor');\n```\n\n<Hint>\nIf you want to use the core build, avoid importing `\"quill\"` directly throughout your project.\nDoing so results in a full build, as `\"quill\"` registers the full build's formats and modules upon import.\n</Hint>\n\n`Quill.import()` is also available for the npm build to access Quill's library.\nHowever, a more natural approach in npm enviroment is to import the formats and modules directly.\n\n```js\nimport Quill from 'quill';\n// Or if you only need the core build\n// import Quill from 'quill/core';\n\nimport { Delta } from 'quill';\n// Or if you only need the core build\n// import { Delta } from 'quill/core';\n// Or const Delta = Quill.import('delta');\n\nimport Link from 'quill/formats/link';\n// Or const Link = Quill.import('formats/link');\n```\n\n### Styles\n\nQuill's npm package also comes with the stylesheets for the core and themes, just like the CDN build.\nThose stylesheets live in the `dist` directory.\n\nYou can import them in your JavaScript files if you have a proper bundler setup.\n\n```js\nimport \"quill/dist/quill.core.css\";\n```\n\nRefer to [webpack-example](https://github.com/quilljs/webpack-example) for a sample project that uses Quill in a webpack project.\n"
  },
  {
    "path": "packages/website/content/docs/modules/clipboard.mdx",
    "content": "---\ntitle: Clipboard Module\n---\n\nThe Clipboard handles copy, cut and paste between Quill and external applications. A set of defaults exist to provide sane interpretation of pasted content, with the ability for further customization through matchers.\n\nThe Clipboard interprets pasted HTML by traversing the corresponding DOM tree in [post-order](https://en.wikipedia.org/wiki/Tree_traversal#Post-order), building up a [Delta](/docs/delta/) representation of all subtrees. At each descendant node, matcher functions are called with the DOM Node and Delta interpretation so far, allowing the matcher to return a modified Delta interpretation.\n\nFamiliarity and comfort with [Deltas](/docs/delta/) is necessary in order to effectively use matchers.\n\n\n## API\n\n#### addMatcher\n\nAdds a custom matcher to the Clipboard. Matchers using `nodeType` are called first, in the order they were added, followed by matchers using a CSS `selector`, also in the order they were added. [`nodeType`](https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType) may be `Node.ELEMENT_NODE` or `Node.TEXT_NODE`.\n\n**Methods**\n\n```javascript\naddMatcher(selector: String, (node: Node, delta: Delta) => Delta)\naddMatcher(nodeType: Number, (node: Node, delta: Delta) => Delta)\n```\n\n**Examples**\n\n```javascript\nquill.clipboard.addMatcher(Node.TEXT_NODE, (node, delta) => {\n  return new Delta().insert(node.data);\n});\n\n// Interpret a <b> tag as bold\nquill.clipboard.addMatcher('B', (node, delta) => {\n  return delta.compose(new Delta().retain(delta.length(), { bold: true }));\n});\n```\n\n### dangerouslyPasteHTML\n\nInserts content represented by HTML snippet into editor at a given index. The snippet is interpreted by Clipboard [matchers](#addMatcher), which may not produce the exactly input HTML. If no insertion index is provided, the entire editor contents will be overwritten. The [source](/docs/api/#events) may be `\"user\"`, `\"api\"`, or `\"silent\"`.\n\nImproper handling of HTML can lead to [cross site scripting (XSS)](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)) and failure to sanitize properly is both notoriously error-prone and a leading cause of web vulnerabilities. This method follows React's example and is aptly named to ensure the developer has taken the necessary precautions.\n\n**Methods**\n\n```javascript\ndangerouslyPasteHTML(html: String, source: String = 'api')\ndangerouslyPasteHTML(index: Number, html: String, source: String = 'api')\n```\n\n**Examples**\n\n```javascript\nquill.setText('Hello!');\n\nquill.clipboard.dangerouslyPasteHTML(5, '&nbsp;<b>World</b>');\n// Editor is now '<p>Hello&nbsp;<strong>World</strong>!</p>';\n```\n\n\n## Configuration\n\n### matchers\n\nAn array of matchers can be passed into Clipboard's configuration options. These will be appended after Quill's own default matchers.\n\n```javascript\nconst quill = new Quill('#editor', {\n  modules: {\n    clipboard: {\n      matchers: [\n        ['B', customMatcherA],\n        [Node.TEXT_NODE, customMatcherB]\n      ]\n    }\n  }\n});\n```\n"
  },
  {
    "path": "packages/website/content/docs/modules/history.mdx",
    "content": "---\ntitle: History Module\n---\n\nThe History module is responsible for handling undo and redo for Quill. It can be configured with the following options:\n\n## Configuration\n\n#### delay\n\n- Default: `1000`\n\nChanges occuring within the `delay` number of milliseconds are merged into a single change.\n\nFor example, with delay set to `0`, nearly every character is recorded as one change and so undo would undo one character at a time. With delay set to `1000`, undo would undo all changes that occured within the last 1000 milliseconds.\n\n#### maxStack\n\n- Default: `100`\n\nMaximum size of the history's undo/redo stack. Merged changes with the `delay` option counts as a singular change.\n\n#### userOnly\n\n- Default: `false`\n\nBy default all changes, whether originating from user input or programmatically through the API, are treated the same and change be undone or redone by the history module. If `userOnly` is set to `true`, only user changes will be undone or redone.\n\n### Example\n\n```javascript\nconst quill = new Quill('#editor', {\n  modules: {\n    history: {\n      delay: 2000,\n      maxStack: 500,\n      userOnly: true\n    },\n  },\n  theme: 'snow'\n});\n```\n\n## API\n\n#### clear\n\nClears the history stack.\n\n**Methods**\n\n```js\nclear()\n```\n\n**Examples**\n\n```js\nquill.history.clear();\n```\n\n#### cutoff\n\nNormally changes made in short succession (configured by `delay`) are merged as a single change, so that triggering an undo will undo multiple changes. Using `cutoff()` will reset the merger window so that a changes before and after `cutoff()` is called will not be merged.\n\n**Methods**\n\n```js\ncutoff()\n```\n\n**Examples**\n\n```js\nquill.history.cutoff();\n```\n\n#### undo\n\nUndo last change.\n\n**Methods**\n\n```js\nundo()\n```\n\n**Examples**\n\n```js\nquill.history.undo();\n```\n\n#### redo\n\nIf last change was an undo, redo this undo. Otherwise does nothing.\n\n**Methods**\n\n```js\nredo()\n```\n\n**Examples**\n\n```js\nquill.history.redo();\n```\n"
  },
  {
    "path": "packages/website/content/docs/modules/keyboard.mdx",
    "content": "---\ntitle: Keyboard Module\n---\n\nThe Keyboard module enables custom behavior for keyboard events in particular contexts. Quill uses this to bind formatting hotkeys and prevent undesirable browser side effects.\n\n\n### Key Bindings\n\nKeyboard handlers are bound to a particular key and key modifiers. The `key` is the JavaScript event key code, but string shorthands are allowed for alphanumeric keys and some common keys.\n\nKey modifiers include: `metaKey`, `ctrlKey`, `shiftKey` and `altKey`. In addition, `shortKey` is a platform specific modifier equivalent to `metaKey` on a Mac and `ctrlKey` on Linux and Windows.\n\nHandlers will be called with `this` bound to the keyboard instance and be passed the current selection range.\n\n```js\nquill.keyboard.addBinding({\n  key: 'b',\n  shortKey: true\n}, function(range, context) {\n  this.quill.formatText(range, 'bold', true);\n});\n\n// addBinding may also be called with one parameter,\n// in the same form as in initialization\nquill.keyboard.addBinding({\n  key: 'b',\n  shortKey: true,\n  handler: function(range, context) {\n\n  }\n});\n```\n\nIf a modifier key is `false`, it is assumed to mean that modifier is not active. You may also pass `null` to mean any value for the modifier.\n\n```js\n// Only b with no modifier will trigger\nquill.keyboard.addBinding({ key: 'b' }, handler);\n\n// Only shift+b will trigger\nquill.keyboard.addBinding({ key: ['b', 'B'], shiftKey: true }, handler);\n\n// Either b or shift+b will trigger\nquill.keyboard.addBinding({ key: ['b', 'B'], shiftKey: null }, handler);\n\n```\n\nMultiple handlers may be bound to the same key and modifier combination. Handlers will be called synchronously, in the order they were bound. By default, a handler stops propagating to the next handler, unless it explicitly returns `true`.\n\n\n```js\nquill.keyboard.addBinding({ key: 'Tab' }, function(range) {\n  // I will normally prevent handlers of the tab key\n  // Return true to let later handlers be called\n  return true;\n});\n```\n\nNote: Since Quill's default handlers are added at initialization, the only way to prevent them is to add yours in the [configuration](#configuration).\n\n\n### Context\n\nContexts enable further specification for handlers to be called only in particular scenarios. Regardless if context is specified, a context object is provided as a second parameter for all handlers.\n\n```js\n// If the user hits backspace at the beginning of list or blockquote,\n// remove the format instead delete any text\nquill.keyboard.addBinding({ key: 'Backspace' }, {\n  collapsed: true,\n  format: ['blockquote', 'list'],\n  offset: 0\n}, function(range, context) {\n  if (context.format.list) {\n    this.quill.format('list', false);\n  } else {\n    this.quill.format('blockquote', false);\n  }\n});\n```\n\n#### collapsed\n\nIf `true`, handler is called only if the user's selection is collapsed, i.e. in cursor form. If `false`, the users's selection must be non-zero length, such as when the user has highlighted text.\n\n\n#### empty\n\nIf `true`, called only if user's selection is on an empty line, `false` for a non-empty line. Note setting empty to be true implies collapsed is also true and offset is 0&mdash;otherwise the user's selection would not be on an empty line.\n\n```js\n// If the user hits enter on an empty list, remove the list instead\nquill.keyboard.addBinding({ key: 'Enter' }, {\n  empty: true,    // implies collapsed: true and offset: 0\n  format: ['list']\n}, function(range, context) {\n  this.quill.format('list', false);\n});\n```\n\n\n#### format\n\nWhen an Array, handler will be called if *any* of the specified formats are active. When an Object, *all* specified formats conditions must be met. In either case, the format property of the context parameter will be an Object of all current active formats, the same returned by `quill.getFormat()`.\n\n```js\nconst context = {\n  format: {\n    list: true,       // must be on a list, but can be any value\n    script: 'super',  // must be exactly 'super', 'sub' will not suffice\n    link: false       // cannot be in any link\n  }\n};\n```\n\n\n#### offset\n\nHandler will be only called when the user's selection starts `offset` characters from the beginning of the line. Note this is before printable keys have been applied. This is useful in combination with other context specifications.\n\n\n#### prefix\n\nRegex that must match the text immediately preceding the user's selection's start position. The text will not match cross format boundaries. The supplied `context.prefix` value will be the entire immediately preceding text, not just the regex match.\n\n```js\n// When the user types space...\nquill.keyboard.addBinding({ key: ' ' }, {\n  collapsed: true,\n  format: { list: false },  // ...on a line that's not already a list\n  prefix: /^-$/,            // ...following a '-' character\n  offset: 1,                // ...at the 1st position of the line,\n                            // otherwise handler would trigger if the user\n                            // typed hyphen+space mid sentence\n}, function(range, context) {\n  // the space character is consumed by this handler\n  // so we only need to delete the hyphen\n  this.quill.deleteText(range.index - 1, 1);\n  // apply bullet formatting to the line\n  this.quill.formatLine(range.index, 1, 'list', 'bullet');\n  // restore selection\n  this.quill.setSelection(range.index - 1);\n\n  // console.log(context.prefix) would print '-'\n});\n```\n\n#### suffix\n\nThe same as [`prefix`](#prefix) except matching text immediately following the user's selection's end position.\n\n\n### Configuration\n\nBy default, Quill comes with several useful key bindings, for example indenting lists with tabs. You can add your own upon initialization.\n\nSome bindings are essential to preventing dangerous browser defaults, such as the enter and backspace keys. You cannot remove these bindings to revert to native browser behaviors. However since bindings specified in the configuration will run before Quill's defaults, you can handle special cases and propagate to Quill's otherwise.\n\nAdding a binding with `quill.keyboard.addBinding` will not run before Quill's because the defaults bindings will have been added by that point.\n\nEach binding config must contain `key` and `handler` options, and may optionally include any of the `context` options.\n\n```javascript\nconst bindings = {\n  // This will overwrite the default binding also named 'tab'\n  tab: {\n    key: 9,\n    handler: function() {\n      // Handle tab\n    }\n  },\n\n  // There is no default binding named 'custom'\n  // so this will be added without overwriting anything\n  custom: {\n    key: ['b', 'B'],\n    shiftKey: true,\n    handler: function(range, context) {\n      // Handle shift+b\n    }\n  },\n\n  list: {\n    key: 'Backspace',\n    format: ['list'],\n    handler: function(range, context) {\n      if (context.offset === 0) {\n        // When backspace on the first character of a list,\n        // remove the list instead\n        this.quill.format('list', false, Quill.sources.USER);\n      } else {\n        // Otherwise propogate to Quill's default\n        return true;\n      }\n    }\n  }\n};\n\nconst quill = new Quill('#editor', {\n  modules: {\n    keyboard: {\n      bindings: bindings\n    }\n  }\n});\n```\n\n\n### Performance\n\nLike DOM events, Quill key bindings are blocking calls on every match, so it is a bad idea to have a very expensive handler for a very common key binding. Apply the same performance best practices as you would when attaching to common blocking DOM events, like `scroll` or `mousemove`.\n"
  },
  {
    "path": "packages/website/content/docs/modules/syntax.mdx",
    "content": "---\ntitle: Syntax Highlighter Module\n---\n\nThe Syntax Module enhances the Code Block format by automatically detecting and applying syntax highlighting. The excellent [highlight.js](https://highlightjs.org/) library is used as a dependency to parse and tokenize code blocks.\n\nIn general, you may [configure](https://highlightjs.readthedocs.io/en/latest/api.html#configure-options) highlight.js as needed. However, Quill expects and requires the `useBR` option to be `false` if you are using highlight.js &lt; v11.\n\nQuill supports highlight.js v9.12.0 and later.\n\n### Usage\n\n<Sandpack\n  defaultShowPreview\n  files={{\n  \"/index.html\":  `\n<!-- Include your favorite highlight.js stylesheet -->\n<link href=\"{{site.highlightjs}}/styles/atom-one-dark.min.css\" rel=\"stylesheet\">\n\n<!-- Include the highlight.js library -->\n<script src=\"{{site.highlightjs}}/highlight.min.js\"></script>\n\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n<script src=\"{{site.cdn}}/quill.js\"></script>\n\n<div id=\"editor\"></div>\n\n<script>\nconst quill = new Quill('#editor', {\n  modules: {\n    syntax: true,              // Include syntax module\n    toolbar: [['code-block']]  // Include button in toolbar\n  },\n  theme: 'snow'\n});\n\nconst Delta = Quill.import('delta');\nquill.setContents(\n  new Delta()\n    .insert('const language = \"JavaScript\";')\n    .insert('\\\\n', { 'code-block': 'javascript' })\n    .insert('console.log(\"I love \" + language + \"!\");')\n    .insert('\\\\n', { 'code-block': 'javascript' })\n);\n</script>\n`}}\n/>\n\n### Use npm Package\n\nIf you install highlight.js as an npm package and don't want to expose it to `window`, you need to pass it to syntax module as an option:\n\n```js\nimport Quill from 'quill';\nimport hljs from 'highlight.js';\n\nconst quill = new Quill('#editor', {\n  modules: {\n    syntax: { hljs },\n  },\n});\n```"
  },
  {
    "path": "packages/website/content/docs/modules/toolbar.mdx",
    "content": "---\ntitle: Toolbar Module\n---\n\nThe Toolbar module allow users to easily format Quill's contents.\n\n<div className=\"quill-wrapper\">\n  <div id=\"toolbar-toolbar\" className=\"toolbar\">\n    <span className=\"ql-formats\">\n      <select className=\"ql-font\">\n        <option selected></option>\n        <option value=\"serif\"></option>\n        <option value=\"monospace\"></option>\n      </select>\n      <select className=\"ql-size\">\n        <option value=\"small\"></option>\n        <option selected></option>\n        <option value=\"large\"></option>\n        <option value=\"huge\"></option>\n      </select>\n    </span>\n    <span className=\"ql-formats\">\n      <button className=\"ql-bold\"></button>\n      <button className=\"ql-italic\"></button>\n      <button className=\"ql-underline\"></button>\n      <button className=\"ql-strike\"></button>\n    </span>\n    <span className=\"ql-formats\">\n      <select className=\"ql-color\"></select>\n      <select className=\"ql-background\"></select>\n    </span>\n    <span className=\"ql-formats\">\n      <button className=\"ql-list\" value=\"ordered\"></button>\n      <button className=\"ql-list\" value=\"bullet\"></button>\n      <select className=\"ql-align\">\n        <option selected></option>\n        <option value=\"center\"></option>\n        <option value=\"right\"></option>\n        <option value=\"justify\"></option>\n      </select>\n    </span>\n    <span className=\"ql-formats\">\n      <button className=\"ql-link\"></button>\n      <button className=\"ql-image\"></button>\n    </span>\n  </div>\n  <Editor\n    style={{ display: 'none' }}\n    config={{\n      modules: {\n        toolbar: { container: '#toolbar-toolbar' },\n      },\n      theme: 'snow',\n    }}\n  />\n</div>\n\nIt can be configured with a custom container and handlers.\n\n```javascript\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: {\n      container: '#toolbar', // Selector for toolbar container\n      handlers: {\n        bold: customBoldHandler\n      }\n    }\n  }\n});\n```\n\nBecause the `container` option is so common, a top level shorthand is also allowed.\n\n```javascript\nconst quill = new Quill('#editor', {\n  modules: {\n    // Equivalent to { toolbar: { container: '#toolbar' }}\n    toolbar: '#toolbar'\n  }\n});\n```\n\n## Container\n\nToolbar controls can either be specified by a simple array of format names or a custom HTML container.\n\nTo begin with the simpler array option:\n\n```javascript\nconst toolbarOptions = ['bold', 'italic', 'underline', 'strike'];\n\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: toolbarOptions\n  }\n});\n```\n\nControls can also be grouped by one level of nesting an array. This will wrap controls in a `<span>` with class name `ql-formats`, providing structure for themes to utilize. For example [Snow](/docs/themes/#snow/) adds extra spacing between control groups.\n\n```javascript\nconst toolbarOptions = [['bold', 'italic'], ['link', 'image']];\n```\n\nButtons with custom values can be specified with an Object with the name of the format as its only key.\n\n```javascript\nconst toolbarOptions = [{ header: '3' }];\n```\n\nDropdowns are similarly specified by an Object, but with an array of possible values. CSS is used to control the visual labels for dropdown options.\n\n```javascript\n// Note false, not 'normal', is the correct value\n// quill.format('size', false) removes the format,\n// allowing default styling to work\nconst toolbarOptions = [\n  { size: [ 'small', false, 'large', 'huge' ]}\n];\n```\n\nNote [Themes](/docs/themes/) may also specify default values for dropdowns. For example, [Snow](/docs/themes/#snow/) provides a default list of 35 colors for the `color` and `background` formats, if set to an empty array.\n\n```javascript\nconst toolbarOptions = [\n  ['bold', 'italic', 'underline', 'strike'],        // toggled buttons\n  ['blockquote', 'code-block'],\n  ['link', 'image', 'video', 'formula'],\n\n  [{ 'header': 1 }, { 'header': 2 }],               // custom button values\n  [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }],\n  [{ 'script': 'sub'}, { 'script': 'super' }],      // superscript/subscript\n  [{ 'indent': '-1'}, { 'indent': '+1' }],          // outdent/indent\n  [{ 'direction': 'rtl' }],                         // text direction\n\n  [{ 'size': ['small', false, 'large', 'huge'] }],  // custom dropdown\n  [{ 'header': [1, 2, 3, 4, 5, 6, false] }],\n\n  [{ 'color': [] }, { 'background': [] }],          // dropdown with defaults from theme\n  [{ 'font': [] }],\n  [{ 'align': [] }],\n\n  ['clean']                                         // remove formatting button\n];\n\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: toolbarOptions\n  },\n  theme: 'snow'\n});\n```\n\nFor use cases requiring even more customization, you can manually create a toolbar in HTML, and pass the DOM element or selector into Quill. The `ql-toolbar` class will be added to the toolbar container and Quill attach appropriate handlers to `<button>` and `<select>` elements with a class name in the form `ql-${format}`. Buttons element may optionally have a custom `value` attribute.\n\n```html\n<!-- Create toolbar container -->\n<div id=\"toolbar\">\n  <!-- Add font size dropdown -->\n  <select class=\"ql-size\">\n    <option value=\"small\"></option>\n    <!-- Note a missing, thus falsy value, is used to reset to default -->\n    <option selected></option>\n    <option value=\"large\"></option>\n    <option value=\"huge\"></option>\n  </select>\n  <!-- Add a bold button -->\n  <button class=\"ql-bold\"></button>\n  <!-- Add subscript and superscript buttons -->\n  <button class=\"ql-script\" value=\"sub\"></button>\n  <button class=\"ql-script\" value=\"super\"></button>\n</div>\n<div id=\"editor\"></div>\n\n<!-- Initialize editor with toolbar -->\n<script>\n  const quill = new Quill('#editor', {\n    modules: {\n      toolbar: '#toolbar'\n    }\n  });\n</script>\n```\n\nNote by supplying your own HTML element, Quill searches for particular input elements, but your own inputs that have nothing to do with Quill can still be added and styled and coexist.\n\n```html\n<div id=\"toolbar\">\n  <!-- Add buttons as you would before -->\n  <button class=\"ql-bold\"></button>\n  <button class=\"ql-italic\"></button>\n\n  <!-- But you can also add your own -->\n  <button id=\"custom-button\"></button>\n</div>\n<div id=\"editor\"></div>\n\n<script>\n  const quill = new Quill('#editor', {\n    modules: {\n      toolbar: '#toolbar',\n    },\n  });\n\n  const customButton = document.querySelector('#custom-button');\n  customButton.addEventListener('click', function () {\n    console.log('Clicked!');\n  });\n</script>\n```\n\n## Handlers\n\nThe toolbar controls by default applies and removes formatting, but you can also overwrite this with custom handlers, for example in order to show external UI.\n\nHandler functions will be bound to the toolbar (so using `this` will refer to the toolbar instance) and passed the `value` attribute of the input if the corresponding format is inactive, and `false` otherwise. Adding a custom handler will overwrite the default toolbar and theme behavior.\n\n```javascript\nconst toolbarOptions = {\n  handlers: {\n    // handlers object will be merged with default handlers object\n    link: function (value) {\n      if (value) {\n        const href = prompt('Enter the URL');\n        this.quill.format('link', href);\n      } else {\n        this.quill.format('link', false);\n      }\n    }\n  }\n};\n\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: toolbarOptions\n  }\n});\n\n// Handlers can also be added post initialization\nconst toolbar = quill.getModule('toolbar');\ntoolbar.addHandler('image', showImageUI);\n```\n"
  },
  {
    "path": "packages/website/content/docs/modules.mdx",
    "content": "---\ntitle: Modules\n---\n\nModules allow Quill's behavior and functionality to be customized. Several officially supported modules are available to pick and choose from, some with additional configuration options and APIs. Refer to their respective documentation pages for more details.\n\nTo enable a module, simply include it in Quill's configuration.\n\n```javascript\nconst quill = new Quill('#editor', {\n  modules: {\n    history: {          // Enable with custom configurations\n      delay: 2500,\n      userOnly: true\n    },\n    syntax: true        // Enable with default configuration\n  }\n});\n```\n\nThe [Clipboard](/docs/modules/clipboard/), [Keyboard](/docs/modules/keyboard/), and [History](/docs/modules/history/) modules are required by Quill and do not need to be included explictly, but may be configured like any other module.\n\n\n## Extending\n\nModules may also be extended and re-registered, replacing the original module. Even required modules may be re-registered and replaced.\n\n```javascript\nconst Clipboard = Quill.import('modules/clipboard');\nconst Delta = Quill.import('delta');\n\nclass PlainClipboard extends Clipboard {\n  convert(html = null) {\n    if (typeof html === 'string') {\n      this.container.innerHTML = html;\n    }\n    let text = this.container.innerText;\n    this.container.innerHTML = '';\n    return new Delta().insert(text);\n  }\n}\n\nQuill.register('modules/clipboard', PlainClipboard, true);\n\n// Will be created with instance of PlainClipboard\nconst quill = new Quill('#editor');\n```\n\n<Hint>\nThis particular example was selected to show what is possible. It is often easier to just use an API or configuration the existing module exposes. In this example, the existing Clipboard's [addMatcher](/docs/modules/clipboard/#addmatcher) API is suitable for most paste customization scenarios.\n</Hint>\n"
  },
  {
    "path": "packages/website/content/docs/quickstart.mdx",
    "content": "---\ntitle: Quickstart\n---\n\nThe best way to get started is to try a simple example. Quill is initialized with a DOM element to contain the editor. The contents of that element will become the initial contents of Quill.\n\n  <Sandpack files={{\n  \"/index.html\":  `\n<!-- Include stylesheet -->\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n\n<!-- Create the editor container -->\n<div id=\"editor\">\n  <p>Hello World!</p>\n  <p>Some initial <strong>bold</strong> text</p>\n  <p><br /></p>\n</div>\n\n<!-- Include the Quill library -->\n<script src=\"{{site.cdn}}/quill.js\"></script>\n\n<!-- Initialize Quill editor -->\n<script>\n  const quill = new Quill('#editor', {\n    theme: 'snow'\n  });\n</script>`\n\n  }}/ >\n\nAnd that's all there is to it!\n\n## Next Steps\n\nThe real magic of Quill comes in its flexibility and extensibility. You can get an idea of what is possible by playing around with the demos throughout this site or head straight to the [Interactive Playground](/playground/). For an in-depth walkthrough, take a look at [How to Customize Quill](/guides/how-to-customize-quill/).\n"
  },
  {
    "path": "packages/website/content/docs/upgrading-to-2-0.mdx",
    "content": "---\ntitle: Upgrading to 2.0\n---\n\nQuill has been significantly modernized. Leveraging the latest browser-supported APIs, Quill now delivers a more efficient and reliable editing experience.\n\n## Quill\n\nThe Quill repository has been rewritten in TypeScript, providing an official TypeScript definition file.\n\n- If you have `@types/quill` installed, uninstall it, as it is no longer needed\n- SVG icons are now inlined in the source code, eliminating the need to set up loaders for .svg files in your bundler.\n\n### Options\n\n- `strict` *removed*\n\n    Previously some changes that were small in practice (renames) but would warrant a semver major bump would be hidden under this configuration.\n    This ended up being more confusing than helpful as we will no longer make use of this.\n\n- `registry` - added to allow multiple editors with different formats to coexist on the same page. [Learn more](/docs/registries).\n\n- `scrollingContainer` *removed*\n\n    Quill will now automatically detect the scrollable ancestor, eliminating the need to provide this option. This new behavior is more robust and works seamlessly with nested scrollable elements.\n\n## Clipboard\n\n- `convert` - API changed to include both HTML and text and previous functionality is broken into multiple method calls (`convert`, `onCapturePaste`) to allow more surface to hook into.\n- `onCapturePaste` - Added\n\n### Configuration\n\n- `matchVisual` *removed* - Previously there was a choice between using visual or semantic interpretation of pasted whitespace; now just the semantic interpretation is used. Visual matching was expensive, requiring the DOM renderer which is no longer available in the new clipboard rewrite.\n- `pasteHTML` *removed* - Deprecated alias to `dangerouslyPasteHTML`.\n\n## Keyboard\n\n- Binding `key` is no longer case insensitive. To support bindings like `key: '@'`, modifiers are taken into account so the shift modifier will affect case sensitivity.\n- Binding `key` now supports an array of keys to easily bind to multiple shortcuts.\n- Native keyboard event object is now also passed into handlers.\n\n## Parchment\n\n- All lists use `<ol>` instead of both `<ul>` and `<ol>` allowing better nesting between the two. Copied content will generate the correct semantic markup for paste into other applications.\n- Code block markup now uses `<div>` to better support syntax highlighting.\n- Static `register` method added to allow dependent chains of registration.\n- Static `formats` method now passes in `scroll`.\n- Blot constructor now requires `scroll` to be passed in.\n- Attributors are exported as top-level classes.\n\n    Instead of accessing class attributor via `Parchment.Attributor.Class`, you now use it at `Parchment.ClassAttributor`.\n    Similarly, `Parchment.Attributor.Style` is now `Parchment.StyleAttributor`, and `Parchment.Attributor.Attribute` is now `Parchment.Attributor`.\n- Exports are using full names.\n\n    Instead of `Parchment.Scroll`, you now use `Parchment.ScrollBlot`. The similar change applies to `Parchment.Embed`, `Parchment.Text`, `Parchment.Block`, `Parchment.Inline`, and more.\n\n## Delta\n\n- Support for the deprecated delta format, where embeds had integer values and list attributes had different keys, is now removed\n\n## Browser\n\n- Internet Explorer support is dropped.\n"
  },
  {
    "path": "packages/website/content/docs/why-quill.mdx",
    "content": "---\ntitle: Why Quill\n---\n\nContent creation has been at the core to the web since its beginning. The `<textarea>` provides a native and essential solution to almost any web application. But at some point you may need to add formatting to text input. This is where rich text editors come in. There are many solutions to choose from, but Quill brings a few modern ideas to consider.\n\n\n### API Driven Design\n\nRich text editors are built to help people write text. Yet surprisingly, most rich text editors have no idea what text the user composed. These editors see their content through the same lens a web developer does: the DOM. This presents an impedance mismatch since the DOM is made up of Nodes organized in an unbalanced tree, whereas text is made up of lines, words and characters.\n\nThere is no DOM API where characters is the unit of measure. With this limitation, most rich text editors cannot answer simple questions such as \"What text is in this range?\" or \"Is the cursor on bolded text?\" Trying to build rich editing experiences on top of such primitives is very difficult and frustrating.\n\nQuill was designed for editing and characters in mind, and built its APIs on top of these natural text centric units. To find out if something is bold, Quill does not require traversing the DOM looking for `<b>` or `<strong>` nodes or a font-weight style attributes&mdash;just call [`getFormat(5, 1)`](/docs/api/#getformat). All of its core [API](/docs/api) calls allow arbitrary indexes and lengths for access or modification. Its [event API](/docs/api/#events) also reports changes in an intuitive JSON format. No need to parse HTML or diff DOM trees.\n\n\n### Custom Content and Formatting\n\nIt was not far in the past that evaluating rich text editors was as simple as comparing a checklist of desired formats. The mark of a good rich text editor was simply how many formats it supported. This is still an important measure, but the lower bound is approaching infinity.\n\nText is no longer written to be printed. It is written to be rendered on the web&mdash;a much richer canvas than paper. Content can be live, interactive, or even collaborative. Only some rich text editors can even support simple media like images and videos; almost none can embed a tweet or interactive graph. Yet this is the direction the web is moving: richer and more interactive. The tools supporting content creation need to consider these use cases.\n\nQuill exposes its own document model, a powerful abstraction over the DOM, allowing for extension and customization. The upper limit on the formats and content Quill can support is unlimited. Users have already used it to add embedded slide decks, interactive checklists, and 3D models.\n\n\n### Cross Platform\n\nCross platform support is important to many JavaScript libraries, but the criteria for what this means often differs. For Quill, the bar is not just that it runs or works, it has to run or work *the same way*. Not only is functionality a cross platform consideration, but user and developer experience is as well. If some content produces a particular markup in Chrome on OSX, it will produce the same markup on IE. If hitting enter preserves bold format state in Firefox on Windows, it will be preserved on mobile Safari.\n\n\n### Easy to Use\n\nAll of these benefits come in an easy to use package. Quill ships with sane defaults you can immediately use with just a few lines of JavaScript:\n\n<SandpackWithQuillTemplate\n  files={{\n    'index.js': `\nconst quill = new Quill('#editor', {\n  modules: { toolbar: true },\n  theme: 'snow'\n});\n`\n  }}\n/>\n\nIf your application never demands it, you never have to customize Quill&mdash;just enjoy the rich and consistent experience that comes out of the box.\n\nEnjoy!\n"
  },
  {
    "path": "packages/website/env.js",
    "content": "const { version, homepage } = require('./package.json');\n\nconst cdn = process.env.NEXT_PUBLIC_LOCAL_QUILL\n  ? `http://localhost:${process.env.npm_package_config_ports_webpack}`\n  : `https://cdn.jsdelivr.net/npm/quill@${version}/dist`;\n\nmodule.exports = {\n  version,\n  cdn,\n  github: 'https://github.com/slab/quill/tree/main/packages/website/',\n  highlightjs: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0',\n  katex: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist',\n  url: homepage,\n  title: 'Quill - Your powerful rich text editor',\n  shortTitle: 'Quill Rich Text Editor',\n  description:\n    'Quill is a free, open source WYSIWYG editor built for the modern web. Completely customize it for any need with its modular architecture and expressive API.',\n  shortDescription:\n    'Quill is a free, open source rich text editor built for the modern web.',\n};\n"
  },
  {
    "path": "packages/website/next.config.mjs",
    "content": "import withMDX from '@next/mdx';\nimport env from './env.js';\n\n/** @type {import('next').NextConfig} */\nexport default withMDX()({\n  output: 'export',\n  images: {\n    unoptimized: true,\n  },\n  env: env,\n  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],\n  redirects: () => [\n    {\n      source: '/guides/upgrading-to-2-0',\n      destination: '/docs/upgrading-to-2-0',\n      permanent: true,\n    },\n    {\n      source: '/guides/why-quill',\n      destination: '/docs/why-quill',\n      permanent: true,\n    },\n    {\n      source: '/guides/how-to-customize-quill',\n      destination: '/docs/customization',\n      permanent: true,\n    },\n    {\n      source: '/guides/building-a-custom-module',\n      destination: '/docs/guides/building-a-custom-module',\n      permanent: true,\n    },\n    {\n      source: '/guides/cloning-medium-with-parchment',\n      destination: '/docs/guides/cloning-medium-with-parchment',\n      permanent: true,\n    },\n    {\n      source: '/guides/designing-the-delta-format',\n      destination: '/docs/guides/designing-the-delta-format',\n      permanent: true,\n    },\n    {\n      source: '/docs/registries',\n      destination: '/docs/customization/registries',\n      permanent: true,\n    },\n    {\n      source: '/docs/themes',\n      destination: '/docs/customization/themes',\n      permanent: true,\n    },\n  ],\n  webpack(config) {\n    // Grab the existing rule that handles SVG imports\n    const fileLoaderRule = config.module.rules.find((rule) =>\n      rule.test?.test?.('.svg'),\n    );\n\n    config.module.rules.push(\n      // Reapply the existing rule, but only for svg imports ending in ?url\n      {\n        ...fileLoaderRule,\n        test: /\\.svg$/i,\n        resourceQuery: /url/, // *.svg?url\n      },\n      // Convert all other *.svg imports to React components\n      {\n        test: /\\.svg$/i,\n        issuer: fileLoaderRule.issuer,\n        resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url\n        use: [\n          {\n            loader: '@svgr/webpack',\n            options: {\n              svgoConfig: {\n                plugins: [{ name: 'preset-default' }],\n              },\n            },\n          },\n        ],\n      },\n    );\n\n    // Modify the file loader rule to ignore *.svg, since we have it handled now.\n    fileLoaderRule.exclude = /\\.svg$/i;\n\n    return config;\n  },\n});\n"
  },
  {
    "path": "packages/website/package.json",
    "content": "{\n  \"name\": \"website\",\n  \"version\": \"2.0.3\",\n  \"description\": \"Quill official website\",\n  \"private\": true,\n  \"homepage\": \"https://quilljs.com\",\n  \"keywords\": [],\n  \"license\": \"BSD-3-Clause\",\n  \"scripts\": {\n    \"dev\": \"PORT=$npm_package_config_ports_website next dev\",\n    \"start\": \"npm run dev\",\n    \"build\": \"next build\",\n    \"lint\": \"next lint\",\n    \"serve\": \"npm run serve\"\n  },\n  \"dependencies\": {\n    \"@codesandbox/sandpack-react\": \"^2.11.3\",\n    \"@docsearch/react\": \"^3.5.2\",\n    \"@mdx-js/loader\": \"^3.0.0\",\n    \"@mdx-js/mdx\": \"^2.1.5\",\n    \"@mdx-js/react\": \"^2.3.0\",\n    \"@next/mdx\": \"^14.0.4\",\n    \"@next/third-parties\": \"^14.1.0\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/themes\": \"^2.0.3\",\n    \"@svgr/webpack\": \"^8.1.0\",\n    \"@types/mdx\": \"^2.0.10\",\n    \"classnames\": \"^2.3.2\",\n    \"eslint-config-next\": \"^14.1.0\",\n    \"lz-string\": \"^1.5.0\",\n    \"next\": \"^14.0.4\",\n    \"next-mdx-remote\": \"^4.4.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-helmet\": \"^6.1.0\",\n    \"slugify\": \"^1.6.5\"\n  },\n  \"prettier\": {\n    \"singleQuote\": true\n  },\n  \"devDependencies\": {\n    \"http-proxy\": \"^1.18.1\",\n    \"prism-react-renderer\": \"^2.3.0\",\n    \"prismjs\": \"^1.29.0\",\n    \"sass\": \"^1.55.0\"\n  }\n}\n"
  },
  {
    "path": "packages/website/public/CNAME",
    "content": "quilljs.com"
  },
  {
    "path": "packages/website/public/robots.txt",
    "content": "User-agent: *\nAllow: /\n"
  },
  {
    "path": "packages/website/src/components/ActiveLink.jsx",
    "content": "import { useRouter } from 'next/router';\nimport Link from 'next/link';\nimport React, { useState, useEffect, useCallback } from 'react';\n\nconst ActiveLink = ({\n  children,\n  activeClassName,\n  className = '',\n  activePath,\n  ...props\n}) => {\n  const { asPath, isReady } = useRouter();\n\n  const getClassName = useCallback(() => {\n    // Using URL().pathname to get rid of query and hash\n    const activePathname = asPath;\n\n    const isActive = activePath\n      ? activePathname.startsWith(activePath)\n      : linkPathname === activePathname;\n    return isActive ? `${className} ${activeClassName}`.trim() : className;\n  }, [asPath, activePath, className, activeClassName]);\n\n  const [computedClassName, setComputedClassName] = useState(getClassName());\n\n  useEffect(() => {\n    // Check if the router fields are updated client-side\n    if (isReady) {\n      const newClassName = getClassName();\n\n      if (newClassName !== computedClassName) {\n        setComputedClassName(newClassName);\n      }\n    }\n  }, [isReady, computedClassName, getClassName]);\n\n  return (\n    <Link className={computedClassName} {...props}>\n      {children}\n    </Link>\n  );\n};\n\nexport default ActiveLink;\n"
  },
  {
    "path": "packages/website/src/components/ClickOutsideHandler.jsx",
    "content": "import { useEffect, useRef } from 'react';\n\nconst TOUCH_EVENT = { react: 'onTouchStart', native: 'touchstart' };\nconst MOUSE_EVENT = { react: 'onMouseDown', native: 'mousedown' };\n\nconst setUpReactEventHandlers = (handler, props) => ({\n  ...props,\n  [TOUCH_EVENT.react]: (e) => {\n    handler();\n    props[TOUCH_EVENT.react]?.(e);\n  },\n  [MOUSE_EVENT.react]: (e) => {\n    handler();\n    props[MOUSE_EVENT.react]?.(e);\n  },\n});\n\nconst ClickOutsideHandler = ({ onClickOutside, ...props }) => {\n  const isTargetInsideReactTreeRef = useRef(false);\n\n  const onClickOutsideRef = useRef(onClickOutside);\n  useEffect(() => {\n    onClickOutsideRef.current = onClickOutside;\n  }, [onClickOutside]);\n\n  useEffect(() => {\n    const handler = (e) => {\n      if (!isTargetInsideReactTreeRef.current) {\n        onClickOutsideRef.current?.(e);\n      }\n\n      isTargetInsideReactTreeRef.current = false;\n    };\n\n    document.addEventListener(TOUCH_EVENT.native, handler, { passive: true });\n    document.addEventListener(MOUSE_EVENT.native, handler, { passive: true });\n    return () => {\n      document.removeEventListener(TOUCH_EVENT.native, handler);\n      document.removeEventListener(MOUSE_EVENT.native, handler);\n    };\n  }, []);\n\n  const handleReactEvent = () => {\n    isTargetInsideReactTreeRef.current = true;\n  };\n\n  return <div {...setUpReactEventHandlers(handleReactEvent, props)} />;\n};\n\nexport default ClickOutsideHandler;\n"
  },
  {
    "path": "packages/website/src/components/Editor.jsx",
    "content": "import { useLayoutEffect, useRef } from 'react';\nimport { withoutSSR } from './NoSSR';\n\nconst Editor = ({\n  children,\n  rootStyle,\n  config,\n  onSelectionChange,\n  onLoad,\n  ...props\n}) => {\n  const ref = useRef(null);\n  const rootStyleRef = useRef(rootStyle);\n  const onSelectionChangeRef = useRef(onSelectionChange);\n  const onLoadRef = useRef(onLoad);\n\n  useLayoutEffect(() => {\n    onSelectionChangeRef.current = onSelectionChange;\n  }, [onSelectionChange]);\n\n  useLayoutEffect(() => {\n    onLoadRef.current = onLoad;\n  }, [onLoad]);\n\n  const configRef = useRef(config);\n\n  useLayoutEffect(() => {\n    const quill = new window.Quill(ref.current, configRef.current);\n    if (rootStyleRef) {\n      Object.assign(quill.root.style, rootStyleRef.current);\n    }\n    quill.on(window.Quill.events.SELECTION_CHANGE, () => {\n      onSelectionChangeRef.current?.();\n    });\n\n    onLoadRef.current?.(quill);\n  }, []);\n\n  return (\n    <div ref={ref} {...props}>\n      {children}\n    </div>\n  );\n};\n\nexport default withoutSSR(Editor);\n"
  },
  {
    "path": "packages/website/src/components/GitHub.jsx",
    "content": "import classNames from 'classnames';\nimport { useEffect, useState } from 'react';\nimport OctocatIcon from '../svg/octocat.svg';\nimport * as styles from './GitHub.module.scss';\n\nconst placeholderCount = (37622).toLocaleString();\n\nconst GitHub = ({ dark = false }) => {\n  const [count, setCount] = useState(placeholderCount);\n\n  useEffect(() => {\n    fetch(\n      'https://api.github.com/search/repositories?q=quill+user:slab+repo:quill&sort=stars&order=desc',\n    )\n      .then((response) => response.json())\n      .then((data) => {\n        if (data.items && data.items[0].full_name === 'slab/quill') {\n          setCount(data.items[0].stargazers_count.toLocaleString());\n        }\n      });\n  }, []);\n\n  return (\n    <div className={classNames(styles.button, { [styles.isDark]: dark })}>\n      <a\n        className={styles.action}\n        target=\"_blank\"\n        title=\"Star Quill on GitHub\"\n        href=\"https://github.com/slab/quill/\"\n      >\n        <OctocatIcon />\n        Star\n      </a>\n      <a\n        className={styles.count}\n        target=\"_blank\"\n        title=\"Quill Stargazers\"\n        href=\"https://github.com/slab/quill/stargazers\"\n      >\n        {count}\n      </a>\n    </div>\n  );\n};\n\nexport default GitHub;\n"
  },
  {
    "path": "packages/website/src/components/GitHub.module.scss",
    "content": ".button {\n  background-color: #1d1e30;\n  border: 3px solid #1d1e30;\n  display: flex;\n  flex-direction: row;\n  font-family: 'Sofia Pro', sans-serif;\n  font-weight: bold;\n  letter-spacing: 0.15rem;\n  text-transform: uppercase;\n  width: min-content;\n}\n\n.action {\n  color: #fff;\n  display: flex;\n  flex-direction: row;\n  font-size: 1.33rem;\n  line-height: 32px;\n  padding: 10px 22px;\n}\n\n.action:hover {\n  color: #fff;\n}\n\n.action svg {\n  float: left;\n  height: 32px;\n  margin-right: 12px;\n  width: 32px;\n}\n\n.action path {\n  fill: #fff;\n}\n\n.count {\n  color: inherit;\n  background-color: #fff;\n  font-size: 1.75rem;\n  line-height: 32px;\n  padding: 10px 30px;\n}\n\n.button.isDark {\n  background-color: #fff;\n  border: 3px solid #fff;\n}\n\n.button.isDark .action {\n  color: #1d1e30;\n}\n\n.button.isDark .action path {\n  fill: #1d1e30;\n}\n\n.button.isDark .count {\n  background-color: #1d1e30;\n  color: #fff;\n}\n"
  },
  {
    "path": "packages/website/src/components/Header.jsx",
    "content": "import classNames from 'classnames';\nimport LogoIcon from '../svg/logo.svg';\nimport Link from 'next/link';\nimport OctocatIcon from '../svg/octocat.svg';\nimport ExternalLinkIcon from '../svg/external-link.svg';\nimport DropdownIcon from '../svg/dropdown.svg';\nimport * as styles from './Header.module.scss';\nimport { DocSearch } from '@docsearch/react';\nimport { useState } from 'react';\nimport playground from '../data/playground';\nimport docs from '../data/docs';\nimport ActiveLink from './ActiveLink';\nimport ClickOutsideHandler from './ClickOutsideHandler';\n\nconst MainNav = ({ ...props }) => {\n  return (\n    <nav {...props}>\n      <ActiveLink\n        activeClassName={styles.active}\n        activePath=\"/docs\"\n        href={docs[0].url}\n      >\n        Documentation\n      </ActiveLink>\n      <ActiveLink\n        activeClassName={styles.active}\n        activePath=\"/playground\"\n        href={playground[0].url}\n      >\n        Playground\n      </ActiveLink>\n    </nav>\n  );\n};\n\nconst VersionSelector = () => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  return (\n    <ClickOutsideHandler\n      onClickOutside={() => {\n        setIsOpen(false);\n      }}\n      className={styles.versionWrapper}\n    >\n      <div\n        role=\"button\"\n        className={styles.version}\n        onClick={() => {\n          setIsOpen(!isOpen);\n        }}\n      >\n        v{process.env.version} <DropdownIcon />\n      </div>\n      <div\n        role=\"menu\"\n        className={classNames(styles.versionDropdown, {\n          [styles.isOpen]: isOpen,\n        })}\n      >\n        <a\n          role=\"menuitem\"\n          href={`https://github.com/slab/quill/releases/tag/v${process.env.version}`}\n          className={styles.versionDropdownItem}\n          target=\"_blank\"\n        >\n          Release Notes <ExternalLinkIcon />\n        </a>\n        <a\n          role=\"menuitem\"\n          href={`https://github.com/slab/quill/blob/v${process.env.version}/.github/CONTRIBUTING.md`}\n          className={styles.versionDropdownItem}\n          target=\"_blank\"\n        >\n          Contributing <ExternalLinkIcon />\n        </a>\n        <div className={styles.versionLabel}>Previous Versions</div>\n        <a\n          role=\"menuitem\"\n          href=\"https://v1.quilljs.com\"\n          className={styles.versionDropdownItem}\n          target=\"_blank\"\n        >\n          v{'1.3.7'} <ExternalLinkIcon />\n        </a>\n      </div>\n    </ClickOutsideHandler>\n  );\n};\n\nconst Header = () => {\n  const [isNavOpen, setIsNavOpen] = useState(false);\n\n  return (\n    <header className={styles.header}>\n      <div className={styles.headerContent}>\n        <div className={styles.logo}>\n          <Link href=\"/\">\n            <LogoIcon width=\"60\" />\n          </Link>\n          <VersionSelector />\n        </div>\n        <MainNav className={styles.mainNav} />\n        <nav className={styles.secondaryNav}>\n          <a\n            href=\"https://github.com/slab/quill\"\n            target=\"_blank\"\n            title=\"Edit on GitHub\"\n          >\n            <OctocatIcon />\n          </a>\n          <DocSearch\n            appId=\"ZTZN3V01SS\"\n            indexName=\"quilljsdev\"\n            apiKey=\"7e6ff70a985e6af9bfea77c780411b9a\"\n          />\n        </nav>\n        <button\n          className={styles.mobileNavToggle}\n          onClick={() => setIsNavOpen(!isNavOpen)}\n        >\n          <span></span>\n          <span></span>\n          <span></span>\n        </button>\n      </div>\n      <div\n        className={classNames(styles.mobileNav, {\n          [styles.isNavOpen]: isNavOpen,\n        })}\n      >\n        <MainNav className={styles.mobileMainNav} />\n      </div>\n    </header>\n  );\n};\n\nexport default Header;\n"
  },
  {
    "path": "packages/website/src/components/Header.module.scss",
    "content": ".githubMenuItem {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n$linkBorderRadius: 8px;\n\n.header {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  background: #fff;\n  border-bottom: 1px solid #e2e0e0;\n  padding: 14px 0;\n  position: sticky;\n  top: 0;\n  z-index: 90;\n\n  .headerContent {\n    display: flex;\n    width: 100%;\n    max-width: 1400px;\n    padding: 0 40px;\n    justify-content: space-around;\n\n    .mainNav {\n      display: flex;\n      align-items: center;\n      margin-left: 30px;\n      font-size: 16px;\n\n      a {\n        display: block;\n        margin: 0 8px;\n        color: #323131;\n        transition: background 0.2s;\n        padding: 9px 12px 6px;\n        border-radius: $linkBorderRadius;\n        line-height: 1;\n\n        &:hover,\n        &.active {\n          background: var(--color-bg-inset);\n        }\n        &:active {\n          background: var(--color-bg-inset-emphasis);\n        }\n      }\n    }\n\n    .secondaryNav {\n      margin-left: auto;\n      display: flex;\n      align-items: center;\n\n      a {\n        margin-left: 18px;\n        display: block;\n        width: 26px;\n        transition: opacity 0.2s;\n\n        svg {\n          display: block;\n        }\n\n        &:hover {\n          opacity: 0.6;\n        }\n      }\n\n      :global {\n        .DocSearch-Button {\n          width: 200px;\n        }\n      }\n    }\n\n    .mobileNavToggle {\n      border: none;\n      padding: 0;\n      right: 0;\n      top: 7.5em;\n\n      span {\n        background-color: #333;\n        display: block;\n        height: 3px;\n        margin: 0.53em;\n        width: 2.5em;\n      }\n    }\n\n    @media (min-width: 800px) {\n    }\n  }\n\n  .searchBarContainer {\n    margin-left: auto;\n    font-size: 14px;\n\n    input {\n      width: 200px;\n      background: #eaeaea;\n      border-radius: 3px;\n      border: 0;\n      padding: 8px 12px;\n      line-height: 1;\n    }\n  }\n\n  .logo {\n    display: flex;\n    align-items: center;\n\n    a {\n      display: flex;\n      align-items: center;\n    }\n  }\n\n  :global(.DocSearch-Button-Placeholder) {\n    font-size: 14px;\n    margin-top: 2px;\n  }\n\n  :global(.DocSearch-Button-Keys) {\n    display: flex;\n    justify-content: center;\n    background: white;\n    border: 1px solid #ccc;\n    border-radius: 6px;\n    min-width: 30px;\n    text-align: center;\n    padding: 1px 5px 0;\n    margin-right: 8px;\n    height: 20px;\n  }\n\n  :global(.DocSearch-Search-Icon) {\n    color: var(--docsearch-muted-color);\n  }\n\n  :global(.DocSearch-Button-Key) {\n    margin: 0;\n    padding: 0;\n    width: auto;\n    font-family:\n      system-ui,\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Roboto,\n      Oxygen,\n      Ubuntu,\n      Cantarell,\n      'Open Sans',\n      'Helvetica Neue',\n      sans-serif;\n    line-height: 1;\n  }\n\n  :global(.DocSearch-Button-Key--pressed) {\n    box-shadow: none;\n    transform: none;\n  }\n}\n\n.versionWrapper {\n  position: relative;\n  margin-left: 4px;\n\n  .version {\n    display: flex;\n    align-items: center;\n    font-size: 13px;\n    border-radius: 8px;\n    line-height: 1;\n    background: var(--color-accent);\n    font-family: 'Sofia Pro', sans-serif;\n    transition: all 0.2s;\n    border: 2px solid transparent;\n    padding: 5px 6px 3px 8px;\n    cursor: pointer;\n    user-select: none;\n\n    &:hover {\n      background: transparent;\n      border: 2px solid var(--color-accent);\n    }\n\n    svg {\n      height: 12px;\n      width: 12px;\n      fill: #333;\n    }\n  }\n\n  .version,\n  .versionDropdown {\n    margin-left: 10px;\n  }\n\n  .versionDropdown {\n    margin-top: 8px;\n    position: absolute;\n    padding: 12px 8px 12px;\n    width: max-content;\n    text-align: left;\n    font-size: 16px;\n    transition: opacity 0.2s;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 0 1rem rgba(0, 0, 0, 0.15);\n\n    &:not(.isOpen) {\n      pointer-events: none;\n      opacity: 0;\n    }\n\n    .versionLabel {\n      padding: 0 8px;\n      color: #999;\n      text-transform: uppercase;\n      font-size: 12px;\n      border-top: 1px solid #eee;\n      padding-top: 12px;\n      user-select: none;\n\n      &:not(:first-child) {\n        margin-top: 8px;\n      }\n    }\n\n    .versionDropdownItem {\n      padding: 8px 10px 7px;\n      line-height: 1;\n      transition: background 0.2s;\n      border-radius: $linkBorderRadius;\n\n      span {\n        text-transform: uppercase;\n        font-size: 12px;\n        margin-left: 20px;\n        background: var(--color-accent);\n        padding: 4px 8px;\n        border-radius: 4px;\n        line-height: 1;\n      }\n\n      svg {\n        width: 12px;\n        height: 12px;\n        color: #ccc;\n        margin-left: 4px;\n        margin-top: -1px;\n\n        fill: #999;\n      }\n\n      &.isActive {\n        background: #f5f5f5;\n      }\n\n      &:hover {\n        background: #f5f5f5;\n      }\n    }\n  }\n}\n\n.header {\n  .mobileNav,\n  .mobileNavToggle {\n    display: none;\n  }\n\n  .mobileMainNav {\n    margin-top: 24px;\n\n    a {\n      display: block;\n      font-size: 16px;\n      padding: 8px 0;\n    }\n  }\n\n  @media (max-width: 960px) {\n    :global(.DocSearch-Button) {\n      display: none;\n    }\n  }\n\n  @media (max-width: 800px) {\n    .headerContent {\n      .mainNav,\n      .secondaryNav {\n        display: none;\n      }\n\n      & {\n        justify-content: space-between;\n      }\n    }\n\n    .mobileNavToggle {\n      display: block;\n    }\n\n    .mobileNav.isNavOpen {\n      display: block;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/website/src/components/Heading.jsx",
    "content": "import { createElement } from 'react';\nimport slug from '../utils/slug';\n\nconst EXPERIMENTAL_FLAG = ' #experimental';\n\nconst Heading = ({ level, children, anchor = 'on' }) => {\n  const tag = `h${level}`;\n\n  if (typeof children !== 'string') {\n    return createElement(tag, null, children);\n  }\n\n  const isExperimental = children.endsWith(EXPERIMENTAL_FLAG);\n  const title = isExperimental\n    ? children.slice(0, -EXPERIMENTAL_FLAG.length)\n    : children;\n  const id =\n    anchor === 'on'\n      ? slug(title) + (isExperimental ? '-experimental' : '')\n      : undefined;\n\n  return createElement(\n    tag,\n    { id },\n    <>\n      {id && <a className=\"anchor\" href={`#${id}`}></a>}\n      {title}\n      {isExperimental && <span className=\"experimental\">experimental</span>}\n    </>,\n  );\n};\n\nexport const Heading1 = ({ children, anchor }) => (\n  <Heading level={1} anchor={anchor}>\n    {children}\n  </Heading>\n);\nexport const Heading2 = ({ children, anchor }) => (\n  <Heading level={2} anchor={anchor}>\n    {children}\n  </Heading>\n);\nexport const Heading3 = ({ children, anchor }) => (\n  <Heading level={3} anchor={anchor}>\n    {children}\n  </Heading>\n);\nexport const Heading4 = ({ children, anchor }) => (\n  <Heading level={4} anchor={anchor}>\n    {children}\n  </Heading>\n);\nexport const Heading5 = ({ children, anchor }) => (\n  <Heading level={5} anchor={anchor}>\n    {children}\n  </Heading>\n);\nexport const Heading6 = ({ children, anchor }) => (\n  <Heading level={6} anchor={anchor}>\n    {children}\n  </Heading>\n);\n"
  },
  {
    "path": "packages/website/src/components/Hint.jsx",
    "content": "import * as styles from './Hint.module.scss';\n\nconst Hint = ({ children }) => {\n  return (\n    <div className={styles.container}>\n      <div className={styles.title}>Note</div>\n      {children}\n    </div>\n  );\n};\n\nexport default Hint;\n"
  },
  {
    "path": "packages/website/src/components/Hint.module.scss",
    "content": ".container {\n  padding: 0 0 0 20px;\n  border-left: 3px solid #45aad8;\n  margin: 20px 0 30px;\n\n  .title {\n    color: #45aad8;\n    margin-bottom: 4px;\n  }\n\n  :last-child {\n    margin-bottom: 0;\n  }\n}\n"
  },
  {
    "path": "packages/website/src/components/Layout.jsx",
    "content": "import LogoIcon from '../svg/logo.svg';\nimport Link from 'next/link';\nimport SEO from './SEO';\nimport Header from './Header';\nimport playground from '../data/playground';\nimport docs from '../data/docs';\n\nconst Layout = ({ children, title }) => {\n  return (\n    <>\n      <SEO title={title} />\n      <Header />\n      {children}\n      <footer>\n        <div className=\"container\">\n          <div className=\"logo row\">\n            <LogoIcon />\n          </div>\n          <h1>Your powerful rich text editor.</h1>\n          <div className=\"actions row\">\n            <Link href={docs[0].url} className=\"action documentation\">\n              Documentation\n            </Link>\n            <Link href={playground[0].url} className=\"action\">\n              Playground\n            </Link>\n          </div>\n        </div>\n      </footer>\n    </>\n  );\n};\n\nexport default Layout;\n"
  },
  {
    "path": "packages/website/src/components/Link.jsx",
    "content": "import NextLink from 'next/link';\nimport { Link as RadixLink } from '@radix-ui/themes';\n\nconst Link = ({ children, ...props }) => {\n  return (\n    <RadixLink {...props} asChild>\n      <NextLink>{children}</NextLink>\n    </RadixLink>\n  );\n};\n\nexport default Link;\n"
  },
  {
    "path": "packages/website/src/components/MDX.jsx",
    "content": "import { MDXRemote } from 'next-mdx-remote';\nimport docs from '../data/docs';\nimport { Highlight, themes } from 'prism-react-renderer';\nimport api from '../data/api';\nimport Sandpack, { SandpackWithQuillTemplate } from './Sandpack';\nimport Editor from './Editor';\nimport {\n  Heading1,\n  Heading2,\n  Heading3,\n  Heading4,\n  Heading5,\n  Heading6,\n} from './Heading';\nimport Hint from './Hint';\nimport SEO from './SEO';\nimport Link from './Link';\n\nconst components = {\n  h1: Heading1,\n  h2: Heading2,\n  h3: Heading3,\n  h4: Heading4,\n  h5: Heading5,\n  h6: Heading6,\n  a: Link,\n  Sandpack,\n  SandpackWithQuillTemplate,\n  Hint,\n  Editor,\n  pre: ({ children }) => {\n    const className = children.props.className || '';\n    const matches = className.match(/language-(?<lang>.*)/);\n    return (\n      <Highlight\n        code={children.props.children}\n        theme={{\n          ...themes.oneLight,\n          plain: {\n            ...themes.oneLight.plain,\n            background: 'transparent',\n          },\n        }}\n        language={\n          matches && matches.groups && matches.groups.lang\n            ? matches.groups.lang\n            : ''\n        }\n      >\n        {({ className, style, tokens, getLineProps, getTokenProps }) => (\n          <pre className={className} style={style}>\n            <code>\n              {tokens.map((line, i) =>\n                i === tokens.length - 1 &&\n                line[0].empty &&\n                line.length === 1 ? null : (\n                  <div key={i} {...getLineProps({ line, key: i })}>\n                    {line.map((token, key) => (\n                      <span key={key} {...getTokenProps({ token, key })} />\n                    ))}\n                  </div>\n                ),\n              )}\n            </code>\n          </pre>\n        )}\n      </Highlight>\n    );\n  },\n};\n\nexport default function MDX({ mdxSource, data }) {\n  return (\n    <>\n      <SEO title={mdxSource.frontmatter.title} />\n      <MDXRemote\n        {...mdxSource}\n        components={components}\n        scope={{ data: { api, docs }, scope: data }}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/website/src/components/NoSSR.jsx",
    "content": "import { useEffect, useLayoutEffect, useState } from 'react';\n\nconst useEnhancedEffect =\n  typeof window !== 'undefined' && process.env.NODE_ENV !== 'test'\n    ? useLayoutEffect\n    : useEffect;\n\nconst NoSSR = ({ children, defer, fallback }) => {\n  const [isMounted, setMountedState] = useState(false);\n\n  useEnhancedEffect(() => {\n    if (!defer) setMountedState(true);\n  }, [defer]);\n\n  useEffect(() => {\n    if (defer) setMountedState(true);\n  }, [defer]);\n\n  return isMounted ? children : fallback;\n};\n\nexport const withoutSSR = (Component) => {\n  const Comp = (props) => (\n    <NoSSR>\n      <Component {...props} />\n    </NoSSR>\n  );\n  Comp.displayName = 'withoutSSR';\n\n  return Comp;\n};\n\nexport default NoSSR;\n"
  },
  {
    "path": "packages/website/src/components/OpenSource.jsx",
    "content": "import GitHub from './GitHub';\nimport OpenSourceIcon from '../svg/features/open-source.svg';\nimport classNames from 'classnames';\nimport styles from './OpenSource.module.scss';\nimport Link from './Link';\n\nconst OpenSource = () => (\n  <div className={classNames('feature row', styles.container)}>\n    <div className=\"six columns details\">\n      <h2>An Open Source Project</h2>\n      <span className={styles.about}>\n        Quill is developed and maintained by{' '}\n        <Link href=\"https://slab.com\" target=\"_blank\">\n          Slab\n        </Link>\n        . It is permissively licensed under BSD. Use it freely in personal or\n        commercial projects!\n      </span>\n      <div className={styles.github}>\n        <GitHub />\n      </div>\n    </div>\n    <div className=\"six columns\">\n      <OpenSourceIcon />\n    </div>\n  </div>\n);\n\nexport default OpenSource;\n"
  },
  {
    "path": "packages/website/src/components/OpenSource.module.scss",
    "content": ".container {\n  .about {\n    line-height: 1.2;\n  }\n}\n\n.github {\n  margin-top: 20px;\n}\n"
  },
  {
    "path": "packages/website/src/components/PlaygroundLayout.jsx",
    "content": "import { Select } from '@radix-ui/themes';\nimport classNames from 'classnames';\nimport styles from './PlaygroundLayout.module.scss';\nimport playground from '../data/playground';\nimport { Button } from '@radix-ui/themes';\nimport { Share1Icon } from '@radix-ui/react-icons';\nimport { useRouter } from 'next/navigation';\nimport { useSandpack } from '@codesandbox/sandpack-react';\nimport { compressToEncodedURIComponent } from 'lz-string';\nimport { useEffect, useState } from 'react';\n\nconst PlaygroundLayout = ({ children, permalink, title, files }) => {\n  const router = useRouter();\n  const { sandpack } = useSandpack();\n\n  const [isCopied, setIsCopied] = useState(false);\n\n  useEffect(() => {\n    if (!isCopied) return undefined;\n    const timeout = setTimeout(() => {\n      setIsCopied(false);\n    }, 1000);\n\n    return () => {\n      clearTimeout(timeout);\n    };\n  }, [isCopied]);\n\n  return (\n    <>\n      <div\n        className={classNames(styles.copied, { [styles.active]: isCopied })}\n      ></div>\n      <div className={styles.panel}>\n        <div className={styles.exampleSelector}>\n          <label className={styles.exampleLabel}>Example:</label>\n          <Select.Root\n            defaultValue={playground.find((p) => p.url === permalink).url}\n            onValueChange={(v) => {\n              router.push(v);\n            }}\n          >\n            <Select.Trigger />\n            <Select.Content position=\"popper\">\n              <Select.Group>\n                {playground.map((p) => {\n                  return (\n                    <Select.Item key={p.url} value={p.url}>\n                      {p.title}\n                    </Select.Item>\n                  );\n                })}\n              </Select.Group>\n            </Select.Content>\n          </Select.Root>\n        </div>\n        <div className={styles.panelMeta}>\n          <Button\n            onClick={() => {\n              const map = {};\n              Object.keys(files).forEach((name) => {\n                const fullName = name.startsWith('/') ? name : `/${name}`;\n                map[fullName] = sandpack.files[fullName]?.code;\n              });\n              const encoded = compressToEncodedURIComponent(\n                JSON.stringify(map),\n              );\n              location.hash = `code${encoded}`;\n              navigator.clipboard.writeText(location.href);\n              setIsCopied(true);\n            }}\n          >\n            <Share1Icon /> Share Your Edits\n          </Button>\n        </div>\n      </div>\n      {children}\n    </>\n  );\n};\n\nexport default PlaygroundLayout;\n"
  },
  {
    "path": "packages/website/src/components/PlaygroundLayout.module.scss",
    "content": ".panel {\n  display: flex;\n  background-color: var(--yellow-a1);\n  padding: 10px 16px;\n  border-top-left-radius: 4px;\n  border-top-right-radius: 4px;\n  border: 1px solid #ccc;\n  border-bottom: 0;\n\n  .panelMeta {\n    margin-left: auto;\n  }\n}\n\n.exampleLabel {\n  font-weight: bold;\n  font-size: 14px;\n  color: #999;\n}\n\n.exampleSelector {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  font-size: 14px;\n  text-align: left;\n}\n\n.copied {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  color: white;\n  display: flex;\n  align-items: center;\n  place-content: center;\n  font-size: 20px;\n  opacity: 0;\n  transition: opacity 0.2s;\n  pointer-events: none;\n  z-index: 99999;\n\n  &.active {\n    opacity: 1;\n  }\n\n  &::before {\n    content: 'URL copied to clipboard';\n    background-color: rgba(0, 0, 0, 0.5);\n    width: max-content;\n    padding: 10px 20px;\n    border-radius: 10px;\n    line-height: 20px;\n  }\n}\n"
  },
  {
    "path": "packages/website/src/components/PostLayout.jsx",
    "content": "import classNames from 'classnames';\nimport { usePathname } from 'next/navigation';\nimport docsItems from '../data/docs';\nimport Link from 'next/link';\nimport slug from '../utils/slug';\nimport Layout from '../components/Layout';\nimport OpenSource from '../components/OpenSource';\nimport React, { useState } from 'react';\nimport * as styles from './PostLayout.module.scss';\nimport flattenData from '../utils/flattenData';\n\nconst getPagination = (permalink, items) => {\n  const flattenedItems = flattenData(items);\n  const index = flattenedItems.findIndex((item) => item.url === permalink);\n  if (index === -1) return { prev: null, next: null };\n\n  let prev = null;\n  let next = null;\n\n  if (index > 0) prev = flattenedItems[index - 1];\n  if (index < flattenedItems.length - 1) next = flattenedItems[index + 1];\n\n  return { prev, next };\n};\n\nconst SidebarItem = ({ item }) => {\n  const pathname = usePathname();\n\n  return (\n    <li className={classNames({ active: pathname.includes(item.url) })}>\n      <Link href={item.url}>{item.title}</Link>\n      {item.children && (\n        <ul>\n          {item.children.map((child) => (\n            <SidebarItem key={child.url} item={child} />\n          ))}\n        </ul>\n      )}\n    </li>\n  );\n};\n\nconst PostLayout = ({ title, pageType, filePath, permalink, children }) => {\n  const { prev, next } = getPagination(permalink, docsItems);\n  const [isNavOpen, setIsNavOpen] = useState(false);\n\n  return (\n    <Layout title={title}>\n      <div id=\"docs-wrapper\" className=\"container\">\n        <div className=\"row\">\n          <div\n            id=\"sidebar-container\"\n            className={classNames('three', 'columns', {\n              active: isNavOpen,\n            })}\n          >\n            <button\n              className=\"sidebar-button\"\n              onClick={() => {\n                setIsNavOpen(!isNavOpen);\n              }}\n            >\n              Document Navigation\n            </button>\n            <ul className=\"sidebar-list\">\n              {docsItems.map((item) => (\n                <SidebarItem key={item.url} item={item} />\n              ))}\n            </ul>\n          </div>\n          <div id=\"docs-container\" className=\"nine columns\">\n            <div className={classNames('row', styles.breadcrumbRow)}>\n              <div className={styles.breadcrumb}>\n                <span>Documentation</span>\n                <span>{title}</span>\n              </div>\n              <div className={styles.editOnGitHub}>\n                <a\n                  href={process.env.github + filePath}\n                  target=\"_blank\"\n                  title=\"Edit on GitHub\"\n                >\n                  Edit page on GitHub ↗\n                </a>\n              </div>\n            </div>\n            <article id=\"content-container\" className={styles.content}>\n              <h1 id={slug(title)}>{title}</h1>\n              {children}\n            </article>\n          </div>\n        </div>\n\n        <div className=\"row\">\n          <hr />\n        </div>\n\n        <OpenSource />\n      </div>\n    </Layout>\n  );\n};\n\nexport default PostLayout;\n"
  },
  {
    "path": "packages/website/src/components/PostLayout.module.scss",
    "content": ".breadcrumbRow {\n  align-items: center;\n  display: flex;\n  margin-bottom: 32px;\n  justify-content: space-between;\n\n  &:after {\n    content: none;\n  }\n\n  .breadcrumb {\n    font-size: 1.25rem;\n    display: flex;\n\n    span:not(:last-child) {\n      &::after {\n        content: '>';\n        font-weight: normal;\n        margin: 0 4px 0 6px;\n      }\n    }\n  }\n\n  .breadcrumb span:first-child {\n    font-weight: bold;\n    margin-right: 4px;\n  }\n\n  .editOnGitHub {\n    font-size: 1.08rem;\n    max-width: var(--width-readable);\n    text-transform: uppercase;\n  }\n}\n\n.content {\n  code {\n    background: #f1f1f1;\n  }\n\n  pre {\n    border-radius: 2px;\n\n    code {\n      background: transparent;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/website/src/components/SEO.jsx",
    "content": "import * as React from 'react';\nimport Head from 'next/head';\n\nconst SEO = ({ title, permalink }) => {\n  const pageTitle = title\n    ? `${title} - ${process.env.shortTitle}`\n    : process.env.title;\n\n  return (\n    <Head>\n      <meta name=\"twitter:site\" content=\"@quilljs\" />\n      <meta name=\"twitter:title\" content={pageTitle} />\n      <meta name=\"twitter:description\" content={process.env.shortDescription} />\n      <meta\n        name=\"twitter:image\"\n        content=\"https://quilljs.com/assets/images/brand-asset.png\"\n      />\n      <meta property=\"og:type\" content=\"website\" />\n      <meta\n        property=\"og:url\"\n        content={permalink ? process.env.url + permalink : process.env.url}\n      />\n      <meta\n        property=\"og:image\"\n        content=\"https://quilljs.com/assets/images/brand-asset.png\"\n      />\n      <meta property=\"og:title\" content={pageTitle} />\n      <meta property=\"og:site_name\" content=\"Quill\" />\n      <title>{pageTitle}</title>\n      <meta name=\"description\" content={process.env.description} />\n    </Head>\n  );\n};\n\nexport default SEO;\n"
  },
  {
    "path": "packages/website/src/components/Sandpack.jsx",
    "content": "import {\n  SandpackProvider,\n  SandpackCodeEditor,\n  SandpackFileExplorer,\n  SandpackPreview,\n  useSandpack,\n} from '@codesandbox/sandpack-react';\nimport { useEffect, useState } from 'react';\nimport { Button } from '@radix-ui/themes';\nimport { PlayIcon, Share1Icon } from '@radix-ui/react-icons';\nimport * as styles from './Sandpack.module.scss';\nimport classNames from 'classnames';\nimport {\n  compressToEncodedURIComponent,\n  decompressFromEncodedURIComponent,\n} from 'lz-string';\nimport { withoutSSR } from './NoSSR';\nimport replaceCDN from '../utils/replaceCDN';\n\nconst TogglePreviewButton = ({ isPreviewEnabled, setIsPreviewEnabled }) => {\n  const { sandpack } = useSandpack();\n\n  return (\n    <Button\n      onClick={() => {\n        if (!isPreviewEnabled) {\n          sandpack.runSandpack();\n        }\n        setIsPreviewEnabled(!isPreviewEnabled);\n      }}\n    >\n      {isPreviewEnabled ? (\n        'Hide Result'\n      ) : (\n        <>\n          <PlayIcon /> Run Code\n        </>\n      )}\n    </Button>\n  );\n};\n\nconst ToggleCodeButton = ({ isCodeEnabled, setIsCodeEnabled }) => {\n  const { sandpack } = useSandpack();\n\n  return (\n    <Button\n      className={styles.button}\n      onClick={() => {\n        if (!isCodeEnabled) {\n          sandpack.runSandpack();\n        }\n        setIsCodeEnabled(!isCodeEnabled);\n      }}\n    >\n      {isCodeEnabled ? 'Hide Code' : 'Show Code'}\n    </Button>\n  );\n};\n\nconst LocationOverride = ({ filenames }) => {\n  const { sandpack } = useSandpack();\n  const [isCopied, setIsCopied] = useState(false);\n\n  useEffect(() => {\n    if (!isCopied) return undefined;\n    const timeout = setTimeout(() => {\n      setIsCopied(false);\n    }, 1000);\n\n    return () => {\n      clearTimeout(timeout);\n    };\n  }, [isCopied]);\n\n  return (\n    <div className={styles.shareButton}>\n      <div\n        className={classNames(styles.copied, { [styles.active]: isCopied })}\n      ></div>\n      <Button\n        onClick={() => {\n          const map = {};\n          filenames.forEach((name) => {\n            const fullName = name.startsWith('/') ? name : `/${name}`;\n            map[fullName] = sandpack.files[fullName]?.code;\n          });\n          const encoded = compressToEncodedURIComponent(JSON.stringify(map));\n          location.hash = `code${encoded}`;\n          navigator.clipboard.writeText(location.href);\n          setIsCopied(true);\n        }}\n      >\n        <Share1Icon /> Share Your Edits\n      </Button>\n    </div>\n  );\n};\n\nexport const StandaloneSandpack = withoutSSR(\n  ({ files, visibleFiles, activeFile, externalResources }) => {\n    const [overrides] = useState(() => {\n      if (location.hash.startsWith('#code')) {\n        const code = location.hash.replace('#code', '').trim();\n        let userCode;\n        try {\n          userCode = JSON.parse(decompressFromEncodedURIComponent(code));\n        } catch (err) {}\n        return userCode || {};\n      }\n      return {};\n    });\n\n    return (\n      <SandpackProvider\n        options={{\n          visibleFiles,\n          activeFile,\n          externalResources:\n            externalResources && externalResources.map(replaceCDN),\n        }}\n        template=\"static\"\n        files={Object.keys(files).reduce((f, name) => {\n          const fullName = name.startsWith('/') ? name : `/${name}`;\n          return {\n            ...f,\n            [name]: replaceCDN(overrides[fullName] ?? files[name]).trim(),\n          };\n        }, {})}\n      >\n        <LocationOverride filenames={Object.keys(files)} />\n        <div className={styles.standaloneWrapper}>\n          <div className={styles.standaloneFileTree}>\n            <SandpackFileExplorer autoHiddenFiles />\n          </div>\n          <div className={styles.standaloneEditor}>\n            <SandpackCodeEditor\n              showTabs={false}\n              wrapContent\n              showRunButton={false}\n            />\n          </div>\n          <div className={styles.standalonePreview}>\n            <SandpackPreview showOpenInCodeSandbox={false} />\n          </div>\n        </div>\n      </SandpackProvider>\n    );\n  },\n);\n\nconst Sandpack = ({\n  defaultShowPreview,\n  preferPreview,\n  files,\n  visibleFiles,\n  activeFile,\n  externalResources,\n  showFileTree,\n  defaultShowCode,\n}) => {\n  const [isPreviewEnabled, setIsPreviewEnabled] = useState(\n    preferPreview || defaultShowPreview,\n  );\n  const [isCodeEnabled, setIsCodeEnabled] = useState(\n    !preferPreview || defaultShowCode,\n  );\n  const [isReady, setIsReady] = useState(false);\n\n  useEffect(() => {\n    setTimeout(() => {\n      setIsReady(true);\n    }, 100);\n  }, []);\n\n  return (\n    <div className={styles.container} style={isReady ? {} : { opacity: '0' }}>\n      <SandpackProvider\n        options={{\n          autorun: defaultShowPreview,\n          visibleFiles,\n          activeFile,\n          externalResources:\n            externalResources && externalResources.map(replaceCDN),\n        }}\n        template=\"static\"\n        files={Object.keys(files).reduce(\n          (f, name) => ({\n            ...f,\n            [name]: replaceCDN(files[name]).trim(),\n          }),\n          {},\n        )}\n      >\n        <div\n          className={classNames(styles.wrapper, {\n            [styles.preferPreview]: preferPreview,\n          })}\n        >\n          {isPreviewEnabled && preferPreview && (\n            <div className={styles.previewWrapper}>\n              <div className={styles.preview}>\n                <SandpackPreview\n                  showOpenInCodeSandbox={false}\n                  showRefreshButton={false}\n                />\n              </div>\n              <div className={styles.footer}>\n                <ToggleCodeButton\n                  isCodeEnabled={isCodeEnabled}\n                  setIsCodeEnabled={setIsCodeEnabled}\n                />\n              </div>\n            </div>\n          )}\n          {isCodeEnabled && (\n            <div className={styles.editorWrapper}>\n              <div className={styles.codeArea}>\n                {showFileTree && (\n                  <div className={styles.fileTree}>\n                    <SandpackFileExplorer autoHiddenFiles />\n                  </div>\n                )}\n                <div className={styles.editor}>\n                  <SandpackCodeEditor\n                    showTabs={\n                      !showFileTree &&\n                      (visibleFiles\n                        ? visibleFiles.length > 1\n                        : Object.keys(files).length > 1)\n                    }\n                    wrapContent\n                    showRunButton={false}\n                  />\n                </div>\n              </div>\n              {!preferPreview && (\n                <div className={styles.footer}>\n                  <TogglePreviewButton\n                    defaultShowPreview={defaultShowPreview}\n                    isPreviewEnabled={isPreviewEnabled}\n                    setIsPreviewEnabled={setIsPreviewEnabled}\n                  />\n                </div>\n              )}\n            </div>\n          )}\n          {isPreviewEnabled && !preferPreview && (\n            <div className={styles.preview}>\n              <SandpackPreview showOpenInCodeSandbox={false} />\n            </div>\n          )}\n        </div>\n      </SandpackProvider>\n    </div>\n  );\n};\n\nexport const SandpackWithQuillTemplate = ({\n  files,\n  afterEditor,\n  beforeEditor,\n  content,\n  ...props\n}) => {\n  return (\n    <Sandpack\n      {...props}\n      files={{\n        'index.html': `\n<!-- Include stylesheet -->\n<link href=\"{{site.cdn}}/quill.snow.css\" rel=\"stylesheet\" />\n${beforeEditor || ''}\n<!-- Create the editor container -->\n<div id=\"editor\">${content || ''}\n</div>\n${afterEditor || ''}\n<!-- Include the Quill library -->\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<script src=\"/index.js\"></script>`,\n        ...files,\n      }}\n      visibleFiles={Object.keys(files)}\n      activeFile={Object.keys(files)[0]}\n    />\n  );\n};\n\nexport default Sandpack;\n"
  },
  {
    "path": "packages/website/src/components/Sandpack.module.scss",
    "content": ".container {\n  margin: 24px 0 32px;\n  transition: opacity 0.2s;\n}\n\n.bar {\n  display: flex;\n  margin: 0 0 10px;\n\n  .togglePreviewButton {\n    margin-left: auto;\n  }\n}\n\n$borderColor: #dadcdc;\n.wrapper {\n  display: flex;\n  flex-direction: column;\n\n  .editorWrapper {\n    border: 1px solid $borderColor;\n    position: relative;\n    max-height: 480px;\n    display: flex;\n    flex-direction: column;\n\n    .codeArea {\n      display: flex;\n      flex-direction: row;\n      min-height: 0;\n    }\n\n    .fileTree {\n      border-right: 1px solid $borderColor;\n      width: 200px;\n      overflow: auto;\n    }\n\n    .editor {\n      display: flex;\n      flex: 1;\n    }\n  }\n  .preview {\n    border: 1px solid #ccc;\n    border-top: 0;\n    min-height: 283px;\n    display: flex;\n  }\n\n  &.preferPreview {\n    .preview {\n      border: 1px solid #ccc;\n      border-bottom: 0;\n      min-height: 320px;\n    }\n  }\n}\n\n.widget {\n  border: 1px solid #1ea7fd;\n  border-radius: 2px;\n  padding: 2px 4px 2px 12px;\n  margin-left: 6px;\n  position: relative;\n  cursor: pointer;\n}\n\n.widget:before {\n  content: attr(data-id);\n  background: #1ea7fd;\n  border-radius: 100%;\n  position: absolute;\n  width: 16px;\n  display: block;\n  height: 16px;\n  left: -8px;\n  top: 2px;\n  font-size: 11px;\n  text-align: center;\n  color: white;\n  line-height: 17px;\n}\n\n.footer {\n  background: var(--color-bg-inset);\n  padding: 8px 12px;\n  border-top: 1px solid $borderColor;\n}\n\n.standaloneWrapper {\n  display: grid;\n  grid-template-columns: min-content fit-content(400px) 1fr;\n  height: 100vh;\n\n  .standaloneFileTree {\n    border-right: 1px solid #ccc;\n  }\n\n  .standaloneEditor {\n    border-right: 1px solid #ccc;\n    width: 400px;\n    overflow-y: auto;\n    scrollbar-gutter: stable;\n  }\n\n  .standalonePreview {\n    display: flex;\n  }\n\n  @media (max-width: 900px) {\n    display: flex;\n    flex-direction: column-reverse;\n    height: auto;\n\n    .standalonePreview {\n      height: 320px;\n    }\n\n    .standaloneEditor {\n      width: auto;\n      border-top: 1px solid #ccc;\n      border-right: 0;\n    }\n\n    .standaloneFileTree {\n      border-top: 1px solid #ccc;\n      border-right: 0;\n    }\n  }\n}\n\n.copied {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  color: white;\n  display: flex;\n  align-items: center;\n  place-content: center;\n  font-size: 20px;\n  opacity: 0;\n  transition: opacity 0.2s;\n  pointer-events: none;\n  z-index: 99999;\n\n  &.active {\n    opacity: 1;\n  }\n\n  &::before {\n    content: 'URL copied to clipboard';\n    background-color: rgba(0, 0, 0, 0.5);\n    width: max-content;\n    padding: 10px 20px;\n    border-radius: 10px;\n    line-height: 20px;\n  }\n}\n\n.shareButton {\n  padding: 10px;\n  border-bottom: 1px solid #ccc;\n}\n"
  },
  {
    "path": "packages/website/src/data/api.tsx",
    "content": "const items = [\n  {\n    title: 'Content',\n    hashes: [\n      'deleteText',\n      'getContents',\n      'getLength',\n      'getText',\n      'getSemanticHTML',\n      'insertEmbed',\n      'insertText',\n      'setContents',\n      'setText',\n      'updateContents',\n    ],\n  },\n  {\n    title: 'Formatting',\n    hashes: ['format', 'formatLine', 'formatText', 'getFormat', 'removeFormat'],\n  },\n  {\n    title: 'Selection',\n    hashes: [\n      'getBounds',\n      'getSelection',\n      'setSelection',\n      'scrollSelectionIntoView',\n    ],\n  },\n  {\n    title: 'Editor',\n    hashes: [\n      'blur',\n      'focus',\n      'disable',\n      'enable',\n      'hasFocus',\n      'update',\n      'scrollRectIntoView-experimental',\n    ],\n  },\n  {\n    title: 'Events',\n    hashes: [\n      'text-change',\n      'selection-change',\n      'editor-change',\n      'off',\n      'on',\n      'once',\n    ],\n  },\n  {\n    title: 'Model',\n    hashes: ['find', 'getIndex', 'getLeaf', 'getLine', 'getLines'],\n  },\n  {\n    title: 'Extension',\n    hashes: ['debug', 'import', 'register', 'addContainer', 'getModule'],\n  },\n];\n\nexport default items;\n"
  },
  {
    "path": "packages/website/src/data/docs.tsx",
    "content": "const items = [\n  {\n    title: 'Quickstart',\n    url: '/docs/quickstart',\n  },\n  {\n    title: 'Why Quill',\n    url: '/docs/why-quill',\n  },\n  {\n    title: 'Installation',\n    url: '/docs/installation',\n  },\n  {\n    title: 'Upgrading to 2.0',\n    url: '/docs/upgrading-to-2-0',\n  },\n  {\n    title: 'Configuration',\n    url: '/docs/configuration',\n  },\n  {\n    title: 'Formats',\n    url: '/docs/formats',\n  },\n  {\n    title: 'API',\n    url: '/docs/api',\n    children: [\n      {\n        title: 'Content',\n        url: '/docs/api/#content',\n      },\n      {\n        title: 'Formatting',\n        url: '/docs/api/#formatting',\n      },\n      {\n        title: 'Selection',\n        url: '/docs/api/#selection',\n      },\n      {\n        title: 'Editor',\n        url: '/docs/api/#editor',\n      },\n      {\n        title: 'Events',\n        url: '/docs/api/#events',\n      },\n      {\n        title: 'Model',\n        url: '/docs/api/#model',\n      },\n      {\n        title: 'Extension',\n        url: '/docs/api/#extension',\n      },\n    ],\n  },\n  {\n    title: 'Delta',\n    url: '/docs/delta',\n  },\n  {\n    title: 'Modules',\n    url: '/docs/modules',\n    children: [\n      {\n        title: 'Toolbar',\n        url: '/docs/modules/toolbar',\n      },\n      {\n        title: 'Keyboard',\n        url: '/docs/modules/keyboard',\n      },\n      {\n        title: 'History',\n        url: '/docs/modules/history',\n      },\n      {\n        title: 'Clipboard',\n        url: '/docs/modules/clipboard',\n      },\n      {\n        title: 'Syntax',\n        url: '/docs/modules/syntax',\n      },\n    ],\n  },\n  {\n    title: 'Customization',\n    url: '/docs/customization',\n    children: [\n      {\n        title: 'Themes',\n        url: '/docs/customization/themes',\n      },\n      {\n        title: 'Registries',\n        url: '/docs/customization/registries',\n      },\n    ],\n  },\n  {\n    title: 'Guides',\n    url: '/docs/guides/designing-the-delta-format',\n    children: [\n      {\n        title: 'Designing the Delta Format',\n        url: '/docs/guides/designing-the-delta-format',\n      },\n      {\n        title: 'Building a Custom Module',\n        url: '/docs/guides/building-a-custom-module',\n      },\n      {\n        title: 'Cloning Medium with Parchment',\n        url: '/docs/guides/cloning-medium-with-parchment',\n      },\n    ],\n  },\n];\n\nexport default items;\n"
  },
  {
    "path": "packages/website/src/data/playground.tsx",
    "content": "const playground = [\n  {\n    title: 'Basic setup with snow theme',\n    url: '/playground/snow',\n  },\n  {\n    title: 'Using Quill inside a form',\n    url: '/playground/form',\n  },\n  {\n    title: 'Custom font and formats',\n    url: '/playground/custom-formats',\n  },\n  {\n    title: 'Using Quill with React',\n    url: '/playground/react',\n  },\n];\n\nexport default playground;\n"
  },
  {
    "path": "packages/website/src/pages/404.jsx",
    "content": "import SEO from '../components/SEO';\n\nconst NotFound = () => <div style={{ padding: 10 }}>Not Found</div>;\n\nexport const Head = () => <SEO title=\"Not Found\" />;\n\nexport default NotFound;\n"
  },
  {
    "path": "packages/website/src/pages/_app.jsx",
    "content": "import { GoogleAnalytics } from '@next/third-parties/google';\nimport { Theme } from '@radix-ui/themes';\nimport '@radix-ui/themes/styles.css';\nimport './variables.scss';\nimport './base.css';\nimport './styles.scss';\n\n// This default export is required in a new `pages/_app.js` file.\nexport default function MyApp({ Component, pageProps }) {\n  return (\n    <Theme accentColor=\"yellow\">\n      <GoogleAnalytics gaId=\"G-B37E2WMSPW\" />\n      <Component {...pageProps} />\n    </Theme>\n  );\n}\n"
  },
  {
    "path": "packages/website/src/pages/_document.jsx",
    "content": "import { Html, Head, Main, NextScript } from 'next/document';\nimport Script from 'next/script';\nimport { getSandpackCssText } from '@codesandbox/sandpack-react';\n\nexport default function Document() {\n  return (\n    <Html>\n      <Head>\n        <Script\n          strategy=\"beforeInteractive\"\n          src={`${process.env.katex}/katex.min.js`}\n        />\n        <Script\n          strategy=\"beforeInteractive\"\n          src={`${process.env.highlightjs}/highlight.min.js`}\n        />\n        <Script\n          strategy=\"beforeInteractive\"\n          src={`${process.env.cdn}/quill.js`}\n        />\n        <link\n          rel=\"icon\"\n          type=\"image/x-icon\"\n          href=\"/assets/images/favicon.ico\"\n        />\n        <link rel=\"stylesheet\" href={`${process.env.katex}/katex.min.css`} />\n        <link rel=\"stylesheet\" href={`${process.env.cdn}/quill.snow.css`} />\n        <link rel=\"stylesheet\" href={`${process.env.cdn}/quill.bubble.css`} />\n        <link\n          rel=\"stylesheet\"\n          href=\"https://cdn.jsdelivr.net/npm/@docsearch/css@3\"\n        />\n        <link\n          rel=\"stylesheet\"\n          href={`${process.env.highlightjs}/styles/atom-one-dark.min.css`}\n        />\n        <link\n          rel=\"stylesheet\"\n          href=\"https://fonts.googleapis.com/css?family=Inconsolata&display=optional\"\n        />\n        <link\n          rel=\"stylesheet\"\n          href=\"https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css\"\n        />\n        <style\n          dangerouslySetInnerHTML={{ __html: getSandpackCssText() }}\n          id=\"sandpack\"\n          key=\"sandpack-css\"\n        />\n      </Head>\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n"
  },
  {
    "path": "packages/website/src/pages/base.css",
    "content": "/*************/\n/* Normalize */\n/*************/\n\n/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */\n\n/**\n * 1. Change the default font family in all browsers (opinionated).\n * 2. Prevent adjustments of font size after orientation changes in IE and iOS.\n */\n\nhtml {\n  font-family: sans-serif; /* 1 */\n  -ms-text-size-adjust: 100%; /* 2 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/**\n * Remove the margin in all browsers (opinionated).\n */\n\nbody {\n  margin: 0;\n}\n\n/* HTML5 display definitions\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 9-.\n * 1. Add the correct display in Edge, IE, and Firefox.\n * 2. Add the correct display in IE.\n */\n\narticle,\naside,\ndetails, /* 1 */\nfigcaption,\nfigure,\nfooter,\nheader,\nmain, /* 2 */\nmenu,\nnav,\nsection,\nsummary {\n  /* 1 */\n  display: block;\n}\n\n/**\n * Add the correct display in IE 9-.\n */\n\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block;\n}\n\n/**\n * Add the correct display in iOS 4-7.\n */\n\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n  vertical-align: baseline;\n}\n\n/**\n * Add the correct display in IE 10-.\n * 1. Add the correct display in IE.\n */\n\ntemplate, /* 1 */\n[hidden] {\n  display: none;\n}\n\n/* Links\n   ========================================================================== */\n\n/**\n * 1. Remove the gray background on active links in IE 10.\n * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.\n */\n\na {\n  background-color: transparent; /* 1 */\n  -webkit-text-decoration-skip: objects; /* 2 */\n}\n\n/**\n * Remove the outline on focused links when they are also active or hovered\n * in all browsers (opinionated).\n */\n\na:active,\na:hover {\n  outline-width: 0;\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * 1. Remove the bottom border in Firefox 39-.\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n  border-bottom: none; /* 1 */\n  text-decoration: underline; /* 2 */\n  text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Prevent the duplicate application of `bolder` by the next rule in Safari 6.\n */\n\nb,\nstrong {\n  font-weight: inherit;\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/**\n * Add the correct font style in Android 4.3-.\n */\n\ndfn {\n  font-style: italic;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/**\n * Add the correct background and color in IE 9-.\n */\n\nmark {\n  background-color: #ff0;\n  color: #000;\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10-.\n */\n\nimg {\n  border-style: none;\n}\n\n/**\n * Hide the overflow in IE.\n */\n\nsvg:not(:root) {\n  overflow: hidden;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct margin in IE 8.\n */\n\nfigure {\n  margin: 1em 40px;\n}\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n  box-sizing: content-box; /* 1 */\n  height: 0; /* 1 */\n  overflow: visible; /* 2 */\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change font properties to `inherit` in all browsers (opinionated).\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\nselect,\ntextarea {\n  font: inherit; /* 1 */\n  margin: 0; /* 2 */\n}\n\n/**\n * Restore the font weight unset by the previous rule.\n */\n\noptgroup {\n  font-weight: bold;\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput {\n  /* 1 */\n  overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect {\n  /* 1 */\n  text-transform: none;\n}\n\n/**\n * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n *    controls in Android 4.\n * 2. Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\nhtml [type=\"button\"], /* 1 */\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button; /* 2 */\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type='button']::-moz-focus-inner,\n[type='reset']::-moz-focus-inner,\n[type='submit']::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type='button']:-moz-focusring,\n[type='reset']:-moz-focusring,\n[type='submit']:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n\n/**\n * Change the border, margin, and padding in all browsers (opinionated).\n */\n\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n  box-sizing: border-box; /* 1 */\n  color: inherit; /* 2 */\n  display: table; /* 1 */\n  max-width: 100%; /* 1 */\n  padding: 0; /* 3 */\n  white-space: normal; /* 1 */\n}\n\n/**\n * Remove the default vertical scrollbar in IE.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10-.\n * 2. Remove the padding in IE 10-.\n */\n\n[type='checkbox'],\n[type='radio'] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type='number']::-webkit-inner-spin-button,\n[type='number']::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type='search'] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding and cancel buttons in Chrome and Safari on OS X.\n */\n\n[type='search']::-webkit-search-cancel-button,\n[type='search']::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * Correct the text style of placeholders in Chrome, Edge, and Safari.\n */\n\n::-webkit-input-placeholder {\n  color: inherit;\n  opacity: 0.54;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/*******************/\n/* Grid / Skeleton */\n/*******************/\n\n.container {\n  position: relative;\n  width: 100%;\n  max-width: 960px;\n  margin: 0 auto;\n  padding: 0 20px;\n  box-sizing: border-box;\n}\n.column,\n.columns {\n  width: 100%;\n  float: left;\n  box-sizing: border-box;\n}\n\n/* For devices larger than 400px */\n@media (min-width: 400px) {\n  .container {\n    width: 85%;\n    padding: 0;\n  }\n}\n\n/* For devices larger than 550px */\n@media (min-width: 550px) {\n  .container {\n    width: 80%;\n  }\n  .column,\n  .columns {\n    margin-left: 4%;\n  }\n  .column:first-child,\n  .columns:first-child {\n    margin-left: 0;\n  }\n\n  .one.column,\n  .one.columns {\n    width: 4.66666666667%;\n  }\n  .two.columns {\n    width: 13.3333333333%;\n  }\n  .three.columns {\n    width: 22%;\n  }\n  .four.columns {\n    width: 30.6666666667%;\n  }\n  .five.columns {\n    width: 39.3333333333%;\n  }\n  .six.columns {\n    width: 48%;\n  }\n  .seven.columns {\n    width: 56.6666666667%;\n  }\n  .eight.columns {\n    width: 65.3333333333%;\n  }\n  .nine.columns {\n    width: 74%;\n  }\n  .ten.columns {\n    width: 82.6666666667%;\n  }\n  .eleven.columns {\n    width: 91.3333333333%;\n  }\n  .twelve.columns {\n    width: 100%;\n    margin-left: 0;\n  }\n\n  .one-third.column {\n    width: 30.6666666667%;\n  }\n  .two-thirds.column {\n    width: 65.3333333333%;\n  }\n\n  .one-half.column {\n    width: 48%;\n  }\n\n  /* Offsets */\n  .offset-by-one.column,\n  .offset-by-one.columns {\n    margin-left: 8.66666666667%;\n  }\n  .offset-by-two.column,\n  .offset-by-two.columns {\n    margin-left: 17.3333333333%;\n  }\n  .offset-by-three.column,\n  .offset-by-three.columns {\n    margin-left: 26%;\n  }\n  .offset-by-four.column,\n  .offset-by-four.columns {\n    margin-left: 34.6666666667%;\n  }\n  .offset-by-five.column,\n  .offset-by-five.columns {\n    margin-left: 43.3333333333%;\n  }\n  .offset-by-six.column,\n  .offset-by-six.columns {\n    margin-left: 52%;\n  }\n  .offset-by-seven.column,\n  .offset-by-seven.columns {\n    margin-left: 60.6666666667%;\n  }\n  .offset-by-eight.column,\n  .offset-by-eight.columns {\n    margin-left: 69.3333333333%;\n  }\n  .offset-by-nine.column,\n  .offset-by-nine.columns {\n    margin-left: 78%;\n  }\n  .offset-by-ten.column,\n  .offset-by-ten.columns {\n    margin-left: 86.6666666667%;\n  }\n  .offset-by-eleven.column,\n  .offset-by-eleven.columns {\n    margin-left: 95.3333333333%;\n  }\n\n  .offset-by-one-third.column,\n  .offset-by-one-third.columns {\n    margin-left: 34.6666666667%;\n  }\n  .offset-by-two-thirds.column,\n  .offset-by-two-thirds.columns {\n    margin-left: 69.3333333333%;\n  }\n\n  .offset-by-one-half.column,\n  .offset-by-one-half.columns {\n    margin-left: 52%;\n  }\n}\nhtml {\n  font-size: 62.5%;\n}\nbody {\n  font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */\n  line-height: 1.6;\n  font-weight: 400;\n  color: #222;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  margin-top: 0;\n  margin-bottom: 2rem;\n}\nh1 {\n  font-size: 4rem;\n  line-height: 1.2;\n  letter-spacing: -0.1rem;\n}\nh2 {\n  font-size: 3.6rem;\n  line-height: 1.25;\n  letter-spacing: -0.1rem;\n}\nh3 {\n  font-size: 3rem;\n  line-height: 1.3;\n  letter-spacing: -0.1rem;\n}\nh4 {\n  font-size: 2.4rem;\n  line-height: 1.35;\n  letter-spacing: -0.08rem;\n}\nh5 {\n  font-size: 1.8rem;\n  line-height: 1.5;\n  letter-spacing: -0.05rem;\n}\nh6 {\n  font-size: 1.5rem;\n  line-height: 1.6;\n  letter-spacing: 0;\n}\n\n/* Larger than phablet */\n@media (min-width: 550px) {\n  h1 {\n    font-size: 5rem;\n  }\n  h2 {\n    font-size: 4.2rem;\n  }\n  h3 {\n    font-size: 3.6rem;\n  }\n  h4 {\n    font-size: 3rem;\n  }\n  h5 {\n    font-size: 2.4rem;\n  }\n  h6 {\n    font-size: 1.5rem;\n  }\n}\n\np {\n  margin-top: 0;\n}\n\nul {\n  list-style: circle inside;\n}\nol {\n  list-style: decimal inside;\n}\nol,\nul {\n  padding-left: 0;\n  margin-top: 0;\n}\nul ul,\nul ol,\nol ol,\nol ul {\n  margin: 1.5rem 0 1.5rem 3rem;\n}\nli {\n  margin-bottom: 1rem;\n}\n\ncode {\n  padding: 0.2rem 0.5rem;\n  margin: 0 0.2rem;\n  font-size: 90%;\n  white-space: nowrap;\n}\npre > code {\n  display: block;\n  padding: 1rem 1.5rem;\n  white-space: pre;\n}\n\nhr {\n  margin-top: 2rem;\n  margin-bottom: 2.5rem;\n  border-width: 0;\n  border-top: 1px solid #e1e1e1;\n}\n\npre,\nblockquote,\ndl,\nfigure,\ntable,\np,\nul,\nol,\nform {\n  margin-bottom: 2.5rem;\n}\n\n.container:after,\n.row:after {\n  content: '';\n  display: table;\n  clear: both;\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/[...id].jsx",
    "content": "import { serialize } from 'next-mdx-remote/serialize';\nimport docs from '../../data/docs';\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport PostLayout from '../../components/PostLayout';\nimport env from '../../../env';\nimport MDX from '../../components/MDX';\nimport flattenData from '../../utils/flattenData';\n\nexport async function getStaticPaths() {\n  return {\n    paths: flattenData(docs).map((d) => d.url),\n    fallback: false,\n  };\n}\n\nexport async function getStaticProps({ params }) {\n  const basePath = join('content', 'docs', `${params.id.join('/')}`);\n  const filePath = `${basePath}.mdx`;\n  const markdown = await readFile(join(process.cwd(), filePath), 'utf8');\n  let data = {};\n  try {\n    const path = params.id.join('/');\n    if (path === 'guides/cloning-medium-with-parchment') {\n      data = await import(`../../../content/docs/${path}`);\n    }\n  } catch {}\n  const mdxSource = await serialize(\n    markdown.replace(/\\{\\{site\\.(\\w+)\\}\\}/g, (...args) => {\n      return env[args[1]];\n    }),\n    { parseFrontmatter: true },\n  );\n  return {\n    props: {\n      mdxSource,\n      filePath,\n      permalink: `/docs/${params.id}`,\n      data: JSON.parse(JSON.stringify(data)),\n    },\n  };\n}\n\nexport default function Doc({ mdxSource, filePath, permalink, data }) {\n  return (\n    <PostLayout\n      pageType=\"docs\"\n      filePath={filePath}\n      permalink={permalink}\n      {...mdxSource.frontmatter}\n    >\n      <MDX mdxSource={mdxSource} data={data} />\n    </PostLayout>\n  );\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs.jsx",
    "content": "import { useRouter } from 'next/navigation';\nimport { useEffect } from 'react';\nimport docs from '../data/docs';\n\nconst Docs = () => {\n  const router = useRouter();\n\n  useEffect(() => {\n    router.replace(docs[0].url);\n  }, [router]);\n\n  return null;\n};\n\nexport default Docs;\n"
  },
  {
    "path": "packages/website/src/pages/index.jsx",
    "content": "import DevelopersIcon from '../svg/features/developers.svg';\nimport ScaleIcon from '../svg/features/scale.svg';\nimport GitHub from '../components/GitHub';\nimport CrossPlatformIcon from '../svg/features/cross-platform.svg';\nimport Layout from '../components/Layout';\nimport { useEffect, useRef, useState } from 'react';\nimport Editor from '../components/Editor';\nimport classNames from 'classnames';\nimport Link from 'next/link';\nimport NoSSR, { withoutSSR } from '../components/NoSSR';\n\nimport LinkedInLogo from '../svg/users/linkedin.svg';\nimport MicrosoftLogo from '../svg/users/microsoft.svg';\nimport SalesforceLogo from '../svg/users/salesforce.svg';\nimport ZoomLogo from '../svg/users/zoom.svg';\nimport AirtableLogo from '../svg/users/airtable.svg';\nimport FigmaLogo from '../svg/users/figma.svg';\nimport MiroLogo from '../svg/users/miro.svg';\nimport SlackLogo from '../svg/users/slack.svg';\nimport CalendlyLogo from '../svg/users/calendly.svg';\nimport FrontLogo from '../svg/users/front.svg';\nimport GrammarlyLogo from '../svg/users/grammarly.svg';\nimport VoxMediaLogo from '../svg/users/vox-media.svg';\nimport ApolloLogo from '../svg/users/apollo.svg';\nimport GemLogo from '../svg/users/gem.svg';\nimport ModeLogo from '../svg/users/mode.svg';\nimport TypeformLogo from '../svg/users/typeform.svg';\nimport SlabLogo from '../svg/users/slab.svg';\n\nconst fonts = ['sofia', 'slabo', 'roboto', 'inconsolata', 'ubuntu'];\nconst userBuckets = [\n  [\n    ['LinkedIn', 'https://www.linkedin.com/', LinkedInLogo],\n    ['Microsoft', 'https://www.microsoft.com/', MicrosoftLogo],\n    ['Salesforce', 'https://www.salesforce.com/', SalesforceLogo],\n    ['Zoom', 'https://zoom.us/', ZoomLogo],\n  ],\n  [\n    ['Airtable', 'https://airtable.com/', AirtableLogo],\n    ['Figma', 'https://www.figma.com/', FigmaLogo],\n    ['Miro', 'https://miro.com/', MiroLogo],\n    ['Slack', 'https://slack.com/', SlackLogo],\n  ],\n  [\n    ['Calendly', 'https://calendly.com/', CalendlyLogo],\n    ['Front', 'https://frontapp.com/', FrontLogo],\n    ['Grammarly', 'https://www.grammarly.com/', GrammarlyLogo],\n    ['Vox Media', 'https://www.voxmedia.com/', VoxMediaLogo],\n  ],\n  [\n    ['Apollo', 'https://www.apollo.io/', ApolloLogo],\n    ['Gem', 'https://www.gem.com/', GemLogo],\n    ['Mode', 'https://mode.com/', ModeLogo],\n    ['Typeform', 'https://www.typeform.com/', TypeformLogo],\n  ],\n  [['Slab', 'https://slab.com/', SlabLogo]],\n];\n\nconst Content = () => {\n  const cdn = process.env.cdn;\n\n  return (\n    <div\n      dangerouslySetInnerHTML={{\n        __html: `\n                <h1 class=\"ql-align-center\">Quill Rich Text Editor</h1>\n                <p><br></p>\n                <p>Quill is a free, <a href=\"https://github.com/slab/quill/\">open source</a> WYSIWYG editor built for the modern web. With its <a href=\"https://quilljs.com/docs/modules/\">modular architecture</a> and expressive <a href=\"https://quilljs.com/docs/api\">API</a>, it is completely customizable to fit any need.</p>\n                <p><br></p>\n                <iframe class=\"ql-video ql-align-center\" src=\"https://player.vimeo.com/video/253905163\" width=\"500\" height=\"280\" allowfullscreen></iframe>\n                <p><br></p>\n                <h2 class=\"ql-align-center\">Getting Started is Easy</h2>\n                <p><br></p>\n                <pre data-language=\"javascript\" class=\"ql-syntax\" spellcheck=\"false\"><span class=\"hljs-comment\">// &lt;link href=\"${cdn}/quill.snow.css\" rel=\"stylesheet\"&gt;</span>\n<span class=\"hljs-comment\">// &lt;script src=\"${cdn}/quill.js\"&gt;&lt;/script&gt;</span>\n\n<span class=\"hljs-keyword\">const</span> quill = <span class=\"hljs-keyword\">new</span> Quill(<span class=\"hljs-string\">'#editor'</span>, {\n  modules: {\n    toolbar: <span class=\"hljs-string\">'#toolbar'</span>\n  },\n  theme: <span class=\"hljs-string\">'snow'</span>\n});\n\n<span class=\"hljs-comment\">// Open your browser's developer console to try out the API!</span>\n</pre>\n                <p><br></p>\n                <p><br></p>\n                <p class=\"ql-align-center\"><strong>Built with</strong></p>\n                <p class=\"ql-align-center\"><span class=\"ql-formula\" data-value=\"x^2 + (y - \\\\sqrt[3]{x^2})^2 = 1\"></span></p>\n                <p><br></p>\n\n`,\n      }}\n    />\n  );\n};\n\nconst Users = withoutSSR(() => {\n  const [selectedUsers] = useState(() =>\n    userBuckets.map((bucket) => {\n      const index = Math.floor(Math.random() * bucket.length);\n      return bucket[index];\n    }),\n  );\n\n  return (\n    <ul id=\"logo-container\">\n      <li>Used In</li>\n      {selectedUsers.map(([name, url, Logo]) => (\n        <li key={name}>\n          <a title={name} href={url} target=\"_blank\">\n            <Logo />\n          </a>\n        </li>\n      ))}\n    </ul>\n  );\n});\n\nconst IndexPage = () => {\n  const [activeIndex, setActiveIndex] = useState(1);\n  const [isDemoActive, setIsDemoActive] = useState(false);\n  const isFirstRenderRef = useRef(true);\n\n  useEffect(() => {\n    // @ts-expect-error\n    const Font = Quill.import('formats/font');\n    Font.whitelist = fonts;\n    // @ts-expect-error\n    Quill.register(Font, true);\n\n    function loadFonts() {\n      window.WebFontConfig = {\n        google: {\n          families: [\n            'Inconsolata::latin',\n            'Ubuntu+Mono::latin',\n            'Slabo+27px::latin',\n            'Roboto+Slab::latin',\n          ],\n        },\n      };\n      (function () {\n        var wf = document.createElement('script');\n        wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';\n        wf.type = 'text/javascript';\n        wf.async = 'true';\n        var s = document.getElementsByTagName('script')[0];\n        s.parentNode.insertBefore(wf, s);\n      })();\n    }\n\n    loadFonts();\n  }, []);\n\n  const [quills, setQuills] = useState([]);\n\n  const handleEditorLoad = (index) => (quill) => {\n    setQuills((q) => {\n      const n = [...q];\n      n[index] = quill;\n      return n;\n    });\n  };\n\n  useEffect(() => {\n    const quill = quills[activeIndex];\n    if (!quill) return;\n\n    window.quill = quill;\n\n    if (isFirstRenderRef.current) {\n      console.log(\n        \"Welcome to Quill!\\n\\nThe editor on this page is available via `quill`. Give the API a try:\\n\\n\\tquill.formatText(11, 4, 'bold', true);\\n\\nVisit the API documentation page to learn more: https://quilljs.com/docs/api/\\n\",\n      );\n    } else {\n      console.info('window.quill is now bound to', quill);\n    }\n\n    isFirstRenderRef.current = false;\n  }, [activeIndex, quills]);\n\n  return (\n    <Layout>\n      <div\n        id=\"above-container\"\n        className={classNames({ 'demo-active': isDemoActive })}\n      >\n        <div className=\"container\">\n          <div id=\"announcement-container\">\n            <a\n              target=\"_blank\"\n              href=\"https://slab.com/blog/announcing-quill-2-0/\"\n            >\n              <strong>Quill 2.0 is released!</strong>\n              &nbsp;&nbsp;&bull;&nbsp;&nbsp;Read the\n              announcement&nbsp;&nbsp;&gt;\n            </a>\n          </div>\n          <div id=\"users-container\">\n            <h2>\n              <button\n                className=\"prev\"\n                style={{ visibility: activeIndex === 0 ? 'hidden' : undefined }}\n                onClick={() => setActiveIndex(activeIndex - 1)}\n              >\n                <span className=\"arrow\">\n                  <span className=\"tip\"></span>\n                  <span className=\"shaft\"></span>\n                </span>\n              </button>\n              Switch Examples\n              <button\n                className=\"next\"\n                style={{ visibility: activeIndex === 2 ? 'hidden' : undefined }}\n                onClick={() => setActiveIndex(activeIndex + 1)}\n              >\n                <span className=\"arrow\">\n                  <span className=\"tip\"></span>\n                  <span className=\"shaft\"></span>\n                </span>\n              </button>\n            </h2>\n            <h1>Your powerful rich text editor.</h1>\n            <Users />\n          </div>\n\n          <div id=\"laptop-container\" onClick={() => setIsDemoActive(true)}>\n            <div id=\"camera-container\">\n              {[0, 1, 2].map((index) => (\n                <div\n                  key={index}\n                  className={classNames('camera', {\n                    active: activeIndex === index,\n                  })}\n                  onClick={() => {\n                    setActiveIndex(index);\n                    setIsDemoActive(true);\n                  }}\n                >\n                  <div className=\"dot\" />\n                </div>\n              ))}\n            </div>\n            <NoSSR>\n              <div id=\"demo-container\">\n                <div\n                  id=\"carousel-container\"\n                  style={{ marginLeft: `${activeIndex * -100}%` }}\n                >\n                  <div id=\"bubble-wrapper\">\n                    <div id=\"bubble-container\">\n                      <Editor\n                        config={{\n                          bounds: '#bubble-container .ql-container',\n                          modules: {\n                            syntax: true,\n                          },\n                          theme: 'bubble',\n                        }}\n                        onLoad={handleEditorLoad(0)}\n                      >\n                        <Content />\n                      </Editor>\n                    </div>\n                  </div>\n                  <div id=\"snow-wrapper\">\n                    <div id=\"snow-container\">\n                      <div className=\"toolbar\">\n                        <span className=\"ql-formats\">\n                          <select className=\"ql-header\" defaultValue=\"3\">\n                            <option value=\"1\">Heading</option>\n                            <option value=\"2\">Subheading</option>\n                            <option value=\"3\">Normal</option>\n                          </select>\n                          <select className=\"ql-font\" defaultValue=\"sailec\">\n                            <option value=\"sailec\">Sailec Light</option>\n                            <option value=\"sofia\">Sofia Pro</option>\n                            <option value=\"slabo\">Slabo 27px</option>\n                            <option value=\"roboto\">Roboto Slab</option>\n                            <option value=\"inconsolata\">Inconsolata</option>\n                            <option value=\"ubuntu\">Ubuntu Mono</option>\n                          </select>\n                        </span>\n                        <span className=\"ql-formats\">\n                          <button className=\"ql-bold\"></button>\n                          <button className=\"ql-italic\"></button>\n                          <button className=\"ql-underline\"></button>\n                        </span>\n                        <span className=\"ql-formats\">\n                          <button className=\"ql-list\" value=\"ordered\"></button>\n                          <button className=\"ql-list\" value=\"bullet\"></button>\n                          <select className=\"ql-align\" defaultValue=\"false\">\n                            <option label=\"left\"></option>\n                            <option label=\"center\" value=\"center\"></option>\n                            <option label=\"right\" value=\"right\"></option>\n                            <option label=\"justify\" value=\"justify\"></option>\n                          </select>\n                        </span>\n                        <span className=\"ql-formats\">\n                          <button className=\"ql-link\"></button>\n                          <button className=\"ql-image\"></button>\n                          <button className=\"ql-video\"></button>\n                        </span>\n                        <span className=\"ql-formats\">\n                          <button className=\"ql-formula\"></button>\n                          <button className=\"ql-code-block\"></button>\n                        </span>\n                        <span className=\"ql-formats\">\n                          <button className=\"ql-clean\"></button>\n                        </span>\n                      </div>\n                      <Editor\n                        config={{\n                          bounds: '#snow-container .ql-container',\n                          modules: {\n                            syntax: true,\n                            toolbar: '#snow-container .toolbar',\n                          },\n                          theme: 'snow',\n                        }}\n                        onLoad={handleEditorLoad(1)}\n                      >\n                        <Content />\n                      </Editor>\n                    </div>\n                  </div>\n                  <div id=\"full-wrapper\">\n                    <div id=\"full-container\">\n                      <Editor\n                        config={{\n                          bounds: '#full-container .ql-container',\n                          modules: {\n                            syntax: true,\n                            toolbar: [\n                              [{ font: fonts }, { size: [] }],\n                              ['bold', 'italic', 'underline', 'strike'],\n                              [{ color: [] }, { background: [] }],\n                              [{ script: 'super' }, { script: 'sub' }],\n                              [\n                                { header: '1' },\n                                { header: '2' },\n                                'blockquote',\n                                'code-block',\n                              ],\n                              [\n                                { list: 'ordered' },\n                                { list: 'bullet' },\n                                { indent: '-1' },\n                                { indent: '+1' },\n                              ],\n                              [{ direction: 'rtl' }, { align: [] }],\n                              ['link', 'image', 'video', 'formula'],\n                              ['clean'],\n                            ],\n                          },\n                          theme: 'snow',\n                        }}\n                        onLoad={handleEditorLoad(2)}\n                      >\n                        <Content />\n                      </Editor>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </NoSSR>\n          </div>\n        </div>\n      </div>\n\n      <div id=\"detail-container\">\n        <div className=\"container\">\n          <Link className=\"action\" href=\"/docs/quickstart\">\n            Documentation\n          </Link>\n          <h1>An API Driven Rich Text Editor</h1>\n        </div>\n      </div>\n\n      <div id=\"features-container\">\n        <div className=\"container\">\n          <div className=\"row\">\n            <div className=\"feature columns\">\n              <DevelopersIcon />\n              <div className=\"details\">\n                <h2>Built for Developers</h2>\n                <span>\n                  Granular access to the editor&apos;s content, changes and\n                  events through a simple API. Works consistently and\n                  deterministically with JSON as both input and output.\n                </span>\n              </div>\n            </div>\n            <div className=\"feature columns\">\n              <CrossPlatformIcon />\n              <div className=\"details\">\n                <h2>Cross Platform</h2>\n                <span>\n                  Supports all modern browsers on desktops, tablets and phones.\n                  Experience the same consistent behavior and produced HTML\n                  across platforms.\n                </span>\n              </div>\n            </div>\n          </div>\n\n          <div id=\"github-wrapper\">\n            <div id=\"github-container\">\n              <GitHub />\n            </div>\n          </div>\n\n          <hr />\n\n          <div className=\"feature row\">\n            <div className=\"columns details\">\n              <h2>Fits Like a Glove</h2>\n              <span>\n                Used in small projects and giant Fortune 500s alike. Start\n                simple with the Quill core then easily customize or add your own\n                extensions later if your product needs grow.\n              </span>\n              <Link className=\"action-link\" href=\"/docs/quickstart\">\n                Learn More\n              </Link>\n            </div>\n            <div className=\"columns\">\n              <ScaleIcon />\n            </div>\n          </div>\n        </div>\n      </div>\n    </Layout>\n  );\n};\n\nexport default IndexPage;\n"
  },
  {
    "path": "packages/website/src/pages/playground/[...id].jsx",
    "content": "import playground from '../../data/playground';\nimport { readFile, readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n  SandpackCodeEditor,\n  SandpackPreview,\n  SandpackProvider,\n} from '@codesandbox/sandpack-react';\nimport NoSSR, { withoutSSR } from '../../components/NoSSR';\nimport { useState } from 'react';\nimport replaceCDN from '../../utils/replaceCDN';\nimport styles from './[...id].module.scss';\nimport PlaygroundLayout from '../../components/PlaygroundLayout';\nimport { decompressFromEncodedURIComponent } from 'lz-string';\nimport Layout from '../../components/Layout';\nimport OpenSource from '../../components/OpenSource';\nimport classNames from 'classnames';\n\nexport async function getStaticPaths() {\n  return {\n    paths: playground.map((d) => d.url),\n    fallback: false,\n  };\n}\n\nexport async function getStaticProps({ params }) {\n  const basePath = join('src', 'playground', params.id.join('/'));\n  const files = await readdir(basePath);\n  const pack = {};\n  await Promise.all(\n    files.map(async (file) => {\n      const content = await readFile(join(basePath, file), 'utf-8');\n      pack[file] = content;\n    }),\n  );\n\n  const permalink = `/playground/${params.id}`;\n  return {\n    props: {\n      pack,\n      permalink,\n      title: playground.find((d) => d.url === permalink).title,\n    },\n  };\n}\n\nfunction Playground({ pack, permalink, title }) {\n  const { 'playground.json': raw, ...files } = pack;\n\n  let metadata = {};\n  try {\n    metadata = JSON.parse(raw);\n  } catch (err) {}\n\n  const [overrides] = useState(() => {\n    if (location.hash.startsWith('#code')) {\n      const code = location.hash.replace('#code', '').trim();\n      let userCode;\n      try {\n        userCode = JSON.parse(decompressFromEncodedURIComponent(code));\n      } catch (err) {}\n      return userCode || {};\n    }\n    return {};\n  });\n\n  return (\n    <Layout title={title} pageType=\"playground\">\n      <div className={classNames('container', styles.container)}>\n        <h1>Playground</h1>\n        <NoSSR>\n          <SandpackProvider\n            key={permalink}\n            options={{\n              activeFile:\n                metadata.activeFile ||\n                (files['index.js'] ? 'index.js' : Object.keys(files)[0]),\n              externalResources:\n                metadata.externalResources &&\n                metadata.externalResources.map(replaceCDN),\n            }}\n            template={metadata.template}\n            files={Object.keys(files).reduce((f, name) => {\n              const fullName = name.startsWith('/') ? name : `/${name}`;\n              return {\n                ...f,\n                [name]: replaceCDN(overrides[fullName] ?? files[name]).trim(),\n              };\n            }, {})}\n          >\n            <PlaygroundLayout permalink={permalink} title={title} files={files}>\n              <div className={styles.wrapper}>\n                <div className={styles.editor}>\n                  <SandpackCodeEditor\n                    showTabs\n                    wrapContent\n                    showRunButton={false}\n                  />\n                </div>\n                <div className={styles.preview}>\n                  <SandpackPreview showOpenInCodeSandbox={false} />\n                </div>\n              </div>\n            </PlaygroundLayout>\n          </SandpackProvider>\n        </NoSSR>\n        <OpenSource />\n      </div>\n    </Layout>\n  );\n}\n\nexport default withoutSSR(Playground);\n"
  },
  {
    "path": "packages/website/src/pages/playground/[...id].module.scss",
    "content": ".container {\n  margin: 40px auto 20px;\n}\n\n.wrapper {\n  display: flex;\n  border: 1px solid #ccc;\n  height: 500px;\n\n  .editor {\n    flex: 1;\n    border-right: 1px solid #ccc;\n    display: flex;\n    min-width: 0;\n  }\n\n  .preview {\n    flex: 1;\n    display: flex;\n  }\n}\n"
  },
  {
    "path": "packages/website/src/pages/playground.jsx",
    "content": "import { useRouter } from 'next/navigation';\nimport { useEffect } from 'react';\nimport playground from '../data/playground';\n\nconst Playground = () => {\n  const router = useRouter();\n\n  useEffect(() => {\n    router.replace(playground[0].url);\n  }, [router]);\n\n  return null;\n};\n\nexport default Playground;\n"
  },
  {
    "path": "packages/website/src/pages/standalone/bubble.mdx",
    "content": "<title>Bubble Theme</title>\n\nimport { StandaloneSandpack } from '../../components/Sandpack';\n\n<StandaloneSandpack\n  preferPreview\n  activeFile=\"index.js\"\n  files={{\n    'index.html': `\n<script src=\"{{site.highlightjs}}/highlight.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.highlightjs}}/styles/atom-one-dark.min.css\" />\n<script src=\"{{site.katex}}/katex.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.katex}}/katex.min.css\" />\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.bubble.css\" />\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<div id=\"editor\" style=\"height: 220px\">\n</div>\n<script src=\"/index.js\"></script>\n    `,\n    'index.js': `\nconst quill = new Quill('#editor', {\n placeholder: 'Compose an epic...',\n theme: 'bubble' \n});\n    `\n  }}\n/>"
  },
  {
    "path": "packages/website/src/pages/standalone/full.mdx",
    "content": "import { StandaloneSandpack } from '../../components/Sandpack';\n\n<title>Full Editor</title>\n\n<StandaloneSandpack\n  preferPreview\n  activeFile=\"index.js\"\n  files={{\n    'index.html': `\n<script src=\"{{site.highlightjs}}/highlight.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.highlightjs}}/styles/atom-one-dark.min.css\" />\n<script src=\"{{site.katex}}/katex.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.katex}}/katex.min.css\" />\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.snow.css\" />\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.bubble.css\" />\n<script src=\"{{site.cdn}}/quill.js\"></script>\n\n<div id=\"standalone-container\">\n  <div id=\"toolbar-container\">\n    <span class=\"ql-formats\">\n      <select class=\"ql-font\"></select>\n      <select class=\"ql-size\"></select>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-bold\"></button>\n      <button class=\"ql-italic\"></button>\n      <button class=\"ql-underline\"></button>\n      <button class=\"ql-strike\"></button>\n    </span>\n    <span class=\"ql-formats\">\n      <select class=\"ql-color\"></select>\n      <select class=\"ql-background\"></select>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-script\" value=\"sub\"></button>\n      <button class=\"ql-script\" value=\"super\"></button>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-header\" value=\"1\"></button>\n      <button class=\"ql-header\" value=\"2\"></button>\n      <button class=\"ql-blockquote\"></button>\n      <button class=\"ql-code-block\"></button>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-list\" value=\"ordered\"></button>\n      <button class=\"ql-list\" value=\"bullet\"></button>\n      <button class=\"ql-indent\" value=\"-1\"></button>\n      <button class=\"ql-indent\" value=\"+1\"></button>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-direction\" value=\"rtl\"></button>\n      <select class=\"ql-align\"></select>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-link\"></button>\n      <button class=\"ql-image\"></button>\n      <button class=\"ql-video\"></button>\n      <button class=\"ql-formula\"></button>\n    </span>\n    <span class=\"ql-formats\">\n      <button class=\"ql-clean\"></button>\n    </span>\n  </div>\n  <div id=\"editor\" style=\"height: 220px\">\n  </div>\n</div>\n<script src=\"/index.js\"></script>\n    `,\n    'index.js': `\nconst quill = new Quill('#editor', {\n  modules: {\n    syntax: true,\n    toolbar: '#toolbar-container',\n  },\n  placeholder: 'Compose an epic...',\n  theme: 'snow',\n});\n    `\n  }}\n/>\n"
  },
  {
    "path": "packages/website/src/pages/standalone/snow.mdx",
    "content": "<title>Snow Theme</title>\n\nimport { StandaloneSandpack } from '../../components/Sandpack';\n\n<StandaloneSandpack\n  preferPreview\n  activeFile=\"index.js\"\n  files={{\n    'index.html': `\n<script src=\"{{site.highlightjs}}/highlight.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.highlightjs}}/styles/atom-one-dark.min.css\" />\n<script src=\"{{site.katex}}/katex.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.katex}}/katex.min.css\" />\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.snow.css\" />\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<div id=\"editor\" style=\"height: 220px\">\n</div>\n<script src=\"/index.js\"></script>\n    `,\n    'index.js': `\nconst quill = new Quill('#editor', {\n placeholder: 'Compose an epic...',\n theme: 'snow' \n});\n    `\n  }}\n/>"
  },
  {
    "path": "packages/website/src/pages/standalone/stress.mdx",
    "content": "<title>Stress</title>\n\nimport { StandaloneSandpack } from '../../components/Sandpack';\n\n<StandaloneSandpack\n  preferPreview\n  activeFile=\"index.js\"\n  files={{\n    'index.html': `\n<script src=\"{{site.highlightjs}}/highlight.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.highlightjs}}/styles/atom-one-dark.min.css\" />\n<script src=\"{{site.katex}}/katex.min.js\"></script>\n<link rel=\"stylesheet\" href=\"{{site.katex}}/katex.min.css\" />\n<link rel=\"stylesheet\" href=\"{{site.cdn}}/quill.snow.css\" />\n<script src=\"{{site.cdn}}/quill.js\"></script>\n<div id=\"editor\" style=\"height: 220px\">\n</div>\n<script src=\"/index.js\"></script>\n    `,\n    'index.js': `\n\nconst editor = document.getElementById('editor');\n\neditor.innerHTML = new Array(200).fill(0).map((_, index) => {\n  return \\`\n    <h2>Heading <i>\\${index}</i></h2>\n    <p>List items:</p>\n    <ul>\n    \\${\n      new Array(20).fill(0).map((_, index) => {\n        return \\`<li>List <strong>item</strong> \\${index}</li>\\`\n      }).join('')\n    }\n    </ul>\n  </>\\`\n}).join('')\n\nconst quill = new Quill('#editor', {\n placeholder: 'Compose an epic...',\n theme: 'snow' \n});\n    `\n  }}\n/>"
  },
  {
    "path": "packages/website/src/pages/styles.scss",
    "content": "@font-face {\n  font-family: 'Sofia Pro';\n  src: url('/assets/fonts/sofia-pro.woff2') format('woff2');\n}\n@font-face {\n  font-family: 'Sofia Pro';\n  font-weight: bold;\n  src: url('/assets/fonts/sofia-pro-bold.woff2') format('woff2');\n}\n@font-face {\n  font-family: 'Sailec Light';\n  src: url('/assets/fonts/sailec-light.woff2') format('woff2');\n}\n@font-face {\n  font-family: 'Sailec Light';\n  font-weight: bold;\n  src: url('/assets/fonts/sailec-bold.woff2') format('woff2');\n}\n\n/*** Native ***/\n\n$headerHeight: 74px;\n\nhtml {\n  font-size: 12px;\n  scroll-padding-top: $headerHeight + 24px;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  overflow-x: hidden;\n}\n\na {\n  text-decoration: none;\n  color: inherit;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  font-family: 'Sofia Pro', sans-serif;\n  font-weight: normal;\n}\n\nh1 {\n  font-size: 4.2rem;\n}\n\nh2 {\n  font-size: 3rem;\n}\n\nh3 {\n  font-size: 2.5rem;\n}\n\nh4 {\n  font-size: 2rem;\n}\n\nhr {\n  border-top: 1px solid #d9dce1;\n}\n\nli {\n  list-style: none;\n}\n\ntable {\n  text-align: left;\n}\n\nbutton {\n  background-color: transparent;\n  cursor: pointer;\n  font-size: 11px;\n  outline: none;\n  text-align: center;\n}\n\nstrong {\n  font-weight: bold;\n}\n\npre,\ncode {\n  font-family: 'Inconsolata', monospace;\n}\n\npre {\n  margin-bottom: 2.5rem;\n}\n\ncode {\n  font-size: 1.2rem;\n}\n\npre > code {\n  display: block;\n  border: 1px solid #dadcdc;\n  padding: 0.5em 1em;\n  white-space: pre-wrap;\n  overflow-x: auto;\n}\n\n*:not(pre) > code {\n  border: none;\n  border-radius: 0px;\n  padding: 0.2rem 0.5rem;\n}\n\nh2 + hr {\n  margin-top: 0;\n}\n\n/*** Global ***/\n\nsvg .logo {\n  fill: #1d1e30;\n}\n\nsvg .feat-1 {\n  fill: #f2d123;\n}\nsvg .feat-2 {\n  fill: #e3a931;\n}\nsvg .feat-3 {\n  fill: #4f5363;\n}\n\n.action {\n  background-color: #f2d123;\n  border: 2px solid transparent;\n  display: inline-block;\n  font-family: 'Sofia Pro', sans-serif;\n  font-size: 1.2rem;\n  font-weight: bold;\n  letter-spacing: 0.15rem;\n  text-transform: uppercase;\n  transition:\n    background-color 100ms,\n    border-color 100ms,\n    color 100ms;\n}\n\n.logo {\n  display: inline-block;\n  height: 6.3em;\n  vertical-align: middle;\n  width: 10.5em;\n}\n\n.container {\n  max-width: 1200px;\n}\n\n.center {\n  text-align: center;\n}\n\n\n.arrow {\n  padding: 0.33em 0.67em;\n  .shaft {\n    background-color: #000;\n    border-bottom: 3px solid #fff;\n    border-top: 3px solid #fff;\n    float: left;\n    height: 0.33em;\n    width: 0.66em;\n  }\n  .tip {\n    display: inline-block;\n    border-bottom: 0.17em solid transparent;\n    border-top: 0.17em solid transparent;\n  }\n}\n\n.prev, .next, .arrow {\n  display: inline-block;\n  height: 1em;\n  line-height: 1em;\n}\n\n.prev {\n  float: left;\n\n  &:hover .arrow {\n    padding-left: 0.77em;\n    padding-right: 0.57em;\n  }\n\n  .label {\n    float: left;\n  }\n\n  .arrow {\n    float: left;\n  }\n  .tip {\n    float: left;\n    border-right: 0.33em solid #000;\n  }\n}\n.next, .next .label, .next .arrow {\n  float: right;\n\n  &:hover .arrow {\n    padding-left: 0.57em;\n    padding-right: 0.77em;\n  }\n\n  .label {\n    float: right;\n  }\n\n  .arrow {\n    float: right;\n  }\n\n  .tip {\n    float: right;\n    border-left: 0.33em solid #000;\n  }\n}\n.prev, .next {\n  .label {\n    font-size: 1.25rem;\n    display: inline-block;\n    height: 2em;\n    line-height: 2em;\n  }\n}\n\n.row > hr {\n  width: 90%;\n}\n\n.about a {\n  color: #25408f;\n  text-decoration: underline;\n}\n\n/*** Header ***/\n\nheader {\n  display: flex;\n  font-family: 'Sofia Pro', sans-serif;\n  text-align: center;\n}\nheader li {\n  margin-bottom: 0;\n}\n\nheader .action {\n  border-width: 3px;\n  color: #1d1e30;\n  display: inline;\n  padding: 1em;\n}\n\n.new-blog::after {\n  background-color: #f2d123;\n  border-radius: 6px;\n  color: #1d1e30;\n  content: 'new';\n  font-size: 0.8rem;\n  font-weight: bold;\n  margin-left: 5px;\n  padding: 1px 6px 1px 7px;\n  text-transform: uppercase;\n}\n\n.navbar-drop {\n  background-color: #1d1e30;\n  bottom: 100%;\n  font-size: 2rem;\n  letter-spacing: 0.15rem;\n  list-style: none;\n  padding: 5em 0 1em;\n  position: absolute;\n  transition: margin-bottom 0.4s ease-out 0s;\n  width: 100%;\n  z-index: 10;\n}\n.navbar-drop .navbar-item {\n  height: 3em;\n  line-height: 3em;\n}\n.navbar-drop .navbar-item:last-child {\n  height: 6em;\n  line-height: 6em;\n}\n.navbar-drop .navbar-item a {\n  color: #fff;\n}\n.navbar-drop .github-button {\n  vertical-align: middle;\n}\n.navbar-list {\n  color: #fff;\n  letter-spacing: 0.15rem;\n  list-style: none;\n  margin-bottom: 0;\n  margin-top: 0.67em;\n}\n.navbar-list li {\n  display: inline-block;\n  line-height: 7.33rem;\n}\n.navbar-list .navbar-item.active .navbar-link:before {\n  transform: scaleX(1);\n  visibility: visible;\n}\n.navbar-list .download-item {\n  margin-right: -2.5%;\n  position: absolute;\n  right: 0;\n}\n.navbar-list .download-item .action:hover {\n  background-color: #1d1e30;\n  color: #fff;\n}\n.navbar-link,\n.action-link {\n  position: relative;\n}\n.navbar-open,\n.navbar-close {\n  border: none;\n  margin-right: 10%;\n  padding: 0;\n  position: absolute;\n  right: 0;\n  top: 7.5em;\n}\n.navbar-close {\n  background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCAzNzEuMjMgMzcxLjIzIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzNzEuMjMgMzcxLjIzOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCI+Cjxwb2x5Z29uIHBvaW50cz0iMzcxLjIzLDIxLjIxMyAzNTAuMDE4LDAgMTg1LjYxNSwxNjQuNDAyIDIxLjIxMywwIDAsMjEuMjEzIDE2NC40MDIsMTg1LjYxNSAwLDM1MC4wMTggMjEuMjEzLDM3MS4yMyAgIDE4NS42MTUsMjA2LjgyOCAzNTAuMDE4LDM3MS4yMyAzNzEuMjMsMzUwLjAxOCAyMDYuODI4LDE4NS42MTUgIiBmaWxsPSIjRkZGRkZGIi8+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=);\n  height: 24px;\n  width: 24px;\n}\n.navbar-open {\n  display: none;\n}\n.navbar-open span {\n  background-color: #fff;\n  display: block;\n  height: 3px;\n  margin: 0.53em;\n  width: 2.5em;\n}\n\n/*** Footer ***/\n\nfooter {\n  font-size: 1.5rem;\n  background-color: #1d1e30;\n  color: #fff;\n  margin-top: 5.5em;\n  padding-bottom: 5.5em;\n  padding-top: 7.5em;\n  text-align: center;\n}\n\nfooter h1 {\n  font-family: 'Sailec Light', sans-serif;\n  font-size: 3.75rem;\n  letter-spacing: 0.2rem;\n  margin-bottom: 1.67em;\n  margin-top: 1.67em;\n}\n\nfooter .actions.row {\n  text-align: center;\n  display: flex;\n  justify-content: center;\n}\n\nfooter .action {\n  font-size: 1.5rem;\n  height: 4.44em;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin: 1.4em;\n  vertical-align: middle;\n  width: 14.5em;\n}\n\nfooter .action.documentation {\n  background-color: #1d1e30;\n  border-color: #f2d123;\n  color: #f2d123;\n}\nfooter .action:hover {\n  background-color: #fff;\n  border-color: transparent;\n  color: #1d1e30;\n}\n\nfooter .logo {\n  width: 10em;\n}\n\nfooter .logo circle,\nfooter .logo path {\n  fill: #fff;\n}\n\n/*** Home ***/\n\nheader.home {\n  color: #1d1e30;\n}\nheader.home a {\n  color: #1d1e30;\n}\nheader.home .action {\n  background-color: transparent;\n  border-color: #1d1e30;\n}\nheader.home .navbar-open span {\n  background-color: #1d1e30;\n}\nbody.home .arrow .shaft {\n  border-bottom-color: #f4f5f5;\n  border-top-color: #f4f5f5;\n}\n\n#above-container {\n  background-color: #f4f5f5;\n  position: relative;\n}\n#above-container > .container {\n  height: 840px;\n}\n\n#announcement-container {\n  font-family: 'Sofia Pro', sans-serif;\n  margin-top: 25px;\n  position: absolute;\n  text-align: center;\n  width: 100%;\n  z-index: 1;\n\n  a {\n    background-color: #f2d123;\n    border-radius: 8px;\n    display: inline-block;\n    font-size: 80%;\n    padding: 2px 10px;\n  }\n}\n\n#users-container {\n  font-size: 1.25rem;\n  left: 0;\n  padding-bottom: 0.53em;\n  padding-top: 5.53em;\n  position: absolute;\n  text-align: center;\n  top: 0;\n  width: 100%;\n}\n#users-container h1 {\n  font-size: 5rem;\n  margin-bottom: 0.25em;\n}\n#users-container h2 {\n  display: none;\n  font-size: 4rem;\n}\n#users-container .prev,\n#users-container .next {\n  font-size: 0.75em;\n  border: none;\n  height: 1.33em;\n}\n#logo-container {\n  list-style: none;\n  margin: 4em 0;\n  text-transform: uppercase;\n}\n#logo-container li {\n  display: inline-block;\n  height: 100%;\n  line-height: 100%;\n  margin: 0 1.4em;\n  vertical-align: middle;\n}\n#logo-container li:first-child {\n  font-size: 1.08rem;\n}\n#logo-container li:not(:first-child) {\n  width: 3em;\n}\n#logo-container a {\n  display: block;\n}\n#logo-container svg {\n  max-height: 3em;\n  max-width: 3em;\n}\n\n#laptop-container {\n  background-color: #000;\n  border: 4px solid #707070;\n  bottom: -145px;\n  border-bottom: 0px;\n  border-top-left-radius: 40px;\n  border-top-right-radius: 40px;\n  box-shadow: 0 0 40px 2px rgba(28, 31, 47, 0.1);\n  font-size: 1.5rem;\n  padding: 20px 20px 0 20px;\n  position: absolute;\n  width: 100%;\n}\n#above-container:not(.demo-active) #laptop-container:hover {\n  bottom: -135px;\n}\n#above-container:not(.demo-active) .ql-editor {\n  overflow-y: hidden;\n}\n#camera-container {\n  background-color: #000;\n  border-bottom-left-radius: 8px;\n  border-bottom-right-radius: 8px;\n  left: 50%;\n  line-height: 26px;\n  margin-left: -90px;\n  position: absolute;\n  text-align: center;\n  width: 180px;\n  z-index: 1;\n}\n\n#camera-container .camera {\n  cursor: pointer;\n  display: inline-block;\n  height: 30px;\n  width: 30px;\n}\n\n#camera-container .camera .dot {\n  background-color: #707070;\n  border-radius: 4px;\n  display: inline-block;\n  height: 8px;\n  width: 8px;\n}\n#camera-container .camera.active .dot {\n  background-color: #2dc937;\n}\n\n#demo-container {\n  background-color: #fff;\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n  opacity: 0.85;\n  overflow: hidden;\n  width: 100%;\n}\n#demo-container h1 {\n  font-size: 3rem;\n}\n#demo-container h2 {\n  font-size: 2rem;\n}\n#demo-container blockquote {\n  font-size: 1.5rem;\n}\n#demo-container .ql-size-large {\n  font-size: 24px;\n}\n#above-container.demo-active #demo-container,\n#demo-container:hover {\n  opacity: 1;\n}\n#carousel-container {\n  font-size: 1.5rem;\n  margin-left: -100%;\n  margin-top: 30px;\n  transition: margin 500ms ease-in-out 0s;\n  width: 300%;\n}\n#carousel-container::after {\n  clear: both;\n  content: '';\n  display: table;\n}\n#demo-container .ql-snow {\n  border: none;\n}\n#demo-container .ql-snow a:hover,\n#demo-container .ql-snow a:active {\n  color: #06c;\n}\n#bubble-wrapper .ql-editor {\n  padding: 10% 10% 25px;\n}\n#snow-wrapper {\n  padding: 10px 20px 0;\n}\n#snow-wrapper .ql-editor {\n  padding: 0 17% 25px;\n}\n#demo-container #snow-container,\n#demo-container #full-container,\n#demo-container #bubble-container {\n  display: flex;\n  flex-flow: column;\n  height: 100%;\n}\n#demo-container #snow-container .ql-container,\n#demo-container #full-container .ql-container,\n#demo-container #bubble-container .ql-container {\n  flex: 2;\n  overflow: hidden;\n}\n#bubble-wrapper,\n#snow-wrapper,\n#full-wrapper {\n  display: inline-block;\n  float: left;\n  height: 595px;\n  width: 33.33%;\n}\n#snow-wrapper .toolbar {\n  border: none;\n  padding: 4.5% 16%;\n}\n#snow-wrapper .editor {\n  border: none;\n}\n#full-wrapper .ql-toolbar {\n  border-bottom: 1px solid #ccc;\n}\n#full-wrapper .ql-editor {\n  padding: 5% 8% 25px;\n}\n\n#above-container.demo-active #announcement-container {\n  display: none;\n}\n#above-container.demo-active #users-container h1 {\n  display: none;\n}\n#above-container.demo-active #users-container h2 {\n  display: inline-block;\n}\n#above-container.demo-active #users-container ul {\n  transition: opacity 500ms linear 0s;\n  opacity: 0;\n}\n#above-container.demo-active #demo-container .ql-editor {\n  overflow-y: auto;\n}\n#above-container.demo-active #laptop-container {\n  transition: bottom 500ms ease-in-out 0s;\n  bottom: 0;\n}\n\n#detail-container {\n  background-color: #fff;\n  font-size: 1.5rem;\n  margin-top: -5px;\n  position: relative;\n  text-align: center;\n  z-index: 5;\n}\n\n#detail-container .action {\n  display: inline-block;\n  font-size: 1.5rem;\n  letter-spacing: 0.25rem;\n  margin-bottom: 2em;\n  margin-top: 6.89em;\n  padding: 1.5em 2.78em;\n}\n#detail-container .action:hover {\n  background-color: #1d1e30;\n  color: #fff;\n}\n\n#detail-container h1 {\n  font-family: 'Sailec Light', sans-serif;\n  letter-spacing: 0.2rem;\n  font-size: 3.33rem;\n}\n\n#features-container {\n  background-color: #fff;\n  padding-top: 5.83em;\n  position: relative;\n  z-index: 10;\n}\n\n#features-container .feature {\n  padding: 1.4em 1.4em 5.56em;\n}\n\n#features-container #github-wrapper {\n  height: 3.33em;\n  margin: 0 auto;\n  position: relative;\n  text-align: center;\n  width: 18em;\n}\n#features-container .feature.columns:last-child {\n  border-left: 1px solid #d9dce1;\n}\n#features-container .feature.row {\n  margin-top: 5.56em;\n}\n.feature.columns,\n.feature.row .columns {\n  width: 48%;\n}\n.feature.row {\n  display: block;\n}\n.feature .details {\n  padding: 1.7em 0 1.8em 3.5em;\n}\n\n#features-container #github-container {\n  background-color: #fff;\n  margin-left: 1.8em;\n  margin-top: -1.67em;\n}\n\n#gallery-container {\n  display: flex;\n  flex-flow: row wrap;\n  justify-content: space-between;\n  margin-bottom: 75px;\n}\n#gallery-container .selected-pen {\n  display: none;\n}\n#gallery-container a {\n  border: 2px solid #000;\n  display: block;\n  margin-bottom: 25px;\n  position: relative;\n}\n#gallery-container .pen-label {\n  background-color: #000;\n  bottom: 0;\n  color: #fff;\n  display: block;\n  font-size: 18px;\n  opacity: 0.5;\n  padding: 15px;\n  position: absolute;\n  text-align: center;\n  width: 100%;\n}\n#gallery-container a:hover .pen-label {\n  opacity: 0.75;\n  padding-bottom: 18px;\n  padding-top: 18px;\n}\n#gallery-container img {\n  width: 100%;\n}\n#gallery-container > div {\n  margin-bottom: 15px;\n  margin-left: 0;\n}\n\n#features-container #github-wrapper + hr {\n  margin-top: 0px;\n  width: 85%;\n}\n\n.feature.columns img {\n  margin-left: 1.4em;\n  margin-right: 1.4em;\n}\n\n.feature .action-link {\n  display: inline-block;\n  font-family: 'Sofia Pro', sans-serif;\n  font-size: 1.25rem;\n  font-weight: bold;\n  letter-spacing: 0.15rem;\n  margin-top: 2.5em;\n  text-transform: uppercase;\n}\n\n.feature .details > h2 {\n  font-size: 2.33rem;\n  font-weight: bold;\n  letter-spacing: 0.4rem;\n  margin-bottom: 1.8em;\n  text-transform: uppercase;\n}\n\n.feature .details > span {\n  display: block;\n  font-size: 1.33rem;\n  letter-spacing: 0.1rem;\n  line-height: 200%;\n  padding-right: 2em;\n}\n\n/*** Home Demo ***/\n#demo-container .ql-editor h1,\n#demo-container .ql-editor h2,\n#demo-container .ql-editor h3,\n#demo-container .ql-editor h4,\n#demo-container .ql-editor h5,\n#demo-container .ql-editor h6,\n#demo-container .ql-editor,\n#demo-container .ql-toolbar {\n  font-family: 'Sailec Light', sans-serif;\n}\n#demo-container .ql-toolbar .ql-picker-label {\n  font-size: 15px;\n}\n#demo-container .ql-snow .ql-header.ql-picker {\n  width: 115px;\n}\n#demo-container .ql-snow .ql-font.ql-picker {\n  width: 132px;\n}\n#demo-container\n  .ql-snow\n  .ql-picker.ql-font\n  .ql-picker-label:not([data-label])::before {\n  content: 'Sailec Light';\n}\n#demo-container .ql-snow .ql-font .ql-picker-item[data-value='sofia']::before,\n#demo-container\n  .ql-snow\n  .ql-font\n  .ql-picker-label[data-value='sofia']:not([data-label])::before,\n#demo-container .ql-font-sofia {\n  content: 'Sofia Pro';\n  font-family: 'Sofia Pro', sans-serif;\n}\n#demo-container .ql-snow .ql-font .ql-picker-item[data-value='slabo']::before,\n#demo-container\n  .ql-snow\n  .ql-font\n  .ql-picker-label[data-value='slabo']:not([data-label])::before,\n#demo-container .ql-font-slabo {\n  content: 'Slabo 13px';\n  font-family: 'Slabo 13px', sans-serif;\n}\n#demo-container .ql-snow .ql-font .ql-picker-item[data-value='roboto']::before,\n#demo-container\n  .ql-snow\n  .ql-font\n  .ql-picker-label[data-value='roboto']:not([data-label])::before,\n#demo-container .ql-font-roboto {\n  content: 'Roboto Slab';\n  font-family: 'Roboto Slab', serif;\n}\n#demo-container\n  .ql-snow\n  .ql-font\n  .ql-picker-item[data-value='inconsolata']::before,\n#demo-container\n  .ql-snow\n  .ql-font\n  .ql-picker-label[data-value='inconsolata']:not([data-label])::before,\n#demo-container .ql-font-inconsolata {\n  content: 'Inconsolata';\n  font-family: 'Inconsolata', monospace;\n}\n#demo-container .ql-snow .ql-font .ql-picker-item[data-value='ubuntu']::before,\n#demo-container\n  .ql-snow\n  .ql-font\n  .ql-picker-label[data-value='ubuntu']:not([data-label])::before,\n#demo-container .ql-font-ubuntu {\n  content: 'Ubuntu Mono';\n  font-family: 'Ubuntu Mono', monospace;\n}\n#demo-container .ql-editor {\n  font-size: 1.5em;\n}\n#demo-container .ql-editor li {\n  margin-bottom: 0;\n}\n#demo-container .ql-editor pre,\n#demo-container .ql-editor .ql-code-block-container {\n  font-family: 'Inconsolata', monospace;\n  font-size: 0.8em;\n}\n\n/*** Docs ***/\n\nbody:not(.home) .feature.row {\n  margin-top: 2.78em;\n}\n\nbody:not(.home) .feature h2 {\n  margin-bottom: 1.5em;\n}\n\nbody:not(.home) .navbar-list .download-item .action:hover {\n  background-color: #fff;\n  border-color: #fff;\n  color: #1d1e30;\n}\nbody:not(.home) .navbar-link:before {\n  background-color: #fff;\n}\n\n.experimental {\n  background-color: #f2d123;\n  border-radius: 6px;\n  color: #1d1e30;\n  display: inline-block;\n  font-size: 1.2rem;\n  font-weight: bold;\n  margin-left: 12px;\n  margin-top: 8px;\n  padding: 1px 6px 1px 7px;\n  position: relative;\n  text-transform: uppercase;\n  vertical-align: top;\n}\n.experimental:hover::before,\n.experimental:hover::after {\n  visibility: visible;\n}\n.experimental::before,\n.experimental::after {\n  top: 0;\n  left: 0;\n  margin-left: 100%;\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n  transition: visibility 0s ease 0.2s;\n  visibility: hidden;\n}\n.experimental::before {\n  border-right: 6px solid #444;\n  border-top: 6px solid transparent;\n  border-bottom: 6px solid transparent;\n  content: ' ';\n  height: 0;\n  margin-left: calc(100% + 5px);\n  width: 0;\n}\n.experimental::after {\n  background-color: #444;\n  border-radius: 5px;\n  color: #fff;\n  content: 'Semantic Versioning does not apply to experimental APIs';\n  font-size: 1rem;\n  font-weight: 400;\n  letter-spacing: 0;\n  line-height: 1em;\n  margin-left: calc(100% + 11px);\n  overflow: hidden;\n  padding: 7px 10px 5px;\n  text-decoration: none;\n  text-transform: none;\n  z-index: 1;\n  white-space: nowrap;\n}\n\n#content-container .standalone-link {\n  float: right;\n  font-size: 1.3rem;\n  margin-top: 0.5em;\n  text-decoration: none;\n}\n\n#sidebar-container {\n  font-family: 'Sofia Pro', sans-serif;\n  font-size: 1.2rem;\n  position: sticky;\n  top: $headerHeight;\n  padding-top: 86px;\n\n  height: calc(100vh - $headerHeight);\n  overflow-y: auto;\n}\n#sidebar-container ul {\n  list-style: none;\n}\n#sidebar-container .sidebar-list > li {\n  margin-bottom: 0.5em;\n}\n#sidebar-container li {\n  margin-bottom: 0.3em;\n}\n#sidebar-container > ul {\n  list-style: none;\n}\n#sidebar-container > ul ul {\n  margin: 0.5em 0 0.5em 3em;\n}\n#sidebar-container .sidebar-button {\n  border: none;\n  display: none;\n  font-size: 1.2rem;\n  text-transform: uppercase;\n}\n#sidebar-container .sidebar-button::after {\n  content: '\\25bc';\n  margin-left: 1em;\n}\n#sidebar-container.active .sidebar-button::after {\n  content: '\\25b2';\n}\n\n#docs-container {\n  margin-bottom: 5em;\n  margin-top: 1.5em;\n}\n#docs-container .anchor {\n  background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMS4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDQ4Mi44IDQ4Mi44IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0ODIuOCA0ODIuODsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiPgo8Zz4KCTxnPgoJCTxwYXRoIGQ9Ik0yNTUuMiwyMDkuM2MtNS4zLDUuMy01LjMsMTMuOCwwLDE5LjFjMjEuOSwyMS45LDIxLjksNTcuNSwwLDc5LjRsLTExNSwxMTVjLTIxLjksMjEuOS01Ny41LDIxLjktNzkuNCwwbC0xNy4zLTE3LjMgICAgYy0yMS45LTIxLjktMjEuOS01Ny41LDAtNzkuNGwxMTUtMTE1YzUuMy01LjMsNS4zLTEzLjgsMC0xOS4xcy0xMy44LTUuMy0xOS4xLDBsLTExNSwxMTVDOC43LDMyMi43LDAsMzQzLjYsMCwzNjUuOCAgICBjMCwyMi4yLDguNiw0My4xLDI0LjQsNTguOGwxNy4zLDE3LjNjMTYuMiwxNi4yLDM3LjUsMjQuMyw1OC44LDI0LjNzNDIuNi04LjEsNTguOC0yNC4zbDExNS0xMTVjMzIuNC0zMi40LDMyLjQtODUuMiwwLTExNy42ICAgIEMyNjkuMSwyMDQsMjYwLjUsMjA0LDI1NS4yLDIwOS4zeiIgZmlsbD0iIzAwMDAwMCIvPgoJCTxwYXRoIGQ9Ik00NTguNSw1OC4ybC0xNy4zLTE3LjNjLTMyLjQtMzIuNC04NS4yLTMyLjQtMTE3LjYsMGwtMTE1LDExNWMtMzIuNCwzMi40LTMyLjQsODUuMiwwLDExNy42YzUuMyw1LjMsMTMuOCw1LjMsMTkuMSwwICAgIHM1LjMtMTMuOCwwLTE5LjFjLTIxLjktMjEuOS0yMS45LTU3LjUsMC03OS40bDExNS0xMTVjMjEuOS0yMS45LDU3LjUtMjEuOSw3OS40LDBsMTcuMywxNy4zYzIxLjksMjEuOSwyMS45LDU3LjUsMCw3OS40bC0xMTUsMTE1ICAgIGMtNS4zLDUuMy01LjMsMTMuOCwwLDE5LjFjMi42LDIuNiw2LjEsNCw5LjUsNHM2LjktMS4zLDkuNS00bDExNS0xMTVjMTUuNy0xNS43LDI0LjQtMzYuNiwyNC40LTU4LjggICAgQzQ4Mi44LDk0LjgsNDc0LjIsNzMuOSw0NTguNSw1OC4yeiIgZmlsbD0iIzAwMDAwMCIvPgoJPC9nPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=);\n  background-position: center;\n  background-repeat: no-repeat;\n  content: ' ';\n  display: none;\n  float: left;\n  height: 1em;\n  margin-left: -24px;\n  vertical-align: middle;\n  width: 16px;\n}\n#docs-container h1,\n#docs-container h2,\n#docs-container h3,\n#docs-container h4 {\n  margin-left: -24px;\n  padding-left: 24px;\n}\n#docs-container h1:first-of-type {\n  margin-top: 0;\n}\n#docs-container .table-of-contents {\n  column-count: 3;\n  width: 100%;\n}\n#docs-container .table-of-contents nav {\n  display: inline-block;\n  width: 100%;\n}\n#docs-container .table-of-contents li {\n  list-style: none;\n  margin-bottom: 0.5em;\n}\n#content-container {\n  margin-bottom: 6.67em;\n  max-width: var(--width-readable);\n}\n#content-container > blockquote:before {\n  color: #d9dce1;\n  content: '\\201C';\n  display: inline;\n  float: left;\n  font-size: 4em;\n  margin-left: -0.5em;\n  margin-top: -0.5em;\n}\n#content-container > blockquote {\n  font-style: italic;\n  padding: 1em;\n}\n#content-container > blockquote > p:last-child {\n  margin-bottom: 0;\n}\n#content-container > ul,\n#content-container > ol {\n  padding-left: 2em;\n}\n#content-container > ul > li {\n  list-style: disc;\n}\n#content-container > ol > li {\n  list-style: decimal;\n}\n#content-container h2:after {\n  border-bottom: 1px solid #d9dce1;\n  content: ' ';\n  display: block;\n}\n#content-container li p {\n  margin-bottom: 0;\n}\n#content-container h1,\n#content-container h2,\n#content-container h3,\n#content-container h4,\n#content-container h5,\n#content-container h6 {\n  margin-top: 2em;\n}\n#toolbar-toolbar {\n  margin-bottom: 2em;\n}\n#toolbar-editor {\n  display: none;\n}\n\n#content-container .ql-editor p {\n  font-size: 1em;\n}\n\n/*** Playground ***/\n\n#playground-container .codepen {\n  height: 500px;\n}\n\n/*** Blog Container ***/\n\n#blog-container {\n  margin-top: 5.5em;\n  max-width: var(--width-readable);\n}\n#blog-container .post-meta {\n  margin-bottom: 2.78em;\n  text-align: center;\n}\n#blog-container .post-meta,\n#blog-container .post-meta a {\n  color: #939598;\n}\n#blog-container h1 {\n  font-size: 3.5rem;\n  margin-bottom: 0;\n  text-align: center;\n}\n#blog-container .post-item hr {\n  margin-bottom: 5.5em;\n  margin-top: 2.78em;\n}\n#blog-container p a,\n#blog-container li a {\n  color: #25408f;\n  text-decoration: underline;\n}\n#blog-container img {\n  display: block;\n  margin: auto;\n}\n#blog-container ul li {\n  list-style: disc;\n  margin-left: 2em;\n}\n#blog-container ol li {\n  list-style: decimal;\n  margin-left: 2em;\n}\n#blog-container .more-link {\n  display: inline-block;\n  font-weight: bold;\n  margin-top: 2.78em;\n}\n\n/*** Responsive Rules ***/\n\n@media (max-width: 1023px) {\n  .navbar-open {\n    display: inline-block;\n  }\n  .navbar-drop.active {\n    margin-bottom: -22em;\n    transition: margin-bottom 0.4s ease-out 0s;\n  }\n  .navbar-list .navbar-item,\n  .navbar-list .download-item {\n    display: none;\n  }\n  #users-container h1 {\n    font-size: 4rem;\n  }\n  #laptop-container {\n    padding: 22px 22px 0 22px;\n  }\n  #snow-wrapper .toolbar {\n    padding: 2.5% 8% 5%;\n  }\n  #snow-wrapper .ql-editor {\n    padding: 0 10%;\n  }\n  .feature .details {\n    padding: 1em;\n  }\n  .feature .details > span {\n    line-height: 1.6;\n    padding-right: 0;\n  }\n  .experimental::after {\n    white-space: normal;\n  }\n}\n\n@media (max-width: 767px) {\n  html {\n    font-size: 10px;\n  }\n  .navbar-open,\n  .navbar-close {\n    top: 4em;\n  }\n  .logo {\n    height: 5.3em;\n    width: 8.5em;\n  }\n  #above-container {\n    padding-top: 0;\n  }\n  #above-container > .container {\n    height: 600px;\n  }\n  #announcement-container {\n    display: none;\n  }\n  #users-container {\n    padding-top: 2em;\n  }\n  #users-container .prev {\n    padding-right: 0;\n  }\n  #users-container .next {\n    padding-left: 0;\n  }\n  #users-container .tip {\n    font-size: 1.8em;\n  }\n  #users-container .shaft {\n    display: none;\n  }\n  #users-container h1 {\n    font-size: 2em;\n    margin-bottom: 0.5em;\n    padding: 0 1.2em;\n  }\n  #users-container h2 {\n    font-size: 3rem;\n  }\n  #logo-container {\n    margin: 2em 0;\n  }\n  #logo-container li:first-child {\n    display: block;\n    margin-bottom: 15px;\n  }\n  #logo-container li:not(:first-child) {\n    margin: 0 1em;\n    width: 2em;\n  }\n  #logo-container ul {\n    margin-bottom: 0;\n  }\n  #logo-container svg {\n    max-height: 2em;\n    max-width: 2em;\n  }\n  #demo-container {\n    padding-left: 0;\n    padding-right: 0;\n  }\n  #demo-container .ql-editor {\n    font-size: 1em;\n    padding: 20px;\n  }\n  .demo-active #bubble-wrapper,\n  .demo-active #snow-wrapper,\n  .demo-active #full-wrapper {\n    height: 455px;\n  }\n  #bubble-wrapper,\n  #snow-wrapper,\n  #full-wrapper {\n    height: 495px;\n  }\n  #snow-wrapper .toolbar .ql-font,\n  #snow-wrapper .toolbar .ql-formats:nth-child(3),\n  #snow-wrapper .toolbar .ql-formats:nth-child(5) {\n    display: none;\n  }\n\n  #above-container:not(.demo-active) #laptop-container:hover {\n    bottom: -105px;\n  }\n  #laptop-container {\n    bottom: -110px;\n  }\n  #camera-container {\n    height: 20px;\n    line-height: 16px;\n    margin-left: -60px;\n    width: 120px;\n\n    .camera {\n      height: 20px;\n      width: 20px;\n\n      .dot {\n        height: 6px;\n        width: 6px;\n      }\n    }\n  }\n  #detail-container .action {\n    margin-top: 3em;\n  }\n  #detail-container h1 {\n    font-size: 1.5em;\n    padding: 0 1em;\n  }\n  #features-container {\n    margin-bottom: 6em;\n    padding-top: 0;\n  }\n  #features-container hr {\n    display: none;\n  }\n  #features-container .feature {\n    padding: 1em;\n  }\n  #features-container .feature.columns:last-child {\n    border-left: none;\n  }\n  #features-container #github-wrapper {\n    bottom: -7em;\n    position: absolute;\n    width: 23em;\n    left: 50%;\n    margin-left: -11.5em;\n  }\n  #features-container .feature.row {\n    margin-top: 0;\n  }\n  .feature .details > h2 {\n    font-size: 1em;\n    margin-bottom: 0.75em;\n  }\n  .feature .action-link {\n    margin-top: 1em;\n  }\n  .feature.columns,\n  .feature.row .columns {\n    width: 100%;\n  }\n  .feature.row {\n    display: flex;\n    flex-direction: column-reverse;\n  }\n  footer {\n    font-size: 1rem;\n    padding-top: 5em;\n  }\n  footer h1 {\n    font-size: 2.5rem;\n    padding: 0 1.3em;\n  }\n}\n@media (max-width: 550px) {\n  #bubble-wrapper,\n  #snow-wrapper,\n  #full-wrapper {\n    height: 475px;\n  }\n  #snow-wrapper .toolbar .ql-formats:nth-child(4),\n  #snow-wrapper .toolbar .ql-formats:nth-child(6) {\n    display: none;\n  }\n  #features-container {\n    margin-bottom: 10em;\n  }\n  #features-container #github-wrapper {\n    margin-left: -10em;\n    width: 20em;\n  }\n  #docs-wrapper {\n    position: inherit;\n  }\n  #docs-container {\n    margin-top: 7em;\n  }\n  #docs-container .table-of-contents {\n    column-count: 2;\n  }\n  #sidebar-container {\n    background-color: #fff;\n    left: 0;\n    margin-top: 0;\n    max-height: 8em;\n    overflow: hidden;\n    padding: 2em 0 0;\n    height: auto;\n    right: 0;\n    text-align: center;\n    transition:\n      max-height 800ms ease-in-out 0s,\n      background-color 500ms linear 0s;\n  }\n  #sidebar-container.active {\n    max-height: none;\n  }\n  #sidebar-container .sidebar-button {\n    display: block;\n    padding: 2em;\n    width: 100%;\n  }\n  #sidebar-container .sidebar-list {\n    font-size: 2rem;\n    margin-bottom: 0;\n    margin-top: 0;\n    position: relative;\n  }\n  #sidebar-container .sidebar-list ul {\n    display: none;\n  }\n  #pagination-container .prev,\n  #pagination-container .next {\n    margin-bottom: 1em;\n  }\n}\n@media (max-width: 400px) {\n  #laptop-container {\n    width: 90%;\n  }\n}\n\n@media (min-width: 550px) {\n  #sidebar-container > ul > li > a {\n    border-left: 3px solid transparent;\n    padding-left: 1.15em;\n    padding-top: 2px;\n  }\n  #sidebar-container li.active > a {\n    color: #1d1e30;\n  }\n  #sidebar-container a {\n    color: #797b82;\n    display: block;\n  }\n  #sidebar-container a:hover {\n    color: #1d1e30;\n  }\n  #sidebar-container > ul > li.active > a {\n    border-left: 3px solid #1d1e30;\n    padding-left: 1.15em;\n  }\n}\n@media (min-width: 768px) {\n  .container {\n    width: 90%;\n  }\n  .navbar-list .navbar-link:before,\n  .action-link:before {\n    background-color: #000;\n    bottom: -18px;\n    content: '';\n    height: 3px;\n    left: 0;\n    position: absolute;\n    transform: scaleX(0);\n    transition: all 0.3s ease-in-out 0s;\n    visibility: hidden;\n    width: 100%;\n  }\n  .navbar-list .navbar-link:hover:before,\n  .action-link:hover:before {\n    transform: scaleX(1);\n    visibility: visible;\n  }\n  #sidebar-container .search-item {\n    display: block;\n  }\n  #docs-container h1:hover .anchor,\n  #docs-container h2:hover .anchor,\n  #docs-container h3:hover .anchor,\n  #docs-container h4:hover .anchor {\n    display: inline-block;\n  }\n}\n@media (min-width: 1024px) {\n  .navbar-list li {\n    margin-right: 2.5%;\n  }\n}\n\n@media (min-width: 1200px) {\n  header .action {\n    padding: 16px 26px;\n  }\n  .navbar-list li {\n    margin-right: 3.5%;\n  }\n}\n"
  },
  {
    "path": "packages/website/src/pages/variables.scss",
    "content": ":root {\n  --color-accent: #f2d123;\n  --color-accent-emphasis: #e2b810;\n\n  --color-bg-inset: #f0efea;\n  --color-bg-inset-emphasis: #e1ded1;\n\n  --docsearch-searchbox-background: var(--color-bg-inset);\n  --docsearch-primary-color: var(--color-accent);\n  --docsearch-key-shadow: none;\n  --docsearch-key-gradient: transparent;\n\n  --width-readable: 800px;\n}\n\n[data-accent-color='yellow'] {\n  // Override Radix styles\n  --accent-9: var(--color-accent) !important;\n}\n\n.radix-themes {\n  --default-font-size: 18px;\n}\n"
  },
  {
    "path": "packages/website/src/playground/custom-formats/index.css",
    "content": "/* Set default font-family */\n#editor {\n  font-family: 'Aref Ruqaa';\n  font-size: 18px;\n  height: 375px;\n}\n\n/* Set dropdown font-families */\n#toolbar .ql-font span[data-label='Aref Ruqaa']::before {\n  font-family: 'Aref Ruqaa';\n}\n#toolbar .ql-font span[data-label='Mirza']::before {\n  font-family: 'Mirza';\n}\n#toolbar .ql-font span[data-label='Roboto']::before {\n  font-family: 'Roboto';\n}\n\n/* Set content font-families */\n.ql-font-mirza {\n  font-family: 'Mirza';\n}\n.ql-font-roboto {\n  font-family: 'Roboto';\n}\n/* We do not set Aref Ruqaa since it is the default font */\n"
  },
  {
    "path": "packages/website/src/playground/custom-formats/index.html",
    "content": "<link rel=\"stylesheet\" href=\"/index.css\" />\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Aref+Ruqaa|Mirza|Roboto\" />\n\n<div id=\"toolbar\">\n  <select class=\"ql-font\">\n    <option selected>Aref Ruqaa</option>\n    <option value=\"mirza\">Mirza</option>\n    <option value=\"roboto\">Roboto</option>\n  </select>\n</div>\n\n<div id=\"editor\">\n  <p>When Mr. Bilbo Baggins of Bag End announced that he would shortly be celebrating his eleventy-first birthday with a\n    party of special magnificence, there was much talk and excitement in Hobbiton.</p>\n  <p><br></p>\n  <p>Bilbo was very rich and very peculiar, and had been the wonder of the Shire for sixty years, ever since his\n    remarkable disappearance and unexpected return. The riches he had brought back from his travels had now become a\n    local legend, and it was popularly believed, whatever the old folk might say, that the Hill at Bag End was full of\n    tunnels stuffed with treasure. And if that was not enough for fame, there was also his prolonged vigour to marvel\n    at. Time wore on, but it seemed to have little effect on Mr. Baggins. At ninety he was much the same as at fifty. At\n    ninety-nine they began to call him well-preserved, but unchanged would have been nearer the mark. There were some\n    that shook their heads and thought this was too much of a good thing; it seemed unfair that anyone should possess\n    (apparently) perpetual youth as well as (reputedly) inexhaustible wealth.</p>\n  <p>\"It will have to be paid for,\" they said. \"It isn\"t natural, and trouble will come of it!\"\n    But so far trouble had not come; and as Mr. Baggins was generous with his money, most people were willing to forgive\n    him his oddities and his good fortune. He remained on visiting terms with his relatives (except, of course, the\n    Sackville-Bagginses), and he had many devoted admirers among the hobbits of poor and unimportant families. But he\n    had no close friends, until some of his younger cousins began to grow up.</p>\n\n</div>\n\n<script src=\"/index.js\"></script>"
  },
  {
    "path": "packages/website/src/playground/custom-formats/index.js",
    "content": "// Add fonts to whitelist\nconst Font = Quill.import('formats/font');\n// We do not add Aref Ruqaa since it is the default\nFont.whitelist = ['mirza', 'roboto'];\nQuill.register(Font, true);\n\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: '#toolbar',\n  },\n  theme: 'snow',\n});\n"
  },
  {
    "path": "packages/website/src/playground/custom-formats/playground.json",
    "content": "{\n  \"template\": \"static\",\n  \"externalResources\": [\n    \"{{site.highlightjs}}/highlight.min.js\",\n    \"{{site.highlightjs}}/styles/atom-one-dark.min.css\",\n    \"{{site.katex}}/katex.min.js\",\n    \"{{site.katex}}/katex.min.css\",\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.bubble.css\",\n    \"{{site.cdn}}/quill.js\"\n  ]\n}\n"
  },
  {
    "path": "packages/website/src/playground/form/index.css",
    "content": ".container {\n  width: 500px;\n  max-width: 100%;\n}\n\n.form-group {\n  margin-bottom: 12px;\n}\n\nlabel {\n  display: block;\n  margin-bottom: 4px;\n}\n\ninput {\n  border: 1px solid #ccc;\n}\n\ninput,\n.ql-editor {\n  padding: 4px;\n  font-size: 14px;\n}\n\n#editor {\n  height: 130px;\n}\n"
  },
  {
    "path": "packages/website/src/playground/form/index.html",
    "content": "<link rel=\"stylesheet\" href=\"/index.css\" />\n\n<div class=\"container\">\n  <form action=\"https://httpbin.org/post\" method=\"post\">\n    <div class=\"form-group\">\n      <label for=\"name\">Display name</label>\n      <input id=\"name\" name=\"name\" type=\"text\">\n    </div>\n    <div class=\"form-group\">\n      <label for=\"location\">Location</label>\n      <input id=\"location\" name=\"location\" type=\"text\">\n    </div>\n    <div class=\"form-group\">\n      <label>About me</label>\n      <div id=\"editor\"></div>\n    </div>\n    <button type=\"submit\">Submit Form</button>\n    <button type=\"button\" id=\"resetForm\">Reset to Initial Data</button>\n  </form>\n</div>\n\n<script src=\"/index.js\"></script>"
  },
  {
    "path": "packages/website/src/playground/form/index.js",
    "content": "const initialData = {\n  name: 'Wall-E',\n  location: 'Earth',\n  // `about` is a Delta object\n  // Learn more at: https://quilljs.com/docs/delta\n  about: [\n    {\n      insert:\n        'A robot who has developed sentience, and is the only robot of his kind shown to be still functioning on Earth.\\n',\n    },\n  ],\n};\n\nconst quill = new Quill('#editor', {\n  modules: {\n    toolbar: [\n      ['bold', 'italic'],\n      ['link', 'blockquote', 'code-block', 'image'],\n      [{ list: 'ordered' }, { list: 'bullet' }],\n    ],\n  },\n  theme: 'snow',\n});\n\nconst resetForm = () => {\n  document.querySelector('[name=\"name\"]').value = initialData.name;\n  document.querySelector('[name=\"location\"]').value = initialData.location;\n  quill.setContents(initialData.about);\n};\n\nresetForm();\n\nconst form = document.querySelector('form');\nform.addEventListener('formdata', (event) => {\n  // Append Quill content before submitting\n  event.formData.append('about', JSON.stringify(quill.getContents().ops));\n});\n\ndocument.querySelector('#resetForm').addEventListener('click', () => {\n  resetForm();\n});\n"
  },
  {
    "path": "packages/website/src/playground/form/playground.json",
    "content": "{\n  \"template\": \"static\",\n  \"externalResources\": [\n    \"{{site.highlightjs}}/highlight.min.js\",\n    \"{{site.highlightjs}}/styles/atom-one-dark.min.css\",\n    \"{{site.katex}}/katex.min.js\",\n    \"{{site.katex}}/katex.min.css\",\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.bubble.css\",\n    \"{{site.cdn}}/quill.js\"\n  ]\n}\n"
  },
  {
    "path": "packages/website/src/playground/react/App.js",
    "content": "import React, { useRef, useState } from 'react';\nimport Editor from './Editor';\n\nconst Delta = Quill.import('delta');\n\nconst App = () => {\n  const [range, setRange] = useState();\n  const [lastChange, setLastChange] = useState();\n  const [readOnly, setReadOnly] = useState(false);\n\n  // Use a ref to access the quill instance directly\n  const quillRef = useRef();\n\n  return (\n    <div>\n      <Editor\n        ref={quillRef}\n        readOnly={readOnly}\n        defaultValue={new Delta()\n          .insert('Hello')\n          .insert('\\n', { header: 1 })\n          .insert('Some ')\n          .insert('initial', { bold: true })\n          .insert(' ')\n          .insert('content', { underline: true })\n          .insert('\\n')}\n        onSelectionChange={setRange}\n        onTextChange={setLastChange}\n      />\n      <div class=\"controls\">\n        <label>\n          Read Only:{' '}\n          <input\n            type=\"checkbox\"\n            value={readOnly}\n            onChange={(e) => setReadOnly(e.target.checked)}\n          />\n        </label>\n        <button\n          className=\"controls-right\"\n          type=\"button\"\n          onClick={() => {\n            alert(quillRef.current?.getLength());\n          }}\n        >\n          Get Content Length\n        </button>\n      </div>\n      <div className=\"state\">\n        <div className=\"state-title\">Current Range:</div>\n        {range ? JSON.stringify(range) : 'Empty'}\n      </div>\n      <div className=\"state\">\n        <div className=\"state-title\">Last Change:</div>\n        {lastChange ? JSON.stringify(lastChange.ops) : 'Empty'}\n      </div>\n    </div>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "packages/website/src/playground/react/Editor.js",
    "content": "import React, { forwardRef, useEffect, useLayoutEffect, useRef } from 'react';\n\n// Editor is an uncontrolled React component\nconst Editor = forwardRef(\n  ({ readOnly, defaultValue, onTextChange, onSelectionChange }, ref) => {\n    const containerRef = useRef(null);\n    const defaultValueRef = useRef(defaultValue);\n    const onTextChangeRef = useRef(onTextChange);\n    const onSelectionChangeRef = useRef(onSelectionChange);\n\n    useLayoutEffect(() => {\n      onTextChangeRef.current = onTextChange;\n      onSelectionChangeRef.current = onSelectionChange;\n    });\n\n    useEffect(() => {\n      ref.current?.enable(!readOnly);\n    }, [ref, readOnly]);\n\n    useEffect(() => {\n      const container = containerRef.current;\n      const editorContainer = container.appendChild(\n        container.ownerDocument.createElement('div'),\n      );\n      const quill = new Quill(editorContainer, {\n        theme: 'snow',\n      });\n\n      ref.current = quill;\n\n      if (defaultValueRef.current) {\n        quill.setContents(defaultValueRef.current);\n      }\n\n      quill.on(Quill.events.TEXT_CHANGE, (...args) => {\n        onTextChangeRef.current?.(...args);\n      });\n\n      quill.on(Quill.events.SELECTION_CHANGE, (...args) => {\n        onSelectionChangeRef.current?.(...args);\n      });\n\n      return () => {\n        ref.current = null;\n        container.innerHTML = '';\n      };\n    }, [ref]);\n\n    return <div ref={containerRef}></div>;\n  },\n);\n\nEditor.displayName = 'Editor';\n\nexport default Editor;\n"
  },
  {
    "path": "packages/website/src/playground/react/playground.json",
    "content": "{\n  \"template\": \"react\",\n  \"externalResources\": [\n    \"{{site.highlightjs}}/highlight.min.js\",\n    \"{{site.highlightjs}}/styles/atom-one-dark.min.css\",\n    \"{{site.katex}}/katex.min.js\",\n    \"{{site.katex}}/katex.min.css\",\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.bubble.css\",\n    \"{{site.cdn}}/quill.js\"\n  ],\n  \"activeFile\": \"App.js\"\n}\n"
  },
  {
    "path": "packages/website/src/playground/react/styles.css",
    "content": ".controls {\n  display: flex;\n  border: 1px solid #ccc;\n  border-top: 0;\n  padding: 10px;\n}\n\n.controls-right {\n  margin-left: auto;\n}\n\n.state {\n  margin: 10px 0;\n  font-family: monospace;\n}\n\n.state-title {\n  color: #999;\n  text-transform: uppercase;\n}\n"
  },
  {
    "path": "packages/website/src/playground/snow/index.html",
    "content": "<div id=\"editor\"></div>\n\n<script src=\"/index.js\"></script>"
  },
  {
    "path": "packages/website/src/playground/snow/index.js",
    "content": "const quill = new Quill('#editor', {\n  modules: {\n    toolbar: [\n      [{ header: [1, 2, false] }],\n      ['bold', 'italic', 'underline'],\n      ['image', 'code-block'],\n    ],\n  },\n  placeholder: 'Compose an epic...',\n  theme: 'snow', // or 'bubble'\n});\n"
  },
  {
    "path": "packages/website/src/playground/snow/playground.json",
    "content": "{\n  \"template\": \"static\",\n  \"externalResources\": [\n    \"{{site.highlightjs}}/highlight.min.js\",\n    \"{{site.highlightjs}}/styles/atom-one-dark.min.css\",\n    \"{{site.katex}}/katex.min.js\",\n    \"{{site.katex}}/katex.min.css\",\n    \"{{site.cdn}}/quill.snow.css\",\n    \"{{site.cdn}}/quill.bubble.css\",\n    \"{{site.cdn}}/quill.js\"\n  ]\n}\n"
  },
  {
    "path": "packages/website/src/utils/flattenData.js",
    "content": "function flattenData(root) {\n  const data = [];\n  const flatten = (i) => {\n    i.forEach((child) => {\n      if (child.url.includes('#')) return;\n      data.push(child);\n      if (child.children) {\n        flatten(child.children);\n      }\n    });\n  };\n\n  flatten(root);\n  return data;\n}\n\nexport default flattenData;\n"
  },
  {
    "path": "packages/website/src/utils/replaceCDN.js",
    "content": "import env from '../../env';\n\nconst replaceCDN = (value) => {\n  return value.replace(/\\{\\{site\\.(\\w+)\\}\\}/g, (_, matched) => {\n    return matched === 'cdn' ? process.env.cdn : env[matched];\n  });\n};\n\nexport default replaceCDN;\n"
  },
  {
    "path": "packages/website/src/utils/slug.js",
    "content": "import slugify from 'slugify';\n\nconst slug = text => slugify(text, { lower: true });\n\nexport default slug;\n"
  },
  {
    "path": "scripts/changelog.mjs",
    "content": "/**\n * Fetch the latest release from GitHub and prepend it to the CHANGELOG.md\n * Nothing will happen if the latest release is already in the CHANGELOG.md\n */\nimport { $ } from \"execa\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport configGit from \"./utils/configGit.mjs\";\n\nconst changelogFilename = \"CHANGELOG.md\";\n\nconst changeLogFilePath = join(\n  dirname(fileURLToPath(import.meta.url)),\n  \"..\",\n  changelogFilename\n);\n\nconst currentChangeLog = await readFile(changeLogFilePath, \"utf-8\");\n\nconst { stdout } =\n  await $`gh release list --exclude-drafts --json=tagName,publishedAt,name,isLatest`;\n\nconst release = JSON.parse(stdout).find((release) => release.isLatest);\n\nif (currentChangeLog.includes(`# ${release.tagName}`)) {\n  process.exit(0);\n}\n\nawait configGit();\n\nconst normalizeReleaseNote = (note) => {\n  const ignoreSections = [\n    \"## new contributors\",\n    \"## all changes\",\n    \"## other changes\",\n  ];\n  ignoreSections.forEach((section) => {\n    const index = note.toLowerCase().indexOf(section);\n    if (index > -1) {\n      note = note.slice(0, index).replace(/#\\s*$/, \"\");\n    }\n  });\n\n  return note\n    .replace(/by @([-\\w]+)/g, (_, username) => {\n      return `by [@${username}](https://github.com/${username})`;\n    })\n    .trim();\n};\n\nconst formatDate = (date) => {\n  const str = date.toISOString();\n  return str.substring(0, str.indexOf(\"T\"));\n};\n\nconst { body } = JSON.parse(\n  (await $`gh release view ${release.tagName} --json=body`).stdout\n);\n\nconst note = `# ${release.tagName} (${formatDate(new Date(release.publishedAt))})\\n\\n${normalizeReleaseNote(body)}\\n\\n[All changes](https://github.com/slab/quill/releases/tag/${release.tagName})\\n`;\n\nawait writeFile(changeLogFilePath, `${note}\\n${currentChangeLog}`);\n\nawait $`git add ${changelogFilename}`;\nconst message = `Update ${changelogFilename}: ${release.tagName}`;\nawait $`git commit -m ${message}`;\nawait $`git push origin main`;\n"
  },
  {
    "path": "scripts/release.js",
    "content": "#!/usr/bin/env node\n\nconst exec = require(\"node:child_process\").execSync;\nconst fs = require(\"node:fs\");\nconst path = require(\"node:path\");\nconst crypto = require(\"node:crypto\");\nconst { parseArgs } = require(\"node:util\");\n\nconst args = parseArgs({\n  options: {\n    version: { type: \"string\" },\n    \"dry-run\": { type: \"boolean\", default: false },\n  },\n});\n\nconst dryRun = args.values[\"dry-run\"];\n\nif (dryRun) {\n  console.log('Running in \"dry-run\" mode');\n}\n\nconst exitWithError = (message) => {\n  console.error(`Exit with error: ${message}`);\n  process.exit(1);\n};\n\nif (!process.env.CI) {\n  exitWithError(\"The script should only be run in CI\");\n}\n\nexec('echo \"//registry.npmjs.org/:_authToken=${NPM_TOKEN}\" > ~/.npmrc');\n\nasync function main() {\n  const configGit = (await import(\"./utils/configGit.mjs\")).default;\n  await configGit();\n\n  /*\n   * Check that the git working directory is clean\n   */\n  if (exec(\"git status --porcelain\").length) {\n    exitWithError(\n      \"Make sure the git working directory is clean before releasing\"\n    );\n  }\n\n  /*\n   * Check that the version is valid. Also extract the dist-tag from the version.\n   */\n  const [version, distTag] = (() => {\n    const inputVersion = args.values.version;\n    if (!inputVersion) {\n      exitWithError('Missing required argument: \"--version <version>\"');\n    }\n\n    if (inputVersion === \"experimental\") {\n      const randomId = crypto\n        .randomBytes(Math.ceil(9 / 2))\n        .toString(\"hex\")\n        .slice(0, 9);\n\n      return [\n        `0.0.0-experimental-${randomId}-${new Date()\n          .toISOString()\n          .slice(0, 10)\n          .replace(/-/g, \"\")}`,\n        \"experimental\",\n      ];\n    }\n\n    const match = inputVersion.match(\n      /^(?:[0-9]+\\.){2}(?:[0-9]+)(?:-(dev|alpha|beta|rc)\\.[0-9]+)?$/\n    );\n    if (!match) {\n      exitWithError(`Invalid version: ${inputVersion}`);\n    }\n\n    return [inputVersion, match[1] || \"latest\"];\n  })();\n\n  /*\n   * Get the current version\n   */\n  const currentVersion = JSON.parse(\n    fs.readFileSync(\"package.json\", \"utf-8\")\n  ).version;\n  console.log(\n    `Releasing with version: ${currentVersion} -> ${version} and dist-tag: ${distTag}`\n  );\n\n  /*\n   * Bump npm versions\n   */\n  exec(`npm version ${version} --workspaces --force`);\n  exec(\"git add **/package.json\");\n  exec(`npm version ${version} --include-workspace-root --force`);\n\n  const pushCommand = `git push origin ${process.env.GITHUB_REF_NAME} --follow-tags`;\n  if (distTag === \"experimental\") {\n    console.log(`Skipping: \"${pushCommand}\" for experimental version`);\n  } else {\n    if (dryRun) {\n      console.log(`Skipping: \"${pushCommand}\" in dry-run mode`);\n    } else {\n      exec(pushCommand);\n    }\n  }\n\n  /*\n   * Build Quill package\n   */\n  console.log(\"Building Quill\");\n  exec(\"npm run build:quill\");\n\n  /*\n   * Publish Quill package\n   */\n  console.log(\"Publishing Quill\");\n  const distFolder = \"packages/quill/dist\";\n  if (\n    JSON.parse(fs.readFileSync(path.join(distFolder, \"package.json\"), \"utf-8\"))\n      .version !== version\n  ) {\n    exitWithError(\n      \"Version mismatch between package.json and dist/package.json\"\n    );\n  }\n\n  const readme = fs.readFileSync(\"README.md\", \"utf-8\");\n  fs.writeFileSync(path.join(distFolder, \"README.md\"), readme);\n\n  exec(`npm publish --tag ${distTag}${dryRun ? \" --dry-run\" : \"\"}`, {\n    cwd: distFolder,\n  });\n\n  /*\n   * Create GitHub release\n   */\n  if (distTag === \"experimental\") {\n    console.log(\"Skipping GitHub release for experimental version\");\n  } else {\n    const prereleaseFlag = distTag === \"latest\" ? \"--latest\" : \" --prerelease\";\n    const releaseCommand = `gh release create v${version} ${prereleaseFlag} -t \"Version ${version}\" --generate-notes`;\n    if (dryRun) {\n      console.log(`Skipping: \"${releaseCommand}\" in dry-run mode`);\n    } else {\n      exec(releaseCommand);\n    }\n  }\n\n  /*\n   * Create npm package tarball\n   */\n  exec(\"npm pack\", { cwd: distFolder });\n}\n\nmain();\n"
  },
  {
    "path": "scripts/utils/configGit.mjs",
    "content": "import { $ } from \"execa\";\n\nasync function configGit() {\n  await $`git config --global user.name ${\"Zihua Li\"}`;\n  await $`git config --global user.email ${\"635902+luin@users.noreply.github.com\"}`;\n}\n\nexport default configGit;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"ts-node\": {\n    \"compilerOptions\": {\n      \"esModuleInterop\": true,\n      \"module\": \"commonjs\"\n    }\n  },\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"module\": \"ES2020\",\n    \"moduleResolution\": \"node\",\n    \"noEmit\": true,\n    \"strictNullChecks\": true,\n    \"noImplicitAny\": true,\n    \"noUnusedLocals\": true\n  },\n  \"include\": [\"./**/*\"]\n}\n"
  }
]